@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,263 @@
1
+ import { openDB } from 'idb';
2
+ import { LamportClock, mergeLWW, toDocument, fromDocument } from './crdt.js';
3
+ const DB_NAME = 'zveltio_local';
4
+ const DB_VERSION = 3; // v3: CRDT Lamport clock in meta store
5
+ export class LocalStore {
6
+ db = null;
7
+ clock = null;
8
+ async open() {
9
+ this.db = await openDB(DB_NAME, DB_VERSION, {
10
+ upgrade(db, oldVersion) {
11
+ if (oldVersion < 1) {
12
+ // Store for local data (mirror of server collections)
13
+ const store = db.createObjectStore('records', {
14
+ keyPath: ['collection', 'id'],
15
+ });
16
+ store.createIndex('by-collection', 'collection');
17
+ store.createIndex('by-sync-status', '_syncStatus');
18
+ store.createIndex('by-updated', '_updatedAt');
19
+ // Sync queue (pending operations)
20
+ const queue = db.createObjectStore('sync_queue', { keyPath: 'id' });
21
+ queue.createIndex('by-collection', 'collection');
22
+ queue.createIndex('by-created', 'createdAt');
23
+ // Metadata per collection (last sync timestamp, etc.)
24
+ db.createObjectStore('meta', { keyPath: 'key' });
25
+ }
26
+ if (oldVersion < 2) {
27
+ // Blobs saved offline — upload on first reconnect
28
+ db.createObjectStore('offline_blobs', { keyPath: 'id' });
29
+ }
30
+ // v3: no new stores — Lamport clock stored as key in existing 'meta' store
31
+ },
32
+ });
33
+ // Initialize CRDT Lamport clock (persisted in meta store)
34
+ let clientId = (await this.db.get('meta', 'crdt_client_id'))?.value;
35
+ if (!clientId) {
36
+ clientId = crypto.randomUUID();
37
+ await this.db.put('meta', { key: 'crdt_client_id', value: clientId });
38
+ }
39
+ const storedLamport = (await this.db.get('meta', 'crdt_lamport'))?.value ?? 0;
40
+ this.clock = new LamportClock(clientId, storedLamport);
41
+ }
42
+ async persistLamport() {
43
+ if (!this.db || !this.clock)
44
+ return;
45
+ await this.db.put('meta', { key: 'crdt_lamport', value: this.clock.current });
46
+ }
47
+ /** Write a local record and add to sync queue */
48
+ async put(collection, id, data) {
49
+ if (!this.db)
50
+ throw new Error('LocalStore not opened');
51
+ const existing = (await this.db.get('records', [collection, id]));
52
+ const lamport = this.clock ? this.clock.tick() : Date.now();
53
+ const clientId = this.clock?.id ?? 'unknown';
54
+ const crdtDoc = toDocument(data, lamport, clientId);
55
+ const record = {
56
+ id,
57
+ collection,
58
+ data,
59
+ _localVersion: (existing?._localVersion || 0) + 1,
60
+ _serverVersion: existing?._serverVersion || 0,
61
+ _syncStatus: 'pending',
62
+ _updatedAt: Date.now(),
63
+ _crdtDoc: crdtDoc,
64
+ };
65
+ const tx = this.db.transaction(['records', 'sync_queue'], 'readwrite');
66
+ // 1. Write local record
67
+ await tx.objectStore('records').put(record);
68
+ // 2. Add to sync queue
69
+ const queueItem = {
70
+ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
71
+ collection,
72
+ recordId: id,
73
+ operation: existing ? 'update' : 'create',
74
+ payload: data,
75
+ attempts: 0,
76
+ createdAt: Date.now(),
77
+ };
78
+ await tx.objectStore('sync_queue').add(queueItem);
79
+ await tx.done;
80
+ await this.persistLamport();
81
+ return record;
82
+ }
83
+ /** Read a local record (instant, no network) */
84
+ async get(collection, id) {
85
+ if (!this.db)
86
+ throw new Error('LocalStore not opened');
87
+ const record = (await this.db.get('records', [collection, id]));
88
+ if (record?._deletedAt)
89
+ return undefined; // Soft-deleted
90
+ return record;
91
+ }
92
+ /** List records from a collection (local) */
93
+ async list(collection) {
94
+ if (!this.db)
95
+ throw new Error('LocalStore not opened');
96
+ const all = (await this.db.getAllFromIndex('records', 'by-collection', collection));
97
+ return all.filter((r) => !r._deletedAt);
98
+ }
99
+ /** Soft delete local and add to sync queue */
100
+ async delete(collection, id) {
101
+ if (!this.db)
102
+ throw new Error('LocalStore not opened');
103
+ const tx = this.db.transaction(['records', 'sync_queue'], 'readwrite');
104
+ const existing = (await tx.objectStore('records').get([collection, id]));
105
+ if (existing) {
106
+ existing._deletedAt = Date.now();
107
+ existing._syncStatus = 'pending';
108
+ await tx.objectStore('records').put(existing);
109
+ }
110
+ const queueItem = {
111
+ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
112
+ collection,
113
+ recordId: id,
114
+ operation: 'delete',
115
+ payload: {},
116
+ attempts: 0,
117
+ createdAt: Date.now(),
118
+ };
119
+ await tx.objectStore('sync_queue').add(queueItem);
120
+ await tx.done;
121
+ }
122
+ /** Returns all pending operations from sync queue */
123
+ async getPendingOps() {
124
+ if (!this.db)
125
+ throw new Error('LocalStore not opened');
126
+ return this.db.getAllFromIndex('sync_queue', 'by-created');
127
+ }
128
+ /** Mark an operation as completed (remove from queue, update record status) */
129
+ async markSynced(queueItemId, collection, recordId, serverVersion) {
130
+ if (!this.db)
131
+ throw new Error('LocalStore not opened');
132
+ const tx = this.db.transaction(['records', 'sync_queue'], 'readwrite');
133
+ // Remove from queue
134
+ await tx.objectStore('sync_queue').delete(queueItemId);
135
+ // Update record status
136
+ const record = (await tx
137
+ .objectStore('records')
138
+ .get([collection, recordId]));
139
+ if (record) {
140
+ record._serverVersion = serverVersion;
141
+ record._syncStatus = 'synced';
142
+ await tx.objectStore('records').put(record);
143
+ }
144
+ await tx.done;
145
+ }
146
+ /** Mark an operation as failed (increment attempts, save error) */
147
+ async markFailed(queueItemId, error) {
148
+ if (!this.db)
149
+ throw new Error('LocalStore not opened');
150
+ const item = (await this.db.get('sync_queue', queueItemId));
151
+ if (item) {
152
+ item.attempts += 1;
153
+ item.lastAttemptAt = Date.now();
154
+ item.error = error;
155
+ await this.db.put('sync_queue', item);
156
+ }
157
+ }
158
+ /** Apply data from server (via WebSocket or pull) */
159
+ async applyServerUpdate(collection, id, data, serverVersion) {
160
+ if (!this.db)
161
+ throw new Error('LocalStore not opened');
162
+ const existing = (await this.db.get('records', [collection, id]));
163
+ // Conflict detection: local has changes not yet confirmed by server
164
+ if (existing && existing._localVersion > existing._serverVersion) {
165
+ // CRDT field-level merge if both sides have CRDT docs
166
+ if (existing._crdtDoc && data.__crdt) {
167
+ const remoteCrdtDoc = data.__crdt;
168
+ const merged = mergeLWW(existing._crdtDoc, remoteCrdtDoc);
169
+ const mergedData = fromDocument(merged);
170
+ if (this.clock) {
171
+ this.clock.update(Math.max(...Object.values(merged).map((f) => f.lamport)));
172
+ await this.persistLamport();
173
+ }
174
+ existing.data = mergedData;
175
+ existing._crdtDoc = merged;
176
+ existing._serverVersion = serverVersion;
177
+ existing._syncStatus = 'synced'; // CRDT merge resolved without conflict
178
+ await this.db.put('records', existing);
179
+ return;
180
+ }
181
+ // No CRDT doc — mark as conflict for manual resolution
182
+ existing._conflictData = data;
183
+ existing._serverVersion = serverVersion;
184
+ existing._syncStatus = 'conflict';
185
+ await this.db.put('records', existing);
186
+ return;
187
+ }
188
+ const record = {
189
+ id,
190
+ collection,
191
+ data,
192
+ _localVersion: existing?._localVersion || 0,
193
+ _serverVersion: serverVersion,
194
+ _syncStatus: 'synced',
195
+ _updatedAt: Date.now(),
196
+ };
197
+ await this.db.put('records', record);
198
+ }
199
+ /** Get records with conflicts for UI resolution */
200
+ async getConflicts(collection) {
201
+ if (!this.db)
202
+ throw new Error('LocalStore not opened');
203
+ const all = (await this.db.getAllFromIndex('records', 'by-sync-status', 'conflict'));
204
+ if (collection)
205
+ return all.filter((r) => r.collection === collection);
206
+ return all;
207
+ }
208
+ /** Resolve a conflict (user decides which version wins) */
209
+ async resolveConflict(collection, id, resolvedData) {
210
+ if (!this.db)
211
+ throw new Error('LocalStore not opened');
212
+ const record = (await this.db.get('records', [collection, id]));
213
+ if (record) {
214
+ record.data = resolvedData;
215
+ record._syncStatus = 'pending'; // Re-sync with server
216
+ record._localVersion += 1;
217
+ await this.db.put('records', record);
218
+ }
219
+ }
220
+ /** Save an offline blob — returns a temporary ID 'local_blob_<UUID>' */
221
+ async saveBlob(blob, collection, recordId, field) {
222
+ if (!this.db)
223
+ throw new Error('LocalStore not opened');
224
+ const id = `local_blob_${crypto.randomUUID()}`;
225
+ const item = {
226
+ id,
227
+ blob,
228
+ collection,
229
+ recordId,
230
+ field,
231
+ createdAt: Date.now(),
232
+ };
233
+ await this.db.put('offline_blobs', item);
234
+ return id;
235
+ }
236
+ /** Returns all pending offline blobs */
237
+ async getPendingBlobs() {
238
+ if (!this.db)
239
+ throw new Error('LocalStore not opened');
240
+ return this.db.getAll('offline_blobs');
241
+ }
242
+ /** Delete an offline blob after successful upload */
243
+ async deleteBlob(id) {
244
+ if (!this.db)
245
+ throw new Error('LocalStore not opened');
246
+ await this.db.delete('offline_blobs', id);
247
+ }
248
+ /** Clear all local data */
249
+ async clear() {
250
+ if (!this.db)
251
+ throw new Error('LocalStore not opened');
252
+ const tx = this.db.transaction(['records', 'sync_queue', 'meta', 'offline_blobs'], 'readwrite');
253
+ await tx.objectStore('records').clear();
254
+ await tx.objectStore('sync_queue').clear();
255
+ await tx.objectStore('meta').clear();
256
+ await tx.objectStore('offline_blobs').clear();
257
+ await tx.done;
258
+ }
259
+ async close() {
260
+ this.db?.close();
261
+ this.db = null;
262
+ }
263
+ }
@@ -0,0 +1,14 @@
1
+ export declare class ZveltioRealtime {
2
+ private ws;
3
+ private listeners;
4
+ private baseUrl;
5
+ private reconnectAttempts;
6
+ private maxReconnectAttempts;
7
+ private reconnectTimer;
8
+ constructor(baseUrl: string);
9
+ connect(): void;
10
+ private attemptReconnect;
11
+ subscribe(collection: string, callback: (data: any) => void): () => void;
12
+ disconnect(): void;
13
+ }
14
+ //# sourceMappingURL=realtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime.d.ts","sourceRoot":"","sources":["../src/realtime.ts"],"names":[],"mappings":"AAAA,qBAAa,eAAe;IAC1B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,SAAS,CAAoD;IACrE,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,oBAAoB,CAAM;IAClC,OAAO,CAAC,cAAc,CAA8C;gBAExD,OAAO,EAAE,MAAM;IAK3B,OAAO,IAAI,IAAI;IAmCf,OAAO,CAAC,gBAAgB;IAQxB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI;IAqBxE,UAAU,IAAI,IAAI;CAQnB"}
@@ -0,0 +1,82 @@
1
+ export class ZveltioRealtime {
2
+ ws = null;
3
+ listeners = new Map();
4
+ baseUrl;
5
+ reconnectAttempts = 0;
6
+ maxReconnectAttempts = 10;
7
+ reconnectTimer = null;
8
+ constructor(baseUrl) {
9
+ // Convert http(s) to ws(s)
10
+ this.baseUrl = baseUrl.replace(/^http/, 'ws');
11
+ }
12
+ connect() {
13
+ const wsUrl = `${this.baseUrl}/api/ws`;
14
+ this.ws = new WebSocket(wsUrl);
15
+ this.ws.onopen = () => {
16
+ this.reconnectAttempts = 0;
17
+ // Re-subscribe to all existing collections using the server's expected protocol
18
+ const collections = [...this.listeners.keys()];
19
+ if (collections.length > 0) {
20
+ this.ws?.send(JSON.stringify({ type: 'subscribe', collections }));
21
+ }
22
+ };
23
+ this.ws.onmessage = (event) => {
24
+ try {
25
+ const msg = JSON.parse(event.data);
26
+ const collection = msg.collection;
27
+ const subs = this.listeners.get(collection);
28
+ if (subs) {
29
+ subs.forEach((fn) => {
30
+ try {
31
+ fn(msg);
32
+ }
33
+ catch { /* ignore callback errors */ }
34
+ });
35
+ }
36
+ }
37
+ catch { /* invalid JSON — ignore */ }
38
+ };
39
+ this.ws.onclose = () => {
40
+ this.attemptReconnect();
41
+ };
42
+ this.ws.onerror = () => {
43
+ this.ws?.close();
44
+ };
45
+ }
46
+ attemptReconnect() {
47
+ if (this.reconnectAttempts >= this.maxReconnectAttempts)
48
+ return;
49
+ // Exponential backoff: 1s, 2s, 4s, 8s, ...
50
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30_000);
51
+ this.reconnectAttempts++;
52
+ this.reconnectTimer = setTimeout(() => this.connect(), delay);
53
+ }
54
+ subscribe(collection, callback) {
55
+ if (!this.listeners.has(collection))
56
+ this.listeners.set(collection, new Set());
57
+ this.listeners.get(collection).add(callback);
58
+ // Send subscribe to server using correct protocol { type: 'subscribe', collections: [...] }
59
+ if (this.ws?.readyState === WebSocket.OPEN) {
60
+ this.ws.send(JSON.stringify({ type: 'subscribe', collections: [collection] }));
61
+ }
62
+ // Return unsubscribe function
63
+ return () => {
64
+ this.listeners.get(collection)?.delete(callback);
65
+ if (this.listeners.get(collection)?.size === 0) {
66
+ this.listeners.delete(collection);
67
+ if (this.ws?.readyState === WebSocket.OPEN) {
68
+ this.ws.send(JSON.stringify({ type: 'unsubscribe', collections: [collection] }));
69
+ }
70
+ }
71
+ };
72
+ }
73
+ disconnect() {
74
+ if (this.reconnectTimer)
75
+ clearTimeout(this.reconnectTimer);
76
+ this.reconnectTimer = null;
77
+ this.reconnectAttempts = this.maxReconnectAttempts; // Prevent auto-reconnect
78
+ this.ws?.close();
79
+ this.ws = null;
80
+ this.listeners.clear();
81
+ }
82
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Schema Watcher (P6 — End-to-End Type Safety)
3
+ *
4
+ * Watches the Zveltio engine for DDL schema changes via WebSocket and
5
+ * regenerates a TypeScript types file whenever a collection is created,
6
+ * updated, or deleted.
7
+ *
8
+ * Usage (in `zveltio dev --watch`):
9
+ * import { watchSchema } from '@zveltio/sdk/schema-watcher';
10
+ * await watchSchema('http://localhost:3000', './src/zveltio-types.d.ts');
11
+ */
12
+ export interface CollectionField {
13
+ name: string;
14
+ type: string;
15
+ required?: boolean;
16
+ }
17
+ export interface CollectionSchema {
18
+ name: string;
19
+ display_name?: string;
20
+ fields: CollectionField[];
21
+ }
22
+ export interface WatchSchemaOptions {
23
+ /** API token for the engine (admin key) */
24
+ apiKey?: string;
25
+ /** Reconnect delay in ms (default: 3000) */
26
+ reconnectDelay?: number;
27
+ /** Called after each successful type regeneration */
28
+ onUpdate?: (collections: CollectionSchema[]) => void;
29
+ /** Called on connection errors */
30
+ onError?: (err: Error) => void;
31
+ }
32
+ /**
33
+ * Start watching the engine's WebSocket for schema changes and regenerate
34
+ * TypeScript types at `outputPath` whenever the schema changes.
35
+ *
36
+ * Returns a cleanup function that closes the WebSocket.
37
+ */
38
+ export declare function watchSchema(engineUrl: string, outputPath: string, options?: WatchSchemaOptions): Promise<() => void>;
39
+ /** One-shot type generation without watching. */
40
+ export declare function generateTypes(engineUrl: string, outputPath: string, apiKey?: string): Promise<void>;
41
+ //# sourceMappingURL=schema-watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-watcher.d.ts","sourceRoot":"","sources":["../src/schema-watcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IACrD,kCAAkC;IAClC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAChC;AA4GD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,IAAI,CAAC,CAyErB;AAED,iDAAiD;AACjD,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAKf"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Schema Watcher (P6 — End-to-End Type Safety)
3
+ *
4
+ * Watches the Zveltio engine for DDL schema changes via WebSocket and
5
+ * regenerates a TypeScript types file whenever a collection is created,
6
+ * updated, or deleted.
7
+ *
8
+ * Usage (in `zveltio dev --watch`):
9
+ * import { watchSchema } from '@zveltio/sdk/schema-watcher';
10
+ * await watchSchema('http://localhost:3000', './src/zveltio-types.d.ts');
11
+ */
12
+ const TYPE_MAP = {
13
+ text: 'string',
14
+ textarea: 'string',
15
+ richtext: 'string',
16
+ email: 'string',
17
+ url: 'string',
18
+ slug: 'string',
19
+ color: 'string',
20
+ phone: 'string',
21
+ password: 'string',
22
+ uuid: 'string',
23
+ enum: 'string',
24
+ tags: 'string[]',
25
+ integer: 'number',
26
+ float: 'number',
27
+ number: 'number',
28
+ boolean: 'boolean',
29
+ date: 'string',
30
+ datetime: 'string',
31
+ json: 'Record<string, unknown>',
32
+ file: 'string',
33
+ image: 'string',
34
+ location: '{ lat: number; lng: number }',
35
+ geometry: 'GeoJSON.Geometry',
36
+ vector: 'number[]',
37
+ m2o: 'string',
38
+ o2m: 'string[]',
39
+ m2m: 'string[]',
40
+ };
41
+ function fieldToType(field) {
42
+ return TYPE_MAP[field.type] ?? 'unknown';
43
+ }
44
+ function collectionToInterface(col) {
45
+ const lines = [`export interface ${toPascalCase(col.name)} {`];
46
+ lines.push(` id: string;`);
47
+ lines.push(` created_at: string;`);
48
+ lines.push(` updated_at: string;`);
49
+ for (const field of col.fields) {
50
+ const tsType = fieldToType(field);
51
+ const optional = field.required ? '' : '?';
52
+ lines.push(` ${field.name}${optional}: ${tsType};`);
53
+ }
54
+ lines.push(`}`);
55
+ return lines.join('\n');
56
+ }
57
+ function generateTypesFile(collections) {
58
+ const header = `// Auto-generated by @zveltio/sdk schema-watcher
59
+ // Do not edit manually — changes will be overwritten on schema updates.
60
+ // Generated at: ${new Date().toISOString()}
61
+
62
+ `;
63
+ const interfaces = collections.map(collectionToInterface).join('\n\n');
64
+ const collectionNames = collections.map((c) => `'${c.name}'`).join(' | ');
65
+ const collectionMap = collections
66
+ .map((c) => ` ${c.name}: ${toPascalCase(c.name)};`)
67
+ .join('\n');
68
+ const clientType = `
69
+ export type CollectionName = ${collectionNames || 'never'};
70
+
71
+ export interface CollectionTypeMap {
72
+ ${collectionMap}
73
+ }
74
+
75
+ /** Pass this to createZveltioClient<ZveltioSchema>() for full type inference */
76
+ export type ZveltioSchema = CollectionTypeMap;
77
+ `;
78
+ return header + interfaces + clientType;
79
+ }
80
+ function toPascalCase(str) {
81
+ return str
82
+ .split('_')
83
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
84
+ .join('');
85
+ }
86
+ async function fetchCollections(engineUrl, apiKey) {
87
+ const headers = { 'Content-Type': 'application/json' };
88
+ if (apiKey)
89
+ headers['Authorization'] = `Bearer ${apiKey}`;
90
+ const res = await fetch(`${engineUrl}/api/collections`, { headers });
91
+ if (!res.ok)
92
+ throw new Error(`Failed to fetch collections: ${res.status}`);
93
+ const data = await res.json();
94
+ return data.collections || [];
95
+ }
96
+ async function writeTypes(outputPath, content) {
97
+ // Use Bun.file in Bun runtime, fall back to dynamic import of fs in Node
98
+ if (typeof Bun !== 'undefined') {
99
+ await Bun.write(outputPath, content);
100
+ }
101
+ else {
102
+ const { writeFile } = await import('node:fs/promises');
103
+ await writeFile(outputPath, content, 'utf8');
104
+ }
105
+ }
106
+ /**
107
+ * Start watching the engine's WebSocket for schema changes and regenerate
108
+ * TypeScript types at `outputPath` whenever the schema changes.
109
+ *
110
+ * Returns a cleanup function that closes the WebSocket.
111
+ */
112
+ export async function watchSchema(engineUrl, outputPath, options = {}) {
113
+ const { apiKey, reconnectDelay = 3000, onUpdate, onError } = options;
114
+ // Generate initial types on startup
115
+ try {
116
+ const collections = await fetchCollections(engineUrl, apiKey);
117
+ const content = generateTypesFile(collections);
118
+ await writeTypes(outputPath, content);
119
+ console.log(`[zveltio] ✓ Types generated at ${outputPath} (${collections.length} collections)`);
120
+ onUpdate?.(collections);
121
+ }
122
+ catch (err) {
123
+ const e = err instanceof Error ? err : new Error(String(err));
124
+ console.warn(`[zveltio] Could not generate initial types:`, e.message);
125
+ onError?.(e);
126
+ }
127
+ const wsUrl = engineUrl.replace(/^http/, 'ws') + '/api/ws';
128
+ let ws = null;
129
+ let stopped = false;
130
+ function connect() {
131
+ if (stopped)
132
+ return;
133
+ ws = new WebSocket(wsUrl);
134
+ ws.onopen = () => {
135
+ console.log('[zveltio] Schema watcher connected.');
136
+ // Subscribe to all schema change events
137
+ ws.send(JSON.stringify({ type: 'subscribe', channel: 'schema' }));
138
+ };
139
+ ws.onmessage = async (event) => {
140
+ try {
141
+ const msg = JSON.parse(event.data);
142
+ const isSchemaChange = msg.event === 'schema:changed' ||
143
+ msg.type === 'schema:changed' ||
144
+ ['create_collection', 'drop_collection', 'add_field', 'drop_field'].includes(msg.event);
145
+ if (!isSchemaChange)
146
+ return;
147
+ const collections = await fetchCollections(engineUrl, apiKey);
148
+ const content = generateTypesFile(collections);
149
+ await writeTypes(outputPath, content);
150
+ const changed = msg.collection ?? msg.data?.collection ?? '?';
151
+ console.log(`[zveltio] ✓ Types updated — ${changed} changed (${collections.length} total)`);
152
+ onUpdate?.(collections);
153
+ }
154
+ catch (err) {
155
+ // Non-fatal: keep watching
156
+ }
157
+ };
158
+ ws.onclose = () => {
159
+ if (!stopped) {
160
+ console.log(`[zveltio] Schema watcher disconnected. Reconnecting in ${reconnectDelay}ms…`);
161
+ setTimeout(connect, reconnectDelay);
162
+ }
163
+ };
164
+ ws.onerror = (event) => {
165
+ const err = new Error('WebSocket error');
166
+ onError?.(err);
167
+ };
168
+ }
169
+ connect();
170
+ return () => {
171
+ stopped = true;
172
+ ws?.close();
173
+ console.log('[zveltio] Schema watcher stopped.');
174
+ };
175
+ }
176
+ /** One-shot type generation without watching. */
177
+ export async function generateTypes(engineUrl, outputPath, apiKey) {
178
+ const collections = await fetchCollections(engineUrl, apiKey);
179
+ const content = generateTypesFile(collections);
180
+ await writeTypes(outputPath, content);
181
+ console.log(`[zveltio] ✓ Types generated at ${outputPath} (${collections.length} collections)`);
182
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Svelte 5 runes integration with SyncManager.
3
+ *
4
+ * Usage in Svelte 5 components:
5
+ *
6
+ * ```svelte
7
+ * <script lang="ts">
8
+ * import { SyncManager } from '@zveltio/sdk';
9
+ * import { useSyncCollection, useSyncStatus } from '@zveltio/sdk/svelte';
10
+ *
11
+ * let todos = $state<any[]>([]);
12
+ * const unsub = useSyncCollection(sync, 'todos', (records) => { todos = records; });
13
+ *
14
+ * let status = $state({ pending: 0, conflicts: 0, isOnline: true });
15
+ * const unsubStatus = useSyncStatus(sync, (s) => { status = s; });
16
+ *
17
+ * onDestroy(() => { unsub(); unsubStatus(); });
18
+ * </script>
19
+ * ```
20
+ */
21
+ import type { SyncManager } from './sync-manager.js';
22
+ /**
23
+ * Subscribe to a collection via SyncManager.
24
+ * Calls `setter` immediately with current state and on each update.
25
+ * Returns the unsubscribe function.
26
+ */
27
+ export declare function useSyncCollection(sync: SyncManager, collection: string, setter: (records: any[]) => void): () => void;
28
+ /**
29
+ * Subscribe to sync status (pending, conflicts, isOnline).
30
+ * Calls `setter` immediately and on each sync cycle.
31
+ * Returns the cleanup function.
32
+ */
33
+ export declare function useSyncStatus(sync: SyncManager, setter: (status: {
34
+ pending: number;
35
+ conflicts: number;
36
+ isOnline: boolean;
37
+ }) => void, intervalMs?: number): () => void;
38
+ //# sourceMappingURL=svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svelte.d.ts","sourceRoot":"","sources":["../src/svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,IAAI,GAC/B,MAAM,IAAI,CAGZ;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,CAAC,MAAM,EAAE;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB,KAAK,IAAI,EACV,UAAU,SAAO,GAChB,MAAM,IAAI,CAeZ"}
package/dist/svelte.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Svelte 5 runes integration with SyncManager.
3
+ *
4
+ * Usage in Svelte 5 components:
5
+ *
6
+ * ```svelte
7
+ * <script lang="ts">
8
+ * import { SyncManager } from '@zveltio/sdk';
9
+ * import { useSyncCollection, useSyncStatus } from '@zveltio/sdk/svelte';
10
+ *
11
+ * let todos = $state<any[]>([]);
12
+ * const unsub = useSyncCollection(sync, 'todos', (records) => { todos = records; });
13
+ *
14
+ * let status = $state({ pending: 0, conflicts: 0, isOnline: true });
15
+ * const unsubStatus = useSyncStatus(sync, (s) => { status = s; });
16
+ *
17
+ * onDestroy(() => { unsub(); unsubStatus(); });
18
+ * </script>
19
+ * ```
20
+ */
21
+ /**
22
+ * Subscribe to a collection via SyncManager.
23
+ * Calls `setter` immediately with current state and on each update.
24
+ * Returns the unsubscribe function.
25
+ */
26
+ export function useSyncCollection(sync, collection, setter) {
27
+ const col = sync.collection(collection);
28
+ return col.subscribe(setter);
29
+ }
30
+ /**
31
+ * Subscribe to sync status (pending, conflicts, isOnline).
32
+ * Calls `setter` immediately and on each sync cycle.
33
+ * Returns the cleanup function.
34
+ */
35
+ export function useSyncStatus(sync, setter, intervalMs = 2000) {
36
+ // Emit immediately
37
+ sync.getStatus().then(setter);
38
+ // Periodic poll (SyncManager doesn't have event system for status)
39
+ const timer = setInterval(() => {
40
+ sync
41
+ .getStatus()
42
+ .then(setter)
43
+ .catch(() => {
44
+ /* ignore */
45
+ });
46
+ }, intervalMs);
47
+ return () => clearInterval(timer);
48
+ }