UNPKG

@electric-sql/pglite-sync

Version:

ElectricSQL Sync for PGlite

48 lines 8.77 kB
"use strict";var k=Object.defineProperty;var Z=Object.getOwnPropertyDescriptor;var ee=Object.getOwnPropertyNames;var te=Object.prototype.hasOwnProperty;var se=(t,s)=>{for(var e in s)k(t,e,{get:s[e],enumerable:!0})},ae=(t,s,e,a)=>{if(s&&typeof s=="object"||typeof s=="function")for(let o of ee(s))!te.call(t,o)&&o!==e&&k(t,o,{get:()=>s[o],enumerable:!(a=Z(s,o))||a.enumerable});return t};var ne=t=>ae(k({},"__esModule",{value:!0}),t);var ce={};se(ce,{electricSync:()=>re});module.exports=ne(ce);var A=require("@electric-sql/client"),J=require("@electric-sql/experimental");var oe="subscriptions_metadata";async function K({pg:t,metadataSchema:s,subscriptionKey:e}){let a=await t.query(` SELECT key, shape_metadata, last_lsn FROM ${j(s)} WHERE key = $1 `,[e]);if(a.rows.length===0)return null;if(a.rows.length>1)throw new Error(`Multiple subscriptions found for key: ${e}`);let o=a.rows[0];if(typeof o.last_lsn=="string")return{...o,last_lsn:BigInt(o.last_lsn)};throw new Error(`Invalid last_lsn type: ${typeof o.last_lsn}`)}async function q({pg:t,metadataSchema:s,subscriptionKey:e,shapeMetadata:a,lastLsn:o,debug:u}){u&&console.log("updating subscription state",e,a,o),await t.query(` INSERT INTO ${j(s)} (key, shape_metadata, last_lsn) VALUES ($1, $2, $3) ON CONFLICT(key) DO UPDATE SET shape_metadata = EXCLUDED.shape_metadata, last_lsn = EXCLUDED.last_lsn; `,[e,a,o.toString()])}async function F({pg:t,metadataSchema:s,subscriptionKey:e}){await t.query(`DELETE FROM ${j(s)} WHERE key = $1`,[e])}async function W({pg:t,metadataSchema:s}){await t.exec(` SET ${s}.syncing = false; CREATE SCHEMA IF NOT EXISTS "${s}"; CREATE TABLE IF NOT EXISTS ${j(s)} ( key TEXT PRIMARY KEY, shape_metadata JSONB NOT NULL, last_lsn TEXT NOT NULL ); `)}function j(t){return`"${t}"."${oe}"`}async function H({pg:t,table:s,schema:e="public",message:a,mapColumns:o,primaryKey:u,debug:y}){let f=o?v(o,a):a.value;switch(a.headers.operation){case"insert":{y&&console.log("inserting",f);let l=Object.keys(f);return await t.query(` INSERT INTO "${e}"."${s}" (${l.map(n=>'"'+n+'"').join(", ")}) VALUES (${l.map((n,g)=>"$"+(g+1)).join(", ")}) `,l.map(n=>f[n]))}case"update":{y&&console.log("updating",f);let l=Object.keys(f).filter(n=>!u.includes(n));return l.length===0?void 0:await t.query(` UPDATE "${e}"."${s}" SET ${l.map((n,g)=>'"'+n+'" = $'+(g+1)).join(", ")} WHERE ${u.map((n,g)=>'"'+n+'" = $'+(l.length+g+1)).join(" AND ")} `,[...l.map(n=>f[n]),...u.map(n=>f[n])])}case"delete":return y&&console.log("deleting",f),await t.query(` DELETE FROM "${e}"."${s}" WHERE ${u.map((l,n)=>'"'+l+'" = $'+(n+1)).join(" AND ")} `,[...u.map(l=>f[l])])}}async function X({pg:t,table:s,schema:e="public",messages:a,mapColumns:o,debug:u}){u&&console.log("applying messages with json_to_recordset");let y=a.map(n=>o?v(o,n):n.value),f=(await t.query(` SELECT column_name, udt_name, data_type FROM information_schema.columns WHERE table_name = $1 AND table_schema = $2 `,[s,e])).rows.filter(n=>Object.prototype.hasOwnProperty.call(y[0],n.column_name)),l=1e4;for(let n=0;n<y.length;n+=l){let g=y.slice(n,n+l);await t.query(` INSERT INTO "${e}"."${s}" SELECT x.* from json_to_recordset($1) as x(${f.map(M=>`${M.column_name} ${M.udt_name.replace(/^_/,"")}`+(M.data_type==="ARRAY"?"[]":"")).join(", ")}) `,[g])}u&&console.log(`Inserted ${a.length} rows using json_to_recordset`)}async function Y({pg:t,table:s,schema:e="public",messages:a,mapColumns:o,debug:u}){u&&console.log("applying messages with COPY");let y=a.map(g=>o?v(o,g):g.value),f=Object.keys(y[0]),l=y.map(g=>f.map(M=>{let T=g[M];return typeof T=="string"&&(T.includes(",")||T.includes('"')||T.includes(` `))?`"${T.replace(/"/g,'""')}"`:T===null?"\\N":T}).join(",")).join(` `),n=new Blob([l],{type:"text/csv"});await t.query(` COPY "${e}"."${s}" (${f.map(g=>`"${g}"`).join(", ")}) FROM '/dev/blob' WITH (FORMAT csv, NULL '\\N') `,[],{blob:n}),u&&console.log(`Inserted ${a.length} rows using COPY`)}function v(t,s){if(typeof t=="function")return t(s);let e={};for(let[a,o]of Object.entries(t))e[a]=s.value[o];return e}async function ie(t,s){let e=s?.debug??!1,a=s?.metadataSchema??"electric",o=[],u=new Map,y=!1,f=async()=>{y||(y=!0,await W({pg:t,metadataSchema:a}))},l=async({key:p,shapes:m,useCopy:V=!1,initialInsertMethod:C="insert",onInitialSync:D})=>{let N=!1;await f(),Object.values(m).filter(i=>!i.onMustRefetch).forEach(i=>{if(u.has(i.table))throw new Error("Already syncing shape for table "+i.table);u.set(i.table)});let w=null;p!==null&&(w=await K({pg:t,metadataSchema:a,subscriptionKey:p}),e&&w&&console.log("resuming from subscription state",w));let z=w===null;V&&C==="insert"&&(C="csv",console.warn("The useCopy option is deprecated and will be removed in a future version. Use initialInsertMethod instead."));let x=!z||C==="insert",U=!1,P=new Map(Object.keys(m).map(i=>[i,new Map])),_=new Map(Object.keys(m).map(i=>[i,BigInt(-1)])),I=new Set,B=w?.last_lsn??BigInt(-1),R=new AbortController;Object.values(m).filter(i=>!!i.shape.signal).forEach(i=>{i.shape.signal.addEventListener("abort",()=>R.abort(),{once:!0})});let O=new J.MultiShapeStream({shapes:Object.fromEntries(Object.entries(m).map(([i,E])=>{let b=w?.shape_metadata[i];return[i,{...E.shape,...b?{offset:b.offset,handle:b.handle}:{},signal:R.signal}]}))}),Q=async i=>{let E=new Map(Object.keys(m).map(b=>[b,[]]));for(let[b,h]of P.entries()){let r=E.get(b);for(let c of h.keys())if(c<=i){for(let S of h.get(c))r.push(S);h.delete(c)}}await t.transaction(async b=>{e&&console.time("commit"),await b.exec(`SET LOCAL ${a}.syncing = true;`);for(let[h,r]of E.entries()){let c=m[h],S=r;if(I.has(h)&&(e&&console.log("truncating table",c.table),c.onMustRefetch?await c.onMustRefetch(b):await b.exec(`DELETE FROM ${c.table};`),I.delete(h)),!x){let d=[],$=[],G=!1;for(let L of S)!G&&L.headers.operation==="insert"?d.push(L):(G=!0,$.push(L));d.length>0&&C==="csv"&&$.unshift(d.pop()),S=$,d.length>0&&(await(C==="json"?X:Y)({pg:b,table:c.table,schema:c.schema,messages:d,mapColumns:c.mapColumns,primaryKey:c.primaryKey,debug:e}),x=!0)}for(let d of S)await H({pg:b,table:c.table,schema:c.schema,message:d,mapColumns:c.mapColumns,primaryKey:c.primaryKey,debug:e})}p&&await q({pg:b,metadataSchema:a,subscriptionKey:p,shapeMetadata:Object.fromEntries(Object.keys(m).map(h=>[h,{handle:O.shapes[h].shapeHandle,offset:O.shapes[h].lastOffset}])),lastLsn:i,debug:e}),N&&await b.rollback()}),e&&console.timeEnd("commit"),D&&!U&&O.isUpToDate&&(D(),U=!0)};return O.subscribe(async i=>{if(N)return;e&&console.log("received messages",i.length),i.forEach(r=>{let c=_.get(r.shape)??BigInt(-1);if((0,A.isChangeMessage)(r)){let S=P.get(r.shape),d=typeof r.headers.lsn=="string"?BigInt(r.headers.lsn):BigInt(0);if(d<=c)return;let $=r.headers.last??!1;S.has(d)||S.set(d,[]),S.get(d).push(r),$&&_.set(r.shape,d)}else if((0,A.isControlMessage)(r))switch(r.headers.control){case"up-to-date":{if(e&&console.log("received up-to-date",r),typeof r.headers.global_last_seen_lsn!="string")throw new Error("global_last_seen_lsn is not a string");let S=BigInt(r.headers.global_last_seen_lsn);if(S<=c)return;_.set(r.shape,S);break}case"must-refetch":{e&&console.log("received must-refetch",r),P.get(r.shape).clear(),_.set(r.shape,BigInt(-1)),I.add(r.shape);break}}});let E=Array.from(_.values()).reduce((r,c)=>c<r?c:r),b=E>B,h=E>=B&&I.size>0;(b||h)&&(Q(E),await new Promise(r=>setTimeout(r)))}),o.push({stream:O,aborter:R}),{unsubscribe:()=>{e&&console.log("unsubscribing"),N=!0,O.unsubscribeAll(),R.abort();for(let i of Object.values(m))u.delete(i.table)},get isUpToDate(){return O.isUpToDate},streams:Object.fromEntries(Object.keys(m).map(i=>[i,O.shapes[i]]))}};return{namespaceObj:{initMetadataTables:f,syncShapesToTables:l,syncShapeToTable:async p=>{let m=await l({shapes:{shape:{shape:p.shape,table:p.table,schema:p.schema,mapColumns:p.mapColumns,primaryKey:p.primaryKey,onMustRefetch:p.onMustRefetch}},key:p.shapeKey,useCopy:p.useCopy,initialInsertMethod:p.initialInsertMethod,onInitialSync:p.onInitialSync});return{unsubscribe:m.unsubscribe,get isUpToDate(){return m.isUpToDate},stream:m.streams.shape}},deleteSubscription:async p=>{await F({pg:t,metadataSchema:a,subscriptionKey:p})}},close:async()=>{for(let{stream:p,aborter:m}of o)p.unsubscribeAll(),m.abort()}}}function re(t){return{name:"ElectricSQL Sync",setup:async s=>{let{namespaceObj:e,close:a}=await ie(s,t);return{namespaceObj:e,close:a}}}}0&&(module.exports={electricSync}); //# sourceMappingURL=index.cjs.map