UNPKG

@signaldb/sync

Version:

This is the sync implementation of [SignalDB](https://github.com/maxnowack/signaldb). SignalDB is a local-first JavaScript database with real-time sync, enabling optimistic UI with signal-based reactivity across multiple frameworks.

6 lines (4 loc) 12 kB
(function(f,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("@signaldb/core")):typeof define=="function"&&define.amd?define(["exports","@signaldb/core"],l):(f=typeof globalThis<"u"?globalThis:f||self,l(f.SignalDB={},f.core))})(this,function(f,l){"use strict";var x=Object.defineProperty;var b=(f,l,g)=>l in f?x(f,l,{enumerable:!0,configurable:!0,writable:!0,value:g}):f[l]=g;var p=(f,l,g)=>b(f,typeof l!="symbol"?l+"":l,g);function g(d,e,t={}){let s,n;const{leading:o=!1,trailing:a=!0}=t;function c(...r){const i=o&&!s,h=a&&!s;return s&&clearTimeout(s),s=setTimeout(()=>{s=null,a&&!i&&(n=d.apply(this,r))},e),i?n=d.apply(this,r):h||(n=null),n}return c}class ${constructor(){p(this,"queue",[]);p(this,"pendingPromise",!1)}add(e){return new Promise((t,s)=>{this.queue.push(()=>e().then(t).catch(n=>{throw s(n),n})),this.dequeue()})}hasPendingPromise(){return this.pendingPromise}dequeue(){if(this.pendingPromise||this.queue.length===0)return;const e=this.queue.shift();e&&(this.pendingPromise=!0,e().then(()=>{this.pendingPromise=!1,this.dequeue()}).catch(()=>{this.pendingPromise=!1,this.dequeue()}))}}function C(d,e){const t=[],s=Object.keys(d),n=Object.keys(e),o=new Set([...s,...n]);for(const a of o)if(e[a]!==d[a])if(typeof e[a]=="object"&&typeof d[a]=="object"&&e[a]!=null&&d[a]!=null){const c=C(d[a],e[a]);for(const r of c)t.push(`${a}.${r}`)}else t.push(a);return t}function w(d,e){const t=[],s=[],n=new Map,o=[],a=new Map(d.map(r=>[r.id,r])),c=new Map(e.map(r=>[r.id,r]));for(const[r,i]of a){const h=c.get(r);h?l.isEqual(h,i)||(n.set(h.id,C(i,h)),s.push(h)):o.push(i)}for(const[r,i]of c)a.has(r)||t.push(i);return{added:t,modified:s,modifiedFields:n,removed:o}}function P(d,e){if(e.items!=null)return e.items;const t=d||[];return e.changes.added.forEach(s=>{const n=t.findIndex(o=>o.id===s.id);n===-1?t.push(s):t[n]=s}),e.changes.modified.forEach(s=>{const n=t.findIndex(o=>o.id===s.id);n===-1?t.push(s):t[n]=s}),e.changes.removed.forEach(s=>{const n=t.findIndex(o=>o.id===s.id);n!==-1&&t.splice(n,1)}),t}function v(d,e){const t=new Map(d.map(s=>[s.id,s]));return e.forEach(s=>{if(s.type==="remove")t.delete(s.data);else if(s.type==="insert"){const n=t.get(s.data.id);t.set(s.data.id,n?{...n,...s.data}:s.data)}else{const n=t.get(s.data.id);t.set(s.data.id,n?l.modify(n,s.data.modifier):l.modify({id:s.data.id},s.data.modifier))}}),[...t.values()]}function O(d){return d.added.length>0||d.modified.length>0||d.removed.length>0}function E(d,e){return O(w(d,e))}async function S({changes:d,lastSnapshot:e,data:t,pull:s,push:n,insert:o,update:a,remove:c,batch:r}){let i=t,h=e||[],m=P(e,i);if(d.length>0){const y=v(h,d);if(E(h,y)){const D=v(m,d),N=w(m,D);O(N)&&(await n(N),i=await s(),m=P(m,i)),h=y}}const u=i.changes==null?w(h,i.items):i.changes;return r(()=>{u.added.forEach(y=>o(y)),u.modified.forEach(y=>a(y.id,{$set:y})),u.removed.forEach(y=>c(y.id))}),m}class M{constructor(e){p(this,"options");p(this,"collections",new Map);p(this,"changes");p(this,"snapshots");p(this,"syncOperations");p(this,"scheduledPushes",new Set);p(this,"remoteChanges",[]);p(this,"syncQueues",new Map);p(this,"persistenceReady");p(this,"isDisposed",!1);p(this,"instanceId",l.randomId());p(this,"id");p(this,"debouncedFlush");this.options={autostart:!0,...e},this.id=this.options.id||"default-sync-manager";const{reactivity:t}=this.options,s=this.createPersistenceAdapter("changes"),n=this.createPersistenceAdapter("snapshots"),o=this.createPersistenceAdapter("sync-operations");this.changes=new l.Collection({name:`${this.options.id}-changes`,persistence:s==null?void 0:s.adapter,indices:[l.createIndex("collectionName")],reactivity:t}),this.snapshots=new l.Collection({name:`${this.options.id}-snapshots`,persistence:n==null?void 0:n.adapter,indices:[l.createIndex("collectionName")],reactivity:t}),this.syncOperations=new l.Collection({name:`${this.options.id}-sync-operations`,persistence:o==null?void 0:o.adapter,indices:[l.createIndex("collectionName"),l.createIndex("status")],reactivity:t}),this.changes.on("persistence.error",a=>s==null?void 0:s.handler(a)),this.snapshots.on("persistence.error",a=>n==null?void 0:n.handler(a)),this.syncOperations.on("persistence.error",a=>o==null?void 0:o.handler(a)),this.persistenceReady=Promise.all([this.syncOperations.isReady(),this.changes.isReady(),this.snapshots.isReady()]).then(()=>{}),this.changes.setMaxListeners(1e3),this.snapshots.setMaxListeners(1e3),this.syncOperations.setMaxListeners(1e3),this.debouncedFlush=g(this.flushScheduledPushes,this.options.debounceTime??100)}createPersistenceAdapter(e){if(this.options.persistenceAdapter==null)return;let t=()=>{};return{adapter:this.options.persistenceAdapter(`${this.id}-${e}`,n=>{t=n}),handler:n=>t(n)}}getSyncQueue(e){return this.syncQueues.get(e)==null&&this.syncQueues.set(e,new $),this.syncQueues.get(e)}async dispose(){this.collections.clear(),this.syncQueues.clear(),this.remoteChanges.splice(0),await Promise.all([this.changes.dispose(),this.snapshots.dispose(),this.syncOperations.dispose()]),this.isDisposed=!0}getCollection(e){const{collection:t,options:s}=this.getCollectionProperties(e);return[t,s]}getCollectionProperties(e){const t=this.collections.get(e);if(t==null)throw new Error(`Collection with id '${e}' not found`);return t}addCollection(e,t){if(this.isDisposed)throw new Error("SyncManager is disposed");this.collections.set(t.name,{collection:e,options:t,readyPromise:e.isReady(),syncPaused:!0});const s=o=>{for(const a of this.remoteChanges)if(a!=null&&a.collectionName===o.collectionName&&a.type===o.type&&!(o.type==="remove"&&a.data!==o.data)&&a.data.id===o.data.id)return!0;return!1},n=(o,a)=>{const c=[...this.remoteChanges];for(let r=0;r<c.length;r+=1){const i=c[r];i!=null&&i.collectionName===o&&(i.type==="remove"&&i.data!==a||i.data.id===a&&(c[r]=null))}this.remoteChanges=c.filter(r=>r!=null)};e.on("added",o=>{if(s({collectionName:t.name,type:"insert",data:o})){n(t.name,o.id);return}this.changes.insert({collectionName:t.name,time:Date.now(),type:"insert",data:o}),!this.getCollectionProperties(t.name).syncPaused&&this.schedulePush(t.name)}),e.on("changed",({id:o},a)=>{const c={id:o,modifier:a};if(s({collectionName:t.name,type:"update",data:c})){n(t.name,o);return}this.changes.insert({collectionName:t.name,time:Date.now(),type:"update",data:c}),!this.getCollectionProperties(t.name).syncPaused&&this.schedulePush(t.name)}),e.on("removed",({id:o})=>{if(s({collectionName:t.name,type:"remove",data:o})){n(t.name,o);return}this.changes.insert({collectionName:t.name,time:Date.now(),type:"remove",data:o}),!this.getCollectionProperties(t.name).syncPaused&&this.schedulePush(t.name)}),this.options.autostart&&this.startSync(t.name).catch(o=>{this.options.onError&&this.options.onError(this.getCollectionProperties(t.name).options,o)})}flushScheduledPushes(){this.scheduledPushes.forEach(e=>{this.pushChanges(e).catch(()=>{})}),this.scheduledPushes.clear()}schedulePush(e){this.scheduledPushes.add(e),this.debouncedFlush()}async startAll(){await Promise.all([...this.collections.keys()].map(e=>this.startSync(e)))}async startSync(e){const t=this.getCollectionProperties(e);if(!t.syncPaused)return;this.schedulePush(e);const s=this.options.registerRemoteChange?await this.options.registerRemoteChange(t.options,async n=>{if(n==null)await this.sync(e);else{const o=Date.now(),a=this.syncOperations.insert({start:o,collectionName:e,instanceId:this.instanceId,status:"active"});await this.syncWithData(e,n).then(()=>{this.syncOperations.removeMany({id:{$ne:a},collectionName:e,$or:[{end:{$lte:o}},{status:"active"}]}),this.syncOperations.updateOne({id:a},{$set:{status:"done",end:Date.now()}})}).catch(c=>{throw this.options.onError&&this.options.onError(this.getCollectionProperties(e).options,c),this.syncOperations.updateOne({id:a},{$set:{status:"error",end:Date.now(),error:c.stack||c.message}}),c})}}):void 0;this.collections.set(e,{...t,syncPaused:!1,cleanupFunction:s})}async pauseAll(){await Promise.all([...this.collections.keys()].map(e=>this.pauseSync(e)))}async pauseSync(e){const t=this.getCollectionProperties(e);t.syncPaused||(t.cleanupFunction&&await t.cleanupFunction(),this.collections.set(e,{...t,cleanupFunction:void 0,syncPaused:!0}))}async syncAll(){if(this.isDisposed)throw new Error("SyncManager is disposed");const e=[];if(await Promise.all([...this.collections.keys()].map(t=>this.sync(t).catch(s=>{e.push({id:t,error:s})}))),e.length>0)throw new Error(`Error while syncing collections: ${e.map(t=>`${t.id}: ${t.error.message}`).join(` `)}`)}isSyncing(e){return this.syncOperations.findOne({...e?{collectionName:e}:{},status:"active"},{fields:{status:1}})!=null}async isReady(){await this.persistenceReady}async sync(e,t={}){if(this.isDisposed)throw new Error("SyncManager is disposed");await this.isReady();const{options:s,readyPromise:n}=this.getCollectionProperties(e);await n;const o=this.syncOperations.find({collectionName:e,instanceId:this.instanceId,status:"active"},{reactive:!1}).count()>0,a=Date.now();let c=null;await new Promise(i=>{setTimeout(i,0)});const r=async()=>{const i=this.syncOperations.findOne({collectionName:e,status:"done"},{sort:{end:-1},reactive:!1});if(t!=null&&t.onlyWithChanges&&this.changes.find({collectionName:e,time:{$lte:a}},{sort:{time:1},reactive:!1}).count()===0)return;o||(c=this.syncOperations.insert({start:a,collectionName:e,instanceId:this.instanceId,status:"active"}));const h=await this.options.pull(s,{lastFinishedSyncStart:i==null?void 0:i.start,lastFinishedSyncEnd:i==null?void 0:i.end});await this.syncWithData(e,h)};await(t!=null&&t.force?r():this.getSyncQueue(e).add(r)).catch(i=>{throw c!=null&&(this.options.onError&&this.options.onError(s,i),this.syncOperations.updateOne({id:c},{$set:{status:"error",end:Date.now(),error:i.stack||i.message}})),i}),c!=null&&(this.syncOperations.removeMany({id:{$ne:c},collectionName:e,$or:[{end:{$lte:a}},{status:"active"}]}),this.syncOperations.updateOne({id:c},{$set:{status:"done",end:Date.now()}}))}async pushChanges(e){await this.sync(e,{onlyWithChanges:!0})}async syncWithData(e,t){const{collection:s,options:n}=this.getCollectionProperties(e),o=Date.now(),a=this.syncOperations.findOne({collectionName:e,status:"done"},{sort:{end:-1},reactive:!1}),c=this.snapshots.findOne({collectionName:e},{sort:{time:-1},reactive:!1}),r=this.changes.find({collectionName:e,time:{$lte:o}},{sort:{time:1},reactive:!1}).fetch();await S({changes:r,lastSnapshot:c==null?void 0:c.items,data:t,pull:()=>this.options.pull(n,{lastFinishedSyncStart:a==null?void 0:a.start,lastFinishedSyncEnd:a==null?void 0:a.end}),push:i=>this.options.push(n,{changes:i,rawChanges:r}),insert:i=>{this.remoteChanges.push({collectionName:e,type:"insert",data:i},{collectionName:e,type:"update",data:{id:i.id,modifier:{$set:i}}}),s.replaceOne({id:i.id},i,{upsert:!0})},update:(i,h)=>{this.remoteChanges.push({collectionName:e,type:"insert",data:{id:i,...h.$set}},{collectionName:e,type:"update",data:{id:i,modifier:h}}),s.updateOne({id:i},{...h,$setOnInsert:{id:i}},{upsert:!0})},remove:i=>{s.findOne({id:i},{reactive:!1})&&(this.remoteChanges.push({collectionName:e,type:"remove",data:i}),s.removeOne({id:i}))},batch:i=>{s.batch(()=>{i()})}}).then(async i=>{if(this.snapshots.removeMany({collectionName:e,time:{$lte:o}}),this.changes.removeMany({collectionName:e,id:{$in:r.map(u=>u.id)}}),this.snapshots.insert({time:o,collectionName:e,items:i}),await new Promise(u=>{setTimeout(u,0)}),this.changes.find({collectionName:e},{reactive:!1}).count()>0){await this.sync(e,{force:!0,onlyWithChanges:!0});return}const m=s.find({id:{$nin:i.map(u=>u.id)}},{reactive:!1}).map(u=>u.id);s.batch(()=>{i.forEach(u=>{this.remoteChanges.push({collectionName:e,type:"insert",data:u},{collectionName:e,type:"update",data:{id:u.id,modifier:{$set:u}}}),s.replaceOne({id:u.id},u,{upsert:!0})}),m.forEach(u=>{s.removeOne({id:u})})})})}}f.SyncManager=M,Object.defineProperty(f,Symbol.toStringTag,{value:"Module"})}); //# sourceMappingURL=index.umd.js.map