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