@zveltio/sdk 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +191 -0
  2. package/dist/client/Auth.d.ts +19 -0
  3. package/dist/client/Auth.d.ts.map +1 -0
  4. package/dist/client/Auth.js +45 -0
  5. package/dist/client/QueryBuilder.d.ts +17 -0
  6. package/dist/client/QueryBuilder.d.ts.map +1 -0
  7. package/dist/client/QueryBuilder.js +76 -0
  8. package/dist/client/RealtimeClient.d.ts +22 -0
  9. package/dist/client/RealtimeClient.d.ts.map +1 -0
  10. package/dist/client/RealtimeClient.js +96 -0
  11. package/dist/client/ZveltioClient.d.ts +20 -0
  12. package/dist/client/ZveltioClient.d.ts.map +1 -0
  13. package/dist/client/ZveltioClient.js +78 -0
  14. package/dist/client.d.ts +82 -0
  15. package/dist/client.d.ts.map +1 -0
  16. package/dist/client.js +122 -0
  17. package/dist/core.d.ts +61 -0
  18. package/dist/core.d.ts.map +1 -0
  19. package/dist/core.js +65 -0
  20. package/dist/crdt.d.ts +41 -0
  21. package/dist/crdt.d.ts.map +1 -0
  22. package/dist/crdt.js +87 -0
  23. package/dist/extension/index.d.ts +46 -0
  24. package/dist/extension/index.d.ts.map +1 -0
  25. package/dist/extension/index.js +1 -0
  26. package/dist/index.d.ts +14 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +9 -0
  29. package/dist/local-store.d.ts +68 -0
  30. package/dist/local-store.d.ts.map +1 -0
  31. package/dist/local-store.js +263 -0
  32. package/dist/realtime.d.ts +14 -0
  33. package/dist/realtime.d.ts.map +1 -0
  34. package/dist/realtime.js +82 -0
  35. package/dist/schema-watcher.d.ts +41 -0
  36. package/dist/schema-watcher.d.ts.map +1 -0
  37. package/dist/schema-watcher.js +182 -0
  38. package/dist/svelte.d.ts +38 -0
  39. package/dist/svelte.d.ts.map +1 -0
  40. package/dist/svelte.js +48 -0
  41. package/dist/sync-manager.d.ts +73 -0
  42. package/dist/sync-manager.d.ts.map +1 -0
  43. package/dist/sync-manager.js +293 -0
  44. package/dist/tests/local-store.test.d.ts +2 -0
  45. package/dist/tests/local-store.test.d.ts.map +1 -0
  46. package/dist/tests/local-store.test.js +76 -0
  47. package/dist/tests/setup.d.ts +2 -0
  48. package/dist/tests/setup.d.ts.map +1 -0
  49. package/dist/tests/setup.js +3 -0
  50. package/dist/types/index.d.ts +39 -0
  51. package/dist/types/index.d.ts.map +1 -0
  52. package/dist/types/index.js +1 -0
  53. package/package.json +35 -0
