@datafn/client 0.0.1

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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1216 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ EventBus: () => EventBus,
24
+ IndexedDbStorageAdapter: () => IndexedDbStorageAdapter,
25
+ MemoryStorageAdapter: () => MemoryStorageAdapter,
26
+ createClientError: () => createClientError,
27
+ createDatafnClient: () => createDatafnClient,
28
+ matchesFilter: () => matchesFilter,
29
+ unwrapRemoteSuccess: () => unwrapRemoteSuccess
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/client.ts
34
+ var import_core2 = require("@datafn/core");
35
+
36
+ // src/events/filter.ts
37
+ function matchesFilter(event, filter) {
38
+ if (!filter) return true;
39
+ if (filter.type !== void 0) {
40
+ if (Array.isArray(filter.type)) {
41
+ if (!filter.type.includes(event.type)) return false;
42
+ } else {
43
+ if (event.type !== filter.type) return false;
44
+ }
45
+ }
46
+ if (filter.resource !== void 0 && event.resource) {
47
+ if (Array.isArray(filter.resource)) {
48
+ if (!filter.resource.includes(event.resource)) return false;
49
+ } else {
50
+ if (event.resource !== filter.resource) return false;
51
+ }
52
+ }
53
+ if (filter.ids !== void 0 && event.ids) {
54
+ const filterIds = Array.isArray(filter.ids) ? filter.ids : [filter.ids];
55
+ const eventIds = Array.isArray(event.ids) ? event.ids : [event.ids];
56
+ const hasMatch = eventIds.some((id) => filterIds.includes(id));
57
+ if (!hasMatch) return false;
58
+ }
59
+ if (filter.mutationId !== void 0 && event.mutationId) {
60
+ if (Array.isArray(filter.mutationId)) {
61
+ if (!filter.mutationId.includes(event.mutationId)) return false;
62
+ } else {
63
+ if (event.mutationId !== filter.mutationId) return false;
64
+ }
65
+ }
66
+ if (filter.action !== void 0 && event.action) {
67
+ if (Array.isArray(filter.action)) {
68
+ if (!filter.action.includes(event.action)) return false;
69
+ } else {
70
+ if (event.action !== filter.action) return false;
71
+ }
72
+ }
73
+ if (filter.fields !== void 0 && event.fields) {
74
+ const filterFields = Array.isArray(filter.fields) ? filter.fields : [filter.fields];
75
+ const eventFields = Array.isArray(event.fields) ? event.fields : [event.fields];
76
+ const hasIntersection = filterFields.some((f) => eventFields.includes(f));
77
+ if (!hasIntersection) return false;
78
+ }
79
+ if (filter.contextKeys !== void 0 && filter.contextKeys.length > 0) {
80
+ if (!event.context || typeof event.context !== "object") {
81
+ return false;
82
+ }
83
+ const ctx = event.context;
84
+ const allExist = filter.contextKeys.every((key) => key in ctx);
85
+ if (!allExist) return false;
86
+ }
87
+ return true;
88
+ }
89
+
90
+ // src/events/bus.ts
91
+ var EventBus = class {
92
+ constructor() {
93
+ this.subscriptions = [];
94
+ this.nextId = 1;
95
+ }
96
+ /**
97
+ * Subscribe to events with optional filtering
98
+ */
99
+ subscribe(handler, filter) {
100
+ const subscription = {
101
+ id: this.nextId++,
102
+ handler,
103
+ filter
104
+ };
105
+ this.subscriptions.push(subscription);
106
+ return () => {
107
+ this.subscriptions = this.subscriptions.filter(
108
+ (s) => s.id !== subscription.id
109
+ );
110
+ };
111
+ }
112
+ /**
113
+ * Emit an event to all matching subscribers
114
+ */
115
+ emit(event) {
116
+ for (const subscription of this.subscriptions) {
117
+ if (matchesFilter(event, subscription.filter)) {
118
+ subscription.handler(event);
119
+ }
120
+ }
121
+ }
122
+ };
123
+
124
+ // src/errors.ts
125
+ function createClientError(code, message, details) {
126
+ const error = {
127
+ code,
128
+ message,
129
+ details
130
+ };
131
+ throw error;
132
+ }
133
+ function isTransportError(error) {
134
+ if (typeof error !== "object" || error === null) return false;
135
+ const err = error;
136
+ if (err.code === "TRANSPORT_ERROR") return true;
137
+ if (err.name === "TypeError" && err.message.includes("fetch")) return true;
138
+ if (err.name === "AbortError") return true;
139
+ return false;
140
+ }
141
+
142
+ // src/tables/table.ts
143
+ function createTable(name, version, client, signalRegistry) {
144
+ return {
145
+ name,
146
+ version,
147
+ /**
148
+ * Execute a query with resource/version merged (CLIENT-QUERY-001)
149
+ */
150
+ async query(q) {
151
+ const fragment = typeof q === "object" && q !== null ? q : {};
152
+ const { resource: _r, version: _v, ...rest } = fragment;
153
+ const fullQuery = {
154
+ resource: name,
155
+ version,
156
+ ...rest
157
+ };
158
+ return client.query(fullQuery);
159
+ },
160
+ /**
161
+ * Execute a mutation with resource/version merged (CLIENT-MUT-001)
162
+ */
163
+ async mutate(m) {
164
+ const fragment = typeof m === "object" && m !== null ? m : {};
165
+ const { resource: _r, version: _v, ...rest } = fragment;
166
+ const fullMutation = {
167
+ resource: name,
168
+ version,
169
+ ...rest
170
+ };
171
+ return client.mutate(fullMutation);
172
+ },
173
+ /**
174
+ * Execute a transaction (CLIENT-TX-001)
175
+ */
176
+ async transact(payload) {
177
+ return client.transact(payload);
178
+ },
179
+ /**
180
+ * Create reactive query signal (CLIENT-SIGNAL-001)
181
+ */
182
+ signal(q) {
183
+ const fragment = typeof q === "object" && q !== null ? q : {};
184
+ const { resource: _r, version: _v, ...rest } = fragment;
185
+ const fullQuery = {
186
+ resource: name,
187
+ version,
188
+ ...rest
189
+ };
190
+ return signalRegistry.getSignal(fullQuery);
191
+ },
192
+ /**
193
+ * Subscribe to events for this table's resource (CLIENT-SUB-001)
194
+ */
195
+ subscribe(handler, filter) {
196
+ const tableFilter = {
197
+ ...filter,
198
+ resource: name
199
+ // Always use table's resource, ignore user-provided
200
+ };
201
+ return client.subscribe(handler, tableFilter);
202
+ }
203
+ };
204
+ }
205
+
206
+ // src/tables/registry.ts
207
+ var TableRegistry = class {
208
+ constructor(schema, client, signalRegistry) {
209
+ this.schema = schema;
210
+ this.tables = /* @__PURE__ */ new Map();
211
+ this.client = client;
212
+ this.signalRegistry = signalRegistry;
213
+ }
214
+ /**
215
+ * Get a table handle by name.
216
+ * Caches table instances for object identity.
217
+ * Throws DFQL_UNKNOWN_RESOURCE for unknown tables.
218
+ */
219
+ getTable(name) {
220
+ const cached = this.tables.get(name);
221
+ if (cached) {
222
+ return cached;
223
+ }
224
+ const resource = this.schema.resources.find((r) => r.name === name);
225
+ if (!resource) {
226
+ createClientError("DFQL_UNKNOWN_RESOURCE", `Unknown resource: ${name}`, {
227
+ path: "resource",
228
+ resource: name
229
+ });
230
+ }
231
+ const table = createTable(
232
+ resource.name,
233
+ resource.version,
234
+ this.client,
235
+ this.signalRegistry
236
+ );
237
+ this.tables.set(name, table);
238
+ return table;
239
+ }
240
+ /**
241
+ * Get all declared table names from schema
242
+ */
243
+ getTableNames() {
244
+ return this.schema.resources.map((r) => r.name);
245
+ }
246
+ };
247
+
248
+ // src/remote/unwrap.ts
249
+ function unwrapRemoteSuccess(response) {
250
+ if (typeof response !== "object" || response === null) {
251
+ createClientError(
252
+ "TRANSPORT_ERROR",
253
+ "Transport error: unexpected response shape",
254
+ { path: "$" }
255
+ );
256
+ }
257
+ const resp = response;
258
+ if ("ok" in resp) {
259
+ if (resp.ok === true && "result" in resp) {
260
+ return resp.result;
261
+ } else if (resp.ok === false && "error" in resp) {
262
+ const error = resp.error;
263
+ createClientError(
264
+ error.code || "INTERNAL",
265
+ error.message || "Unknown error",
266
+ error.details || { path: "$" }
267
+ );
268
+ }
269
+ }
270
+ if ("data" in resp && "nextCursor" in resp) {
271
+ return response;
272
+ }
273
+ if ("groups" in resp && "nextCursor" in resp) {
274
+ return response;
275
+ }
276
+ createClientError(
277
+ "TRANSPORT_ERROR",
278
+ "Transport error: unexpected response shape",
279
+ { path: "$" }
280
+ );
281
+ }
282
+
283
+ // src/offline/query.ts
284
+ async function executeLocalQuery(storage, query) {
285
+ const resource = query.resource;
286
+ const select = query.select || [];
287
+ const filters = query.filters || {};
288
+ const sort = query.sort || [];
289
+ const limit = typeof query.limit === "number" ? query.limit : void 0;
290
+ const offset = typeof query.offset === "number" ? query.offset : 0;
291
+ let records = await storage.listRecords(resource);
292
+ if (Object.keys(filters).length > 0) {
293
+ records = records.filter((record) => matchesFilters(record, filters));
294
+ }
295
+ if (sort.length > 0) {
296
+ records.sort(createComparator(sort));
297
+ }
298
+ if (offset > 0) {
299
+ records = records.slice(offset);
300
+ }
301
+ if (limit !== void 0) {
302
+ records = records.slice(0, limit);
303
+ }
304
+ const data = records.map((record) => projectFields(record, select));
305
+ return {
306
+ data,
307
+ nextCursor: null
308
+ // Local cursor logic omitted for MVP (id-based or offset based)
309
+ };
310
+ }
311
+ function matchesFilters(record, filters) {
312
+ for (const [key, filterValue] of Object.entries(filters)) {
313
+ const recordValue = getDotPath(record, key);
314
+ if (typeof filterValue === "object" && filterValue !== null) {
315
+ if (!evaluateOperators(recordValue, filterValue)) {
316
+ return false;
317
+ }
318
+ } else {
319
+ if (recordValue !== filterValue) {
320
+ return false;
321
+ }
322
+ }
323
+ }
324
+ return true;
325
+ }
326
+ function evaluateOperators(value, operators) {
327
+ for (const [op, target] of Object.entries(operators)) {
328
+ switch (op) {
329
+ case "$eq":
330
+ case "eq":
331
+ if (value !== target) return false;
332
+ break;
333
+ case "$neq":
334
+ case "neq":
335
+ if (value === target) return false;
336
+ break;
337
+ case "$in":
338
+ case "in":
339
+ if (Array.isArray(target) && !target.includes(value)) return false;
340
+ break;
341
+ case "$gt":
342
+ case "gt":
343
+ if (!(value > target)) return false;
344
+ break;
345
+ case "$lt":
346
+ case "lt":
347
+ if (!(value < target)) return false;
348
+ break;
349
+ case "$gte":
350
+ case "gte":
351
+ if (!(value >= target)) return false;
352
+ break;
353
+ case "$lte":
354
+ case "lte":
355
+ if (!(value <= target)) return false;
356
+ break;
357
+ case "$contains":
358
+ case "contains":
359
+ if (typeof value === "string" && typeof target === "string") {
360
+ if (!value.includes(target)) return false;
361
+ } else if (Array.isArray(value)) {
362
+ if (!value.includes(target)) return false;
363
+ } else {
364
+ return false;
365
+ }
366
+ break;
367
+ }
368
+ }
369
+ return true;
370
+ }
371
+ function createComparator(sortRules) {
372
+ return (a, b) => {
373
+ for (const rule of sortRules) {
374
+ const valA = getDotPath(a, rule.field);
375
+ const valB = getDotPath(b, rule.field);
376
+ if (valA === valB) continue;
377
+ const comparison = valA < valB ? -1 : 1;
378
+ return rule.dir === "asc" ? comparison : -comparison;
379
+ }
380
+ const idA = a.id;
381
+ const idB = b.id;
382
+ return idA < idB ? -1 : idA > idB ? 1 : 0;
383
+ };
384
+ }
385
+ function getDotPath(obj, path) {
386
+ return path.split(".").reduce((curr, part) => curr?.[part], obj);
387
+ }
388
+ function projectFields(record, select) {
389
+ if (!select || select.length === 0) {
390
+ return { ...record };
391
+ }
392
+ const projected = { id: record.id };
393
+ for (const field of select) {
394
+ if (field in record) {
395
+ projected[field] = record[field];
396
+ }
397
+ }
398
+ return projected;
399
+ }
400
+
401
+ // src/plugins/run-hooks.ts
402
+ function getClientPlugins(plugins) {
403
+ return plugins.filter((p) => p.runsOn && p.runsOn.includes("client"));
404
+ }
405
+ async function runBeforeQuery(plugins, schema, query) {
406
+ let transformed = query;
407
+ const ctx = { env: "client", schema };
408
+ for (const plugin of getClientPlugins(plugins)) {
409
+ if (plugin.beforeQuery) {
410
+ try {
411
+ const result = await plugin.beforeQuery(ctx, transformed);
412
+ if (result !== void 0) {
413
+ transformed = result;
414
+ }
415
+ } catch (error) {
416
+ throw {
417
+ code: error.code || "INTERNAL",
418
+ message: error.message || "Plugin error",
419
+ details: {
420
+ path: `plugins.${plugin.name}.beforeQuery`,
421
+ ...error.details || {}
422
+ }
423
+ };
424
+ }
425
+ }
426
+ }
427
+ return transformed;
428
+ }
429
+ async function runAfterQuery(plugins, schema, query, result) {
430
+ let transformed = result;
431
+ const ctx = { env: "client", schema };
432
+ for (const plugin of getClientPlugins(plugins)) {
433
+ if (plugin.afterQuery) {
434
+ try {
435
+ const pluginResult = await plugin.afterQuery(ctx, query, transformed);
436
+ if (pluginResult !== void 0) {
437
+ transformed = pluginResult;
438
+ }
439
+ } catch (error) {
440
+ console.error(`Plugin ${plugin.name}.afterQuery failed:`, error);
441
+ }
442
+ }
443
+ }
444
+ return transformed;
445
+ }
446
+ async function runBeforeMutation(plugins, schema, mutation) {
447
+ let transformed = mutation;
448
+ const ctx = { env: "client", schema };
449
+ for (const plugin of getClientPlugins(plugins)) {
450
+ if (plugin.beforeMutation) {
451
+ try {
452
+ const result = await plugin.beforeMutation(ctx, transformed);
453
+ if (result !== void 0) {
454
+ transformed = result;
455
+ }
456
+ } catch (error) {
457
+ throw {
458
+ code: error.code || "INTERNAL",
459
+ message: error.message || "Plugin error",
460
+ details: {
461
+ path: `plugins.${plugin.name}.beforeMutation`,
462
+ ...error.details || {}
463
+ }
464
+ };
465
+ }
466
+ }
467
+ }
468
+ return transformed;
469
+ }
470
+ async function runAfterMutation(plugins, schema, mutation, result) {
471
+ let transformed = result;
472
+ const ctx = { env: "client", schema };
473
+ for (const plugin of getClientPlugins(plugins)) {
474
+ if (plugin.afterMutation) {
475
+ try {
476
+ await plugin.afterMutation(ctx, mutation, transformed);
477
+ } catch (error) {
478
+ console.error(`Plugin ${plugin.name}.afterMutation failed:`, error);
479
+ }
480
+ }
481
+ }
482
+ return transformed;
483
+ }
484
+
485
+ // src/query.ts
486
+ async function executeQuery(remote, q, storage, plugins = [], schema) {
487
+ const transformedQuery = schema ? await runBeforeQuery(plugins, schema, q) : q;
488
+ if (!storage) {
489
+ const response2 = await remote.query(transformedQuery);
490
+ const result2 = unwrapRemoteSuccess(response2);
491
+ return schema ? runAfterQuery(plugins, schema, transformedQuery, result2) : result2;
492
+ }
493
+ if (Array.isArray(transformedQuery)) {
494
+ const response2 = await remote.query(transformedQuery);
495
+ const result2 = unwrapRemoteSuccess(response2);
496
+ return schema ? runAfterQuery(plugins, schema, transformedQuery, result2) : result2;
497
+ }
498
+ const query = transformedQuery;
499
+ const resource = query.resource;
500
+ if (resource) {
501
+ const hydrationState = await storage.getHydrationState(resource);
502
+ if (hydrationState === "ready") {
503
+ const result2 = await executeLocalQuery(storage, query);
504
+ return schema ? runAfterQuery(plugins, schema, query, result2) : result2;
505
+ }
506
+ }
507
+ const response = await remote.query(transformedQuery);
508
+ const result = unwrapRemoteSuccess(response);
509
+ return schema ? runAfterQuery(plugins, schema, transformedQuery, result) : result;
510
+ }
511
+
512
+ // src/offline/mutate.ts
513
+ async function handleOfflineMutation(storage, mutation, timestampMs) {
514
+ const clientId = mutation.clientId;
515
+ const mutationId = mutation.mutationId;
516
+ const resource = mutation.resource;
517
+ const id = mutation.id;
518
+ try {
519
+ await storage.changelogAppend({
520
+ clientId,
521
+ mutationId,
522
+ mutation,
523
+ timestampMs
524
+ });
525
+ } catch (err) {
526
+ throw err;
527
+ }
528
+ const operation = mutation.operation;
529
+ const record = mutation.record || {};
530
+ if (operation === "delete") {
531
+ await storage.deleteRecord(resource, id);
532
+ } else if (operation === "merge") {
533
+ const existing = await storage.getRecord(resource, id);
534
+ const merged = existing ? { ...existing, ...record } : { ...record, id };
535
+ merged.id = id;
536
+ await storage.upsertRecord(resource, merged);
537
+ } else if (operation === "insert" || operation === "replace") {
538
+ const toWrite = { ...record, id };
539
+ await storage.upsertRecord(resource, toWrite);
540
+ }
541
+ return {
542
+ ok: true,
543
+ mutationId,
544
+ affectedIds: [id],
545
+ deduped: false
546
+ // local apply is fresh
547
+ };
548
+ }
549
+
550
+ // src/mutate.ts
551
+ async function executeMutation(remote, eventBus, getTimestamp, m, storage, plugins = [], schema) {
552
+ const transformedMutation = schema ? await runBeforeMutation(plugins, schema, m) : m;
553
+ let result;
554
+ let fromOfflineFallback = false;
555
+ try {
556
+ const response = await remote.mutation(transformedMutation);
557
+ result = unwrapRemoteSuccess(response);
558
+ result = schema ? await runAfterMutation(plugins, schema, transformedMutation, result) : result;
559
+ } catch (err) {
560
+ if (!Array.isArray(transformedMutation)) {
561
+ emitRejectionForError(
562
+ eventBus,
563
+ getTimestamp,
564
+ transformedMutation,
565
+ err
566
+ );
567
+ } else {
568
+ for (const mut of transformedMutation) {
569
+ emitRejectionForError(eventBus, getTimestamp, mut, err);
570
+ }
571
+ }
572
+ if (storage && !Array.isArray(m) && isTransportError(err)) {
573
+ try {
574
+ result = await handleOfflineMutation(
575
+ storage,
576
+ m,
577
+ getTimestamp()
578
+ );
579
+ fromOfflineFallback = true;
580
+ } catch (offlineErr) {
581
+ throw offlineErr;
582
+ }
583
+ } else {
584
+ throw err;
585
+ }
586
+ }
587
+ if (!Array.isArray(m)) {
588
+ emitMutationEvents(eventBus, getTimestamp, m, result);
589
+ return result;
590
+ }
591
+ const mutations = m;
592
+ const results = result;
593
+ for (let i = 0; i < mutations.length; i++) {
594
+ emitMutationEvents(eventBus, getTimestamp, mutations[i], results[i]);
595
+ }
596
+ return results;
597
+ }
598
+ function emitMutationEvents(eventBus, getTimestamp, mutation, result) {
599
+ const action = mutation.operation;
600
+ let fields;
601
+ if (mutation.operation !== "delete" && mutation.record) {
602
+ fields = Object.keys(mutation.record).filter((k) => k !== "id").sort();
603
+ }
604
+ const baseEvent = {
605
+ resource: mutation.resource,
606
+ ids: Array.isArray(mutation.id) ? mutation.id : [mutation.id],
607
+ mutationId: mutation.mutationId,
608
+ clientId: mutation.clientId,
609
+ timestampMs: getTimestamp(),
610
+ action,
611
+ // NEW: CLIENT-EVENT-001
612
+ fields
613
+ // NEW: CLIENT-EVENT-001
614
+ };
615
+ if (result.ok) {
616
+ eventBus.emit({
617
+ type: "mutation_applied",
618
+ ...baseEvent
619
+ });
620
+ } else {
621
+ const errorContext = result.errors?.[0] || {
622
+ code: "UNKNOWN",
623
+ message: "Mutation failed",
624
+ path: "$"
625
+ };
626
+ eventBus.emit({
627
+ type: "mutation_rejected",
628
+ ...baseEvent,
629
+ context: errorContext
630
+ });
631
+ }
632
+ }
633
+ function emitRejectionForError(eventBus, getTimestamp, mutation, error) {
634
+ const action = mutation.operation;
635
+ let fields;
636
+ if (mutation.operation !== "delete" && mutation.record) {
637
+ fields = Object.keys(mutation.record).filter((k) => k !== "id").sort();
638
+ }
639
+ const errorContext = {
640
+ code: error.code || "INTERNAL",
641
+ message: error.message || "Remote error",
642
+ path: error.path || "$"
643
+ };
644
+ eventBus.emit({
645
+ type: "mutation_rejected",
646
+ resource: mutation.resource,
647
+ ids: Array.isArray(mutation.id) ? mutation.id : [mutation.id],
648
+ mutationId: mutation.mutationId,
649
+ clientId: mutation.clientId,
650
+ timestampMs: getTimestamp(),
651
+ action,
652
+ fields,
653
+ context: errorContext
654
+ });
655
+ }
656
+
657
+ // src/transact.ts
658
+ async function executeTransact(remote, payload) {
659
+ const response = await remote.transact(payload);
660
+ return unwrapRemoteSuccess(response);
661
+ }
662
+
663
+ // src/signals/querySignal.ts
664
+ var import_core = require("@datafn/core");
665
+ var SignalRegistry = class {
666
+ constructor(client, eventBus) {
667
+ this.signals = /* @__PURE__ */ new Map();
668
+ this.client = client;
669
+ this.eventBus = eventBus;
670
+ }
671
+ getSignal(fullQuery) {
672
+ const key = (0, import_core.dfqlKey)(fullQuery);
673
+ if (this.signals.has(key)) {
674
+ return this.signals.get(key);
675
+ }
676
+ const signal = createQuerySignal(this.client, this.eventBus, fullQuery);
677
+ this.signals.set(key, signal);
678
+ return signal;
679
+ }
680
+ };
681
+ function createQuerySignal(client, eventBus, fullQuery) {
682
+ let currentValue;
683
+ let status = "idle";
684
+ const subscribers = /* @__PURE__ */ new Set();
685
+ let inFlight = false;
686
+ let queuedRefresh = false;
687
+ const resource = fullQuery.resource;
688
+ const fetchQuery = async (isRefresh = false) => {
689
+ if (inFlight) {
690
+ if (isRefresh) {
691
+ queuedRefresh = true;
692
+ }
693
+ return;
694
+ }
695
+ inFlight = true;
696
+ queuedRefresh = false;
697
+ try {
698
+ const result = await client.query(fullQuery);
699
+ currentValue = result;
700
+ status = "idle";
701
+ if (!isRefresh || currentValue !== void 0) {
702
+ subscribers.forEach((fn) => fn(currentValue));
703
+ }
704
+ } catch (error) {
705
+ if (!isRefresh) {
706
+ status = "error";
707
+ throw error;
708
+ }
709
+ } finally {
710
+ inFlight = false;
711
+ if (queuedRefresh) {
712
+ queuedRefresh = false;
713
+ fetchQuery(true);
714
+ }
715
+ }
716
+ };
717
+ eventBus.subscribe((event) => {
718
+ if (event.type === "mutation_applied" && event.resource === resource) {
719
+ fetchQuery(true);
720
+ }
721
+ });
722
+ return {
723
+ subscribe(fn) {
724
+ subscribers.add(fn);
725
+ if (subscribers.size === 1 && status === "idle" && currentValue === void 0) {
726
+ fetchQuery(false).catch(() => {
727
+ });
728
+ } else if (currentValue !== void 0) {
729
+ fn(currentValue);
730
+ }
731
+ return () => {
732
+ subscribers.delete(fn);
733
+ };
734
+ },
735
+ get() {
736
+ return currentValue;
737
+ }
738
+ };
739
+ }
740
+
741
+ // src/sync/apply.ts
742
+ async function applyCloneResult(storage, result) {
743
+ if (!result.ok) {
744
+ return;
745
+ }
746
+ const { data, cursors } = result;
747
+ for (const [resource, records] of Object.entries(data)) {
748
+ await storage.setHydrationState(resource, "hydrating");
749
+ for (const record of records) {
750
+ await storage.upsertRecord(resource, record);
751
+ }
752
+ const cursor = cursors[resource];
753
+ if (cursor !== void 0) {
754
+ await storage.setCursor(resource, cursor);
755
+ }
756
+ await storage.setHydrationState(resource, "ready");
757
+ }
758
+ }
759
+ async function applyPullResult(storage, result) {
760
+ if (!result.ok) {
761
+ return;
762
+ }
763
+ const { records, deleted, cursors } = result;
764
+ for (const [resource, resourceRecords] of Object.entries(records)) {
765
+ for (const record of resourceRecords) {
766
+ await storage.upsertRecord(resource, record);
767
+ }
768
+ }
769
+ for (const [resource, deletedIds] of Object.entries(deleted)) {
770
+ for (const id of deletedIds) {
771
+ await storage.deleteRecord(resource, id);
772
+ }
773
+ }
774
+ for (const [resource, newCursor] of Object.entries(cursors)) {
775
+ await setCursorMonotonically(storage, resource, newCursor);
776
+ }
777
+ }
778
+ async function setCursorMonotonically(storage, resource, newCursor) {
779
+ const existingCursor = await storage.getCursor(resource);
780
+ if (existingCursor === null) {
781
+ await storage.setCursor(resource, newCursor);
782
+ return;
783
+ }
784
+ const existingSeq = parseInt(existingCursor, 10);
785
+ const newSeq = parseInt(newCursor, 10);
786
+ if (newSeq > existingSeq) {
787
+ await storage.setCursor(resource, newCursor);
788
+ }
789
+ }
790
+
791
+ // src/sync.ts
792
+ function createSyncFacade(remote, storage) {
793
+ const callSyncMethod = async (methodName, payload) => {
794
+ const method = remote[methodName];
795
+ if (typeof method !== "function") {
796
+ throw createClientError(
797
+ "TRANSPORT_ERROR",
798
+ `Transport error: remote method missing: ${methodName}`,
799
+ { path: `sync.${methodName}` }
800
+ );
801
+ }
802
+ const response = await method.call(remote, payload);
803
+ return unwrapRemoteSuccess(response);
804
+ };
805
+ return {
806
+ async seed(payload) {
807
+ return callSyncMethod("seed", payload);
808
+ },
809
+ async clone(payload) {
810
+ const result = await callSyncMethod("clone", payload);
811
+ if (storage) {
812
+ await applyCloneResult(storage, result);
813
+ }
814
+ return result;
815
+ },
816
+ async pull(payload) {
817
+ const result = await callSyncMethod("pull", payload);
818
+ if (storage) {
819
+ await applyPullResult(storage, result);
820
+ }
821
+ return result;
822
+ },
823
+ async push(payload) {
824
+ return callSyncMethod("push", payload);
825
+ }
826
+ };
827
+ }
828
+
829
+ // src/client.ts
830
+ function createDatafnClient(config) {
831
+ const validationResult = (0, import_core2.validateSchema)(config.schema);
832
+ if (!validationResult.ok) {
833
+ createClientError(
834
+ validationResult.error.code,
835
+ validationResult.error.message,
836
+ validationResult.error.details
837
+ );
838
+ }
839
+ const schema = validationResult.result;
840
+ const eventBus = new EventBus();
841
+ const getTimestamp = config.getTimestamp || (() => Date.now());
842
+ const RESERVED_KEYS = /* @__PURE__ */ new Set(["then", "toJSON", "inspect"]);
843
+ const client = {
844
+ table: null,
845
+ // Will be set below
846
+ /**
847
+ * Execute a query (CLIENT-QUERY-001, CLIENT-OFFLINE-QUERY-001)
848
+ */
849
+ async query(q) {
850
+ return executeQuery(
851
+ config.remote,
852
+ q,
853
+ config.storage,
854
+ config.plugins || [],
855
+ schema
856
+ );
857
+ },
858
+ /**
859
+ * Sync facade (CLIENT-SYNC-001, CLIENT-SYNC-APPLY-001)
860
+ */
861
+ sync: createSyncFacade(config.remote, config.storage),
862
+ /**
863
+ * Execute a transaction (CLIENT-TX-001)
864
+ */
865
+ async transact(payload) {
866
+ return executeTransact(config.remote, payload);
867
+ },
868
+ /**
869
+ * Execute a mutation (CLIENT-MUT-001, CLIENT-OFFLINE-MUT-001)
870
+ */
871
+ async mutate(mutation) {
872
+ return executeMutation(
873
+ config.remote,
874
+ eventBus,
875
+ getTimestamp,
876
+ mutation,
877
+ config.storage,
878
+ config.plugins || [],
879
+ schema
880
+ );
881
+ },
882
+ /**
883
+ * Subscribe to events
884
+ */
885
+ subscribe(handler, filter) {
886
+ return eventBus.subscribe(handler, filter);
887
+ }
888
+ };
889
+ const signalRegistry = new SignalRegistry(client, eventBus);
890
+ const registry = new TableRegistry(schema, client, signalRegistry);
891
+ client.table = (name) => registry.getTable(name);
892
+ return new Proxy(client, {
893
+ get(target, prop) {
894
+ if (typeof prop === "string" && RESERVED_KEYS.has(prop)) {
895
+ return void 0;
896
+ }
897
+ if (prop in target) {
898
+ return target[prop];
899
+ }
900
+ if (typeof prop === "string") {
901
+ return registry.getTable(prop);
902
+ }
903
+ return void 0;
904
+ }
905
+ });
906
+ }
907
+
908
+ // src/adapters/memoryStorage.ts
909
+ var MemoryStorageAdapter = class {
910
+ constructor() {
911
+ this.records = /* @__PURE__ */ new Map();
912
+ this.joinRows = /* @__PURE__ */ new Map();
913
+ this.cursors = /* @__PURE__ */ new Map();
914
+ this.hydration = /* @__PURE__ */ new Map();
915
+ this.changelog = [];
916
+ this.changelogSeq = 1;
917
+ }
918
+ // --- Records ---
919
+ async getRecord(resource, id) {
920
+ const table = this.records.get(resource);
921
+ return table?.get(id) || null;
922
+ }
923
+ async listRecords(resource) {
924
+ const table = this.records.get(resource);
925
+ if (!table) return [];
926
+ return Array.from(table.values()).sort((a, b) => {
927
+ const idA = a.id || "";
928
+ const idB = b.id || "";
929
+ return idA < idB ? -1 : idA > idB ? 1 : 0;
930
+ });
931
+ }
932
+ async upsertRecord(resource, record) {
933
+ if (!this.records.has(resource)) {
934
+ this.records.set(resource, /* @__PURE__ */ new Map());
935
+ }
936
+ const id = record.id;
937
+ if (!id) throw new Error("Record missing id");
938
+ this.records.get(resource).set(id, record);
939
+ }
940
+ async deleteRecord(resource, id) {
941
+ const table = this.records.get(resource);
942
+ if (table) {
943
+ table.delete(id);
944
+ }
945
+ }
946
+ // --- Join Rows ---
947
+ async listJoinRows(relationKey) {
948
+ const table = this.joinRows.get(relationKey);
949
+ if (!table) return [];
950
+ return Array.from(table.values()).sort((a, b) => {
951
+ const keyA = `${a.from}:${a.to}`;
952
+ const keyB = `${b.from}:${b.to}`;
953
+ return keyA < keyB ? -1 : keyA > keyB ? 1 : 0;
954
+ });
955
+ }
956
+ async upsertJoinRow(relationKey, row) {
957
+ if (!this.joinRows.has(relationKey)) {
958
+ this.joinRows.set(relationKey, /* @__PURE__ */ new Map());
959
+ }
960
+ const key = `${row.from}:${row.to}`;
961
+ this.joinRows.get(relationKey).set(key, row);
962
+ }
963
+ async deleteJoinRow(relationKey, from, to) {
964
+ const table = this.joinRows.get(relationKey);
965
+ if (table) {
966
+ table.delete(`${from}:${to}`);
967
+ }
968
+ }
969
+ // --- Sync State ---
970
+ async getCursor(resource) {
971
+ return this.cursors.get(resource) || null;
972
+ }
973
+ async setCursor(resource, cursor) {
974
+ this.cursors.set(resource, cursor);
975
+ }
976
+ async getHydrationState(resource) {
977
+ return this.hydration.get(resource) || "notStarted";
978
+ }
979
+ async setHydrationState(resource, state) {
980
+ this.hydration.set(resource, state);
981
+ }
982
+ // --- Changelog ---
983
+ async changelogAppend(entry) {
984
+ const existing = this.changelog.find(
985
+ (e) => e.clientId === entry.clientId && e.mutationId === entry.mutationId
986
+ );
987
+ if (existing) {
988
+ return existing;
989
+ }
990
+ const newEntry = {
991
+ ...entry,
992
+ seq: this.changelogSeq++
993
+ };
994
+ this.changelog.push(newEntry);
995
+ return newEntry;
996
+ }
997
+ async changelogList(options = {}) {
998
+ const limit = options.limit || 100;
999
+ return this.changelog.slice(0, limit);
1000
+ }
1001
+ async changelogAck(options) {
1002
+ this.changelog = this.changelog.filter((e) => e.seq > options.throughSeq);
1003
+ }
1004
+ // Test helper to clear state
1005
+ clear() {
1006
+ this.records.clear();
1007
+ this.joinRows.clear();
1008
+ this.cursors.clear();
1009
+ this.hydration.clear();
1010
+ this.changelog = [];
1011
+ this.changelogSeq = 1;
1012
+ }
1013
+ };
1014
+
1015
+ // src/adapters/indexedDbStorage.ts
1016
+ var DB_NAME = "datafn_client_db";
1017
+ var DB_VERSION = 1;
1018
+ var IndexedDbStorageAdapter = class {
1019
+ constructor(dbName = DB_NAME) {
1020
+ this.dbPromise = new Promise((resolve, reject) => {
1021
+ const request = indexedDB.open(dbName, DB_VERSION);
1022
+ request.onerror = () => reject(request.error);
1023
+ request.onsuccess = () => resolve(request.result);
1024
+ request.onupgradeneeded = (event) => {
1025
+ const db = request.result;
1026
+ if (!db.objectStoreNames.contains("records")) {
1027
+ const store = db.createObjectStore("records", {
1028
+ keyPath: ["resource", "id"]
1029
+ });
1030
+ store.createIndex("by_resource", "resource", { unique: false });
1031
+ }
1032
+ if (!db.objectStoreNames.contains("join_rows")) {
1033
+ const store = db.createObjectStore("join_rows", {
1034
+ keyPath: ["relationKey", "from", "to"]
1035
+ });
1036
+ store.createIndex("by_relation", "relationKey", { unique: false });
1037
+ }
1038
+ if (!db.objectStoreNames.contains("meta")) {
1039
+ db.createObjectStore("meta", { keyPath: ["type", "key"] });
1040
+ }
1041
+ if (!db.objectStoreNames.contains("changelog")) {
1042
+ const store = db.createObjectStore("changelog", {
1043
+ keyPath: "seq",
1044
+ autoIncrement: true
1045
+ });
1046
+ store.createIndex("by_client_mutation", ["clientId", "mutationId"], {
1047
+ unique: true
1048
+ });
1049
+ }
1050
+ };
1051
+ });
1052
+ }
1053
+ async getStore(storeName, mode) {
1054
+ const db = await this.dbPromise;
1055
+ return db.transaction(storeName, mode).objectStore(storeName);
1056
+ }
1057
+ // --- Records ---
1058
+ async getRecord(resource, id) {
1059
+ const store = await this.getStore("records", "readonly");
1060
+ return new Promise((resolve, reject) => {
1061
+ const request = store.get([resource, id]);
1062
+ request.onsuccess = () => resolve(request.result || null);
1063
+ request.onerror = () => reject(request.error);
1064
+ });
1065
+ }
1066
+ async listRecords(resource) {
1067
+ const store = await this.getStore("records", "readonly");
1068
+ const index = store.index("by_resource");
1069
+ return new Promise((resolve, reject) => {
1070
+ const range = IDBKeyRange.bound([resource, ""], [resource, "\uFFFF"]);
1071
+ const request = store.getAll(range);
1072
+ request.onsuccess = () => resolve(request.result || []);
1073
+ request.onerror = () => reject(request.error);
1074
+ });
1075
+ }
1076
+ async upsertRecord(resource, record) {
1077
+ const store = await this.getStore("records", "readwrite");
1078
+ return new Promise((resolve, reject) => {
1079
+ const recordWithKey = { ...record, resource };
1080
+ const request = store.put(recordWithKey);
1081
+ request.onsuccess = () => resolve();
1082
+ request.onerror = () => reject(request.error);
1083
+ });
1084
+ }
1085
+ async deleteRecord(resource, id) {
1086
+ const store = await this.getStore("records", "readwrite");
1087
+ return new Promise((resolve, reject) => {
1088
+ const request = store.delete([resource, id]);
1089
+ request.onsuccess = () => resolve();
1090
+ request.onerror = () => reject(request.error);
1091
+ });
1092
+ }
1093
+ // --- Join Rows ---
1094
+ async listJoinRows(relationKey) {
1095
+ const store = await this.getStore("join_rows", "readonly");
1096
+ const range = IDBKeyRange.bound(
1097
+ [relationKey, "", ""],
1098
+ [relationKey, "\uFFFF", "\uFFFF"]
1099
+ );
1100
+ return new Promise((resolve, reject) => {
1101
+ const request = store.getAll(range);
1102
+ request.onsuccess = () => resolve(request.result || []);
1103
+ request.onerror = () => reject(request.error);
1104
+ });
1105
+ }
1106
+ async upsertJoinRow(relationKey, row) {
1107
+ const store = await this.getStore("join_rows", "readwrite");
1108
+ return new Promise((resolve, reject) => {
1109
+ const rowWithKey = { ...row, relationKey };
1110
+ const request = store.put(rowWithKey);
1111
+ request.onsuccess = () => resolve();
1112
+ request.onerror = () => reject(request.error);
1113
+ });
1114
+ }
1115
+ async deleteJoinRow(relationKey, from, to) {
1116
+ const store = await this.getStore("join_rows", "readwrite");
1117
+ return new Promise((resolve, reject) => {
1118
+ const request = store.delete([relationKey, from, to]);
1119
+ request.onsuccess = () => resolve();
1120
+ request.onerror = () => reject(request.error);
1121
+ });
1122
+ }
1123
+ // --- Sync State ---
1124
+ async getCursor(resource) {
1125
+ const store = await this.getStore("meta", "readonly");
1126
+ return new Promise((resolve, reject) => {
1127
+ const request = store.get(["cursor", resource]);
1128
+ request.onsuccess = () => resolve(request.result?.value || null);
1129
+ request.onerror = () => reject(request.error);
1130
+ });
1131
+ }
1132
+ async setCursor(resource, cursor) {
1133
+ const store = await this.getStore("meta", "readwrite");
1134
+ return new Promise((resolve, reject) => {
1135
+ const request = store.put({
1136
+ type: "cursor",
1137
+ key: resource,
1138
+ value: cursor
1139
+ });
1140
+ request.onsuccess = () => resolve();
1141
+ request.onerror = () => reject(request.error);
1142
+ });
1143
+ }
1144
+ async getHydrationState(resource) {
1145
+ const store = await this.getStore("meta", "readonly");
1146
+ return new Promise((resolve, reject) => {
1147
+ const request = store.get(["hydration", resource]);
1148
+ request.onsuccess = () => resolve(request.result?.value || "notStarted");
1149
+ request.onerror = () => reject(request.error);
1150
+ });
1151
+ }
1152
+ async setHydrationState(resource, state) {
1153
+ const store = await this.getStore("meta", "readwrite");
1154
+ return new Promise((resolve, reject) => {
1155
+ const request = store.put({
1156
+ type: "hydration",
1157
+ key: resource,
1158
+ value: state
1159
+ });
1160
+ request.onsuccess = () => resolve();
1161
+ request.onerror = () => reject(request.error);
1162
+ });
1163
+ }
1164
+ // --- Changelog ---
1165
+ async changelogAppend(entry) {
1166
+ const store = await this.getStore("changelog", "readwrite");
1167
+ const index = store.index("by_client_mutation");
1168
+ const existing = await new Promise(
1169
+ (resolve, reject) => {
1170
+ const request = index.get([entry.clientId, entry.mutationId]);
1171
+ request.onsuccess = () => resolve(request.result);
1172
+ request.onerror = () => reject(request.error);
1173
+ }
1174
+ );
1175
+ if (existing) {
1176
+ return existing;
1177
+ }
1178
+ return new Promise((resolve, reject) => {
1179
+ const request = store.add(entry);
1180
+ request.onsuccess = () => {
1181
+ const seq = request.result;
1182
+ resolve({ ...entry, seq });
1183
+ };
1184
+ request.onerror = () => reject(request.error);
1185
+ });
1186
+ }
1187
+ async changelogList(options = {}) {
1188
+ const store = await this.getStore("changelog", "readonly");
1189
+ return new Promise((resolve, reject) => {
1190
+ const limit = options.limit || 100;
1191
+ const request = store.getAll(null, limit);
1192
+ request.onsuccess = () => resolve(request.result || []);
1193
+ request.onerror = () => reject(request.error);
1194
+ });
1195
+ }
1196
+ async changelogAck(options) {
1197
+ const store = await this.getStore("changelog", "readwrite");
1198
+ const range = IDBKeyRange.upperBound(options.throughSeq);
1199
+ return new Promise((resolve, reject) => {
1200
+ const request = store.delete(range);
1201
+ request.onsuccess = () => resolve();
1202
+ request.onerror = () => reject(request.error);
1203
+ });
1204
+ }
1205
+ };
1206
+ // Annotate the CommonJS export names for ESM import in node:
1207
+ 0 && (module.exports = {
1208
+ EventBus,
1209
+ IndexedDbStorageAdapter,
1210
+ MemoryStorageAdapter,
1211
+ createClientError,
1212
+ createDatafnClient,
1213
+ matchesFilter,
1214
+ unwrapRemoteSuccess
1215
+ });
1216
+ //# sourceMappingURL=index.cjs.map