@@ -0,0 +1,73 @@
1
+ import type { ZveltioClient } from './client.js';
2
+ export type CRDTConflictResolution = 'lww' | 'custom';
3
+ export interface SyncManagerConfig {
4
+ /** Sync interval in ms (default: 5000) */
5
+ syncInterval?: number;
6
+ /** Max retry attempts per operation (default: 5) */
7
+ maxRetries?: number;
8
+ /** Exponential backoff base in ms (default: 1000) */
9
+ backoffBase?: number;
10
+ /** Callback for conflicts (default: server-wins) */
11
+ onConflict?: (local: any, server: any) => any;
12
+ /** CRDT conflict resolution strategy: 'lww' (field-level LWW merge) or 'custom' (onConflict cb). Default: 'lww' */
13
+ conflictResolution?: CRDTConflictResolution;
14
+ }
15
+ export declare class SyncManager {
16
+ private store;
17
+ private client;
18
+ private realtime;
19
+ private config;
20
+ private syncTimer;
21
+ private isOnline;
22
+ private isSyncing;
23
+ private listeners;
24
+ constructor(client: ZveltioClient, config?: SyncManagerConfig);
25
+ start(realtimeUrl?: string): Promise<void>;
26
+ /**
27
+ * Returns a local-first collection proxy:
28
+ * - list/get reads LOCAL (instant)
29
+ * - create/update/delete writes LOCAL + queues sync
30
+ * - subscribe receives realtime updates
31
+ */
32
+ collection(name: string): {
33
+ /** List records — reads LOCAL instantly */
34
+ list: () => Promise<{
35
+ _syncStatus: "synced" | "pending" | "conflict";
36
+ id: string;
37
+ }[]>;
38
+ /** Get one record — reads LOCAL instantly */
39
+ get: (id: string) => Promise<{
40
+ _syncStatus: "synced" | "pending" | "conflict";
41
+ id: string;
42
+ } | null>;
43
+ /** Create — writes LOCAL + queues sync */
44
+ create: (data: Record<string, any>) => Promise<{
45
+ _syncStatus: "synced" | "pending" | "conflict";
46
+ id: string;
47
+ }>;
48
+ /** Update — writes LOCAL + queues sync */
49
+ update: (id: string, data: Record<string, any>) => Promise<{
50
+ _syncStatus: "synced" | "pending" | "conflict";
51
+ id: string;
52
+ }>;
53
+ /** Delete — soft delete LOCAL + queues sync */
54
+ delete: (id: string) => Promise<void>;
55
+ /** Subscribe la changes (realtime + local writes) */
56
+ subscribe: (callback: (records: any[]) => void) => () => void;
57
+ /** Get pending conflicts for UI resolution */
58
+ getConflicts: () => Promise<import("./local-store.js").LocalRecord[]>;
59
+ /** Resolve a conflict manually */
60
+ resolveConflict: (id: string, resolvedData: Record<string, any>) => Promise<void>;
61
+ };
62
+ /** Force sync now (non-blocking) */
63
+ syncNow(): Promise<void>;
64
+ private notifyListeners;
65
+ /** Status: pending operations count, conflicts count */
66
+ getStatus(): Promise<{
67
+ pending: number;
68
+ conflicts: number;
69
+ isOnline: boolean;
70
+ }>;
71
+ stop(): Promise<void>;
72
+ }
73
+ //# sourceMappingURL=sync-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-manager.d.ts","sourceRoot":"","sources":["../src/sync-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAKjD,MAAM,MAAM,sBAAsB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC;IAC9C,mHAAmH;IACnH,kBAAkB,CAAC,EAAE,sBAAsB,CAAC;CAC7C;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,SAAS,CAAyD;gBAE9D,MAAM,EAAE,aAAa,EAAE,MAAM,GAAE,iBAAsB;IAY3D,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiChD;;;;;OAKG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM;QAEnB,2CAA2C;;;;;QAU3C,6CAA6C;kBAC7B,MAAM;;;;QAUtB,0CAA0C;uBACrB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;;;QAYxC,0CAA0C;qBACvB,MAAM,QAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;;;QAapD,+CAA+C;qBAC5B,MAAM;QAMzB,qDAAqD;8BAC/B,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,IAAI;QAoD9C,8CAA8C;;QAK9C,kCAAkC;8BAE5B,MAAM,gBACI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;IASvC,oCAAoC;IAC9B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YA8GhB,eAAe;IAmB7B,wDAAwD;IAClD,SAAS,IAAI,OAAO,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,OAAO,CAAC;KACnB,CAAC;IAUI,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAK5B"}
@@ -0,0 +1,293 @@
1
+ import { LocalStore } from './local-store.js';
2
+ import { mergeLWW, fromDocument } from './crdt.js';
3
+ export class SyncManager {
4
+ store;
5
+ client;
6
+ realtime = null;
7
+ config;
8
+ syncTimer = null;
9
+ isOnline = true;
10
+ isSyncing = false;
11
+ listeners = new Map();
12
+ constructor(client, config = {}) {
13
+ this.store = new LocalStore();
14
+ this.client = client;
15
+ this.config = {
16
+ syncInterval: config.syncInterval ?? 5000,
17
+ maxRetries: config.maxRetries ?? 5,
18
+ backoffBase: config.backoffBase ?? 1000,
19
+ onConflict: config.onConflict ?? ((_local, server) => server),
20
+ conflictResolution: config.conflictResolution ?? 'lww',
21
+ };
22
+ }
23
+ async start(realtimeUrl) {
24
+ await this.store.open();
25
+ // Online/offline detection
26
+ if (typeof window !== 'undefined') {
27
+ window.addEventListener('online', () => {
28
+ this.isOnline = true;
29
+ this.syncNow(); // Sync immediately on reconnect
30
+ });
31
+ window.addEventListener('offline', () => {
32
+ this.isOnline = false;
33
+ });
34
+ this.isOnline = navigator.onLine;
35
+ }
36
+ // Realtime: receives push updates from server
37
+ if (realtimeUrl) {
38
+ const { ZveltioRealtime } = await import('./realtime.js');
39
+ this.realtime = new ZveltioRealtime(realtimeUrl);
40
+ this.realtime.connect();
41
+ // Subscriptions are per-collection via collection()
42
+ }
43
+ // Periodic sync
44
+ this.syncTimer = setInterval(() => this.syncNow(), this.config.syncInterval);
45
+ // Initial sync
46
+ await this.syncNow();
47
+ }
48
+ /**
49
+ * Returns a local-first collection proxy:
50
+ * - list/get reads LOCAL (instant)
51
+ * - create/update/delete writes LOCAL + queues sync
52
+ * - subscribe receives realtime updates
53
+ */
54
+ collection(name) {
55
+ return {
56
+ /** List records — reads LOCAL instantly */
57
+ list: async () => {
58
+ const records = await this.store.list(name);
59
+ return records.map((r) => ({
60
+ id: r.id,
61
+ ...r.data,
62
+ _syncStatus: r._syncStatus,
63
+ }));
64
+ },
65
+ /** Get one record — reads LOCAL instantly */
66
+ get: async (id) => {
67
+ const record = await this.store.get(name, id);
68
+ if (!record)
69
+ return null;
70
+ return {
71
+ id: record.id,
72
+ ...record.data,
73
+ _syncStatus: record._syncStatus,
74
+ };
75
+ },
76
+ /** Create — writes LOCAL + queues sync */
77
+ create: async (data) => {
78
+ const id = data.id || crypto.randomUUID();
79
+ const record = await this.store.put(name, id, data);
80
+ this.notifyListeners(name);
81
+ this.syncNow(); // Trigger sync immediately (non-blocking)
82
+ return {
83
+ id: record.id,
84
+ ...record.data,
85
+ _syncStatus: record._syncStatus,
86
+ };
87
+ },
88
+ /** Update — writes LOCAL + queues sync */
89
+ update: async (id, data) => {
90
+ const existing = await this.store.get(name, id);
91
+ const merged = { ...(existing?.data || {}), ...data };
92
+ const record = await this.store.put(name, id, merged);
93
+ this.notifyListeners(name);
94
+ this.syncNow();
95
+ return {
96
+ id: record.id,
97
+ ...record.data,
98
+ _syncStatus: record._syncStatus,
99
+ };
100
+ },
101
+ /** Delete — soft delete LOCAL + queues sync */
102
+ delete: async (id) => {
103
+ await this.store.delete(name, id);
104
+ this.notifyListeners(name);
105
+ this.syncNow();
106
+ },
107
+ /** Subscribe la changes (realtime + local writes) */
108
+ subscribe: (callback) => {
109
+ if (!this.listeners.has(name))
110
+ this.listeners.set(name, new Set());
111
+ this.listeners.get(name).add(callback);
112
+ // Subscribe to realtime server push
113
+ let unsubRealtime;
114
+ if (this.realtime) {
115
+ unsubRealtime = this.realtime.subscribe(name, async (event) => {
116
+ // Apply update from server to local store
117
+ if (event.event === 'record.created' ||
118
+ event.event === 'record.updated') {
119
+ try {
120
+ const serverRecord = await this.client
121
+ .collection(name)
122
+ .get(event.record_id);
123
+ await this.store.applyServerUpdate(name, event.record_id, serverRecord, Date.now());
124
+ this.notifyListeners(name);
125
+ }
126
+ catch {
127
+ /* offline or error — ignore, periodic sync will resolve */
128
+ }
129
+ }
130
+ else if (event.event === 'record.deleted') {
131
+ await this.store.delete(name, event.record_id);
132
+ this.notifyListeners(name);
133
+ }
134
+ });
135
+ }
136
+ // Emit current state immediately
137
+ this.store.list(name).then((records) => {
138
+ callback(records.map((r) => ({
139
+ id: r.id,
140
+ ...r.data,
141
+ _syncStatus: r._syncStatus,
142
+ })));
143
+ });
144
+ // Return unsubscribe function
145
+ return () => {
146
+ this.listeners.get(name)?.delete(callback);
147
+ unsubRealtime?.();
148
+ };
149
+ },
150
+ /** Get pending conflicts for UI resolution */
151
+ getConflicts: async () => {
152
+ return this.store.getConflicts(name);
153
+ },
154
+ /** Resolve a conflict manually */
155
+ resolveConflict: async (id, resolvedData) => {
156
+ await this.store.resolveConflict(name, id, resolvedData);
157
+ this.notifyListeners(name);
158
+ this.syncNow();
159
+ },
160
+ };
161
+ }
162
+ /** Force sync now (non-blocking) */
163
+ async syncNow() {
164
+ if (!this.isOnline || this.isSyncing)
165
+ return;
166
+ this.isSyncing = true;
167
+ try {
168
+ // Step 1: Upload offline blobs BEFORE syncing records
169
+ const pendingBlobs = await this.store.getPendingBlobs();
170
+ for (const blobItem of pendingBlobs) {
171
+ try {
172
+ const file = new File([blobItem.blob], `offline_${blobItem.id}`, {
173
+ type: blobItem.blob.type,
174
+ });
175
+ const result = (await this.client.storage.upload(file));
176
+ const url = result?.url || result?.publicUrl || result?.path || '';
177
+ if (!url)
178
+ continue; // Upload returned without URL — skip
179
+ // Replace local_blob_* reference with real URL in record
180
+ const record = await this.store.get(blobItem.collection, blobItem.recordId);
181
+ if (record && record.data[blobItem.field] === blobItem.id) {
182
+ await this.store.put(blobItem.collection, blobItem.recordId, {
183
+ ...record.data,
184
+ [blobItem.field]: url,
185
+ });
186
+ }
187
+ await this.store.deleteBlob(blobItem.id);
188
+ }
189
+ catch {
190
+ // Offline or upload error — skip, retry at next sync cycle
191
+ }
192
+ }
193
+ const pending = await this.store.getPendingOps();
194
+ for (const op of pending) {
195
+ if (op.attempts >= this.config.maxRetries)
196
+ continue; // Skip exhausted operations
197
+ try {
198
+ const serverVersion = Date.now();
199
+ // Attach CRDT doc to payload if available
200
+ const localRecord = await this.store.get(op.collection, op.recordId);
201
+ const crdtPayload = localRecord?._crdtDoc
202
+ ? { ...op.payload, __crdt: localRecord._crdtDoc }
203
+ : op.payload;
204
+ switch (op.operation) {
205
+ case 'create':
206
+ await this.client
207
+ .collection(op.collection)
208
+ .create({ id: op.recordId, ...crdtPayload });
209
+ break;
210
+ case 'update':
211
+ await this.client
212
+ .collection(op.collection)
213
+ .update(op.recordId, crdtPayload);
214
+ break;
215
+ case 'delete':
216
+ await this.client.collection(op.collection).delete(op.recordId);
217
+ break;
218
+ }
219
+ await this.store.markSynced(op.id, op.collection, op.recordId, serverVersion);
220
+ this.notifyListeners(op.collection);
221
+ }
222
+ catch (err) {
223
+ // Conflict from server (409) — apply conflict resolution
224
+ if (err.message?.includes('409')) {
225
+ try {
226
+ const serverRecord = await this.client
227
+ .collection(op.collection)
228
+ .get(op.recordId);
229
+ const localRecord = await this.store.get(op.collection, op.recordId);
230
+ // CRDT LWW merge if both sides have CRDT docs and strategy is 'lww'
231
+ if (this.config.conflictResolution === 'lww' &&
232
+ localRecord?._crdtDoc &&
233
+ serverRecord.__crdt) {
234
+ const merged = mergeLWW(localRecord._crdtDoc, serverRecord.__crdt);
235
+ const resolvedData = fromDocument(merged);
236
+ await this.store.resolveConflict(op.collection, op.recordId, resolvedData);
237
+ }
238
+ else {
239
+ const resolved = this.config.onConflict(localRecord?.data, serverRecord);
240
+ await this.store.resolveConflict(op.collection, op.recordId, resolved);
241
+ }
242
+ }
243
+ catch {
244
+ /* fallback: server wins — ignore error */
245
+ }
246
+ }
247
+ else {
248
+ // Exponential backoff
249
+ await this.store.markFailed(op.id, err.message || 'Unknown error');
250
+ }
251
+ }
252
+ }
253
+ }
254
+ finally {
255
+ this.isSyncing = false;
256
+ }
257
+ }
258
+ async notifyListeners(collection) {
259
+ const callbacks = this.listeners.get(collection);
260
+ if (!callbacks?.size)
261
+ return;
262
+ const records = await this.store.list(collection);
263
+ const mapped = records.map((r) => ({
264
+ id: r.id,
265
+ ...r.data,
266
+ _syncStatus: r._syncStatus,
267
+ }));
268
+ callbacks.forEach((cb) => {
269
+ try {
270
+ cb(mapped);
271
+ }
272
+ catch {
273
+ /* ignore callback errors */
274
+ }
275
+ });
276
+ }
277
+ /** Status: pending operations count, conflicts count */
278
+ async getStatus() {
279
+ const pending = await this.store.getPendingOps();
280
+ const conflicts = await this.store.getConflicts();
281
+ return {
282
+ pending: pending.length,
283
+ conflicts: conflicts.length,
284
+ isOnline: this.isOnline,
285
+ };
286
+ }
287
+ async stop() {
288
+ if (this.syncTimer)
289
+ clearInterval(this.syncTimer);
290
+ this.realtime?.disconnect();
291
+ await this.store.close();
292
+ }
293
+ }
@@ -0,0 +1,2 @@
1
+ import './setup';
2
+ //# sourceMappingURL=local-store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-store.test.d.ts","sourceRoot":"","sources":["../../src/tests/local-store.test.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,CAAC"}
@@ -0,0 +1,76 @@
1
+ import './setup';
2
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
3
+ import { LocalStore } from '../local-store.js';
4
+ describe('LocalStore — conflict detection', () => {
5
+ let store;
6
+ beforeEach(async () => {
7
+ store = new LocalStore();
8
+ await store.open();
9
+ // Reset DB state — fake-indexeddb is a module-level singleton shared across
10
+ // test files within the same Bun process (src/ and dist/ runs both use it).
11
+ // Without clearing, dist/ tests find records left by src/ tests and
12
+ // _localVersion is wrong (e.g. 2 instead of 1 for a fresh put).
13
+ await store.clear();
14
+ });
15
+ afterEach(async () => {
16
+ await store.close();
17
+ });
18
+ it('no conflict when record is clean (no local edits)', async () => {
19
+ // Simulate a record that arrived from server with no local edits
20
+ await store.applyServerUpdate('posts', 'rec-1', { title: 'Hello' }, 1);
21
+ const rec = await store.get('posts', 'rec-1');
22
+ expect(rec?._syncStatus).toBe('synced');
23
+ expect(rec?._conflictData).toBeUndefined();
24
+ });
25
+ it('detects conflict when server update races a pending local write', async () => {
26
+ // 1. Client writes locally (status=pending, localVersion=1, serverVersion=0)
27
+ await store.put('posts', 'rec-2', { title: 'Draft' });
28
+ const before = await store.get('posts', 'rec-2');
29
+ expect(before?._syncStatus).toBe('pending');
30
+ expect(before?._localVersion).toBe(1);
31
+ expect(before?._serverVersion).toBe(0);
32
+ // 2. Server pushes an update
33
+ await store.applyServerUpdate('posts', 'rec-2', { title: 'Server Title' }, 3);
34
+ const after = await store.get('posts', 'rec-2');
35
+ expect(after?._syncStatus).toBe('conflict');
36
+ expect(after?._conflictData).toEqual({ title: 'Server Title' });
37
+ // Local data is NOT overwritten so user can compare both versions
38
+ expect(after?.data).toEqual({ title: 'Draft' });
39
+ });
40
+ it('detects conflict when localVersion > serverVersion regardless of syncStatus (CLAUDE.md scenario)', async () => {
41
+ // Reproduce: _localVersion=2, _serverVersion=1, _syncStatus='synced'
42
+ // Step 1: server sends version 1 → _localVersion=0, _serverVersion=1
43
+ await store.applyServerUpdate('posts', 'rec-3', { title: 'Original' }, 1);
44
+ // Step 2: two local edits → _localVersion=2, _serverVersion=1
45
+ await store.put('posts', 'rec-3', { title: 'Edit 1' }); // localVersion=1
46
+ await store.put('posts', 'rec-3', { title: 'Edit 2' }); // localVersion=2
47
+ // Step 3: forcibly mark as 'synced' while localVersion(2) > serverVersion(1)
48
+ // (simulate the race: sync ACK arrived for v1 but client already at v2)
49
+ const pending = await store.get('posts', 'rec-3');
50
+ expect(pending?._localVersion).toBe(2);
51
+ expect(pending?._serverVersion).toBe(1);
52
+ // Patch syncStatus to 'synced' without advancing serverVersion — the race condition
53
+ const queueItems = await store.getPendingOps();
54
+ // Use markSynced for the first op only — server confirmed v1, client is at v2
55
+ if (queueItems[0]) {
56
+ await store.markSynced(queueItems[0].id, 'posts', 'rec-3', 1);
57
+ }
58
+ const raceState = await store.get('posts', 'rec-3');
59
+ // After markSynced: _serverVersion=1, _localVersion=2 → still dirty
60
+ expect(raceState?._localVersion).toBeGreaterThan(raceState?._serverVersion ?? 0);
61
+ // Step 4: server pushes a concurrent update
62
+ await store.applyServerUpdate('posts', 'rec-3', { title: 'Concurrent Server Edit' }, 5);
63
+ const conflict = await store.get('posts', 'rec-3');
64
+ expect(conflict?._syncStatus).toBe('conflict');
65
+ expect(conflict?._conflictData).toEqual({ title: 'Concurrent Server Edit' });
66
+ });
67
+ it('applies cleanly when server version is newer and no local edits', async () => {
68
+ await store.applyServerUpdate('posts', 'rec-4', { title: 'v1' }, 1);
69
+ await store.applyServerUpdate('posts', 'rec-4', { title: 'v2' }, 2);
70
+ const rec = await store.get('posts', 'rec-4');
71
+ expect(rec?._syncStatus).toBe('synced');
72
+ expect(rec?.data).toEqual({ title: 'v2' });
73
+ expect(rec?._serverVersion).toBe(2);
74
+ expect(rec?._conflictData).toBeUndefined();
75
+ });
76
+ });
@@ -0,0 +1,2 @@
1
+ import 'fake-indexeddb/auto';
2
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/tests/setup.ts"],"names":[],"mappings":"AAEA,OAAO,qBAAqB,CAAC"}
@@ -0,0 +1,3 @@
1
+ // Replaces global indexedDB with a synchronous in-memory implementation.
2
+ // Must be the first import in every test file that uses LocalStore.
3
+ import 'fake-indexeddb/auto';
@@ -0,0 +1,39 @@
1
+ export interface ZveltioConfig {
2
+ baseUrl: string;
3
+ apiKey?: string;
4
+ onUnauthorized?: () => void;
5
+ }
6
+ export interface QueryOptions {
7
+ page?: number;
8
+ limit?: number;
9
+ sort?: string;
10
+ order?: 'asc' | 'desc';
11
+ filter?: Record<string, any>;
12
+ search?: string;
13
+ }
14
+ export interface QueryResponse<T = any> {
15
+ records: T[];
16
+ pagination: {
17
+ total: number;
18
+ page: number;
19
+ limit: number;
20
+ pages: number;
21
+ };
22
+ }
23
+ export interface SingleResponse<T = any> {
24
+ record: T;
25
+ }
26
+ export interface CreateResponse<T = any> {
27
+ record: T;
28
+ }
29
+ export interface DeleteResponse {
30
+ success: boolean;
31
+ id: string;
32
+ }
33
+ export interface RealtimeMessage {
34
+ event: 'insert' | 'update' | 'delete';
35
+ collection: string;
36
+ data: any;
37
+ timestamp: string;
38
+ }
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,GAAG;IACpC,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,UAAU,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,GAAG;IACrC,MAAM,EAAE,CAAC,CAAC;CACX;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,GAAG;IACrC,MAAM,EAAE,CAAC,CAAC;CACX;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,GAAG,CAAC;IACV,SAAS,EAAE,MAAM,CAAC;CACnB"}
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@zveltio/sdk",
3
+ "version": "1.2.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./extension": {
13
+ "types": "./dist/extension/index.d.ts",
14
+ "import": "./dist/extension/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "dev": "tsc --watch",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "bun test --preload ./src/tests/setup.ts"
26
+ },
27
+ "dependencies": {
28
+ "idb": "^8.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "fake-indexeddb": "^6.2.5",
32
+ "hono": "^4.12.5",
33
+ "typescript": "^5.4.0"
34
+ }
35
+ }