@absolutejs/sync 1.4.0 → 1.6.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.
package/dist/index.js CHANGED
@@ -423,8 +423,1693 @@ var syncSocket = ({
423
423
  }
424
424
  });
425
425
  };
426
- // src/devtools.ts
426
+ // src/engine/cdc.ts
427
427
  import { Elysia as Elysia3 } from "elysia";
428
+
429
+ // src/engine/equiJoin.ts
430
+ var shallowEqual = (a, b) => {
431
+ if (a === b) {
432
+ return true;
433
+ }
434
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
435
+ return false;
436
+ }
437
+ const aKeys = Object.keys(a);
438
+ const bKeys = Object.keys(b);
439
+ return aKeys.length === bKeys.length && aKeys.every((key) => a[key] === b[key]);
440
+ };
441
+ var addToIndex = (index, joinValue, key) => {
442
+ let bucket = index.get(joinValue);
443
+ if (bucket === undefined) {
444
+ bucket = new Set;
445
+ index.set(joinValue, bucket);
446
+ }
447
+ bucket.add(key);
448
+ };
449
+ var removeFromIndex = (index, joinValue, key) => {
450
+ const bucket = index.get(joinValue);
451
+ if (bucket === undefined) {
452
+ return;
453
+ }
454
+ bucket.delete(key);
455
+ if (bucket.size === 0) {
456
+ index.delete(joinValue);
457
+ }
458
+ };
459
+ var createEquiJoin = (options) => {
460
+ const { leftKey, rightKey, leftOn, rightOn, select, selectUnmatched } = options;
461
+ const equals = options.equals ?? shallowEqual;
462
+ const lefts = new Map;
463
+ const rights = new Map;
464
+ const leftByJoin = new Map;
465
+ const rightByJoin = new Map;
466
+ const output = new Map;
467
+ const outByLeft = new Map;
468
+ const outKey = (lk, rk) => `${lk} ${rk}`;
469
+ const unmatchedKey = (lk) => `${lk} ~`;
470
+ const leftOutputs = (lk, left) => {
471
+ const result = new Map;
472
+ const rks = rightByJoin.get(leftOn(left));
473
+ if (rks !== undefined && rks.size > 0) {
474
+ for (const rk of rks) {
475
+ const right = rights.get(rk);
476
+ if (right !== undefined) {
477
+ result.set(outKey(lk, rk), select(left, right));
478
+ }
479
+ }
480
+ } else if (selectUnmatched !== undefined) {
481
+ result.set(unmatchedKey(lk), selectUnmatched(left));
482
+ }
483
+ return result;
484
+ };
485
+ const reconcileLeft = (lk, after) => {
486
+ const before = outByLeft.get(lk) ?? new Set;
487
+ const added = [];
488
+ const removed = [];
489
+ const changed = [];
490
+ for (const [ok, value] of after) {
491
+ const previous = output.get(ok);
492
+ if (previous === undefined) {
493
+ added.push(value);
494
+ } else if (!equals(previous, value)) {
495
+ changed.push(value);
496
+ }
497
+ output.set(ok, value);
498
+ }
499
+ for (const ok of before) {
500
+ if (!after.has(ok)) {
501
+ const previous = output.get(ok);
502
+ if (previous !== undefined) {
503
+ removed.push(previous);
504
+ output.delete(ok);
505
+ }
506
+ }
507
+ }
508
+ if (after.size === 0) {
509
+ outByLeft.delete(lk);
510
+ } else {
511
+ outByLeft.set(lk, new Set(after.keys()));
512
+ }
513
+ return { added, removed, changed };
514
+ };
515
+ const mergeInto = (target, diff) => {
516
+ target.added.push(...diff.added);
517
+ target.removed.push(...diff.removed);
518
+ target.changed.push(...diff.changed);
519
+ };
520
+ return {
521
+ hydrate: (left, right) => {
522
+ lefts.clear();
523
+ rights.clear();
524
+ leftByJoin.clear();
525
+ rightByJoin.clear();
526
+ output.clear();
527
+ outByLeft.clear();
528
+ for (const right_ of right) {
529
+ const rk = rightKey(right_);
530
+ rights.set(rk, right_);
531
+ addToIndex(rightByJoin, rightOn(right_), rk);
532
+ }
533
+ for (const left_ of left) {
534
+ const lk = leftKey(left_);
535
+ lefts.set(lk, left_);
536
+ addToIndex(leftByJoin, leftOn(left_), lk);
537
+ const outs = leftOutputs(lk, left_);
538
+ for (const [ok, value] of outs) {
539
+ output.set(ok, value);
540
+ }
541
+ if (outs.size > 0) {
542
+ outByLeft.set(lk, new Set(outs.keys()));
543
+ }
544
+ }
545
+ },
546
+ applyLeft: ({ op, row }) => {
547
+ const lk = leftKey(row);
548
+ const existing = lefts.get(lk);
549
+ if (existing !== undefined) {
550
+ removeFromIndex(leftByJoin, leftOn(existing), lk);
551
+ }
552
+ if (op === "delete") {
553
+ lefts.delete(lk);
554
+ } else {
555
+ lefts.set(lk, row);
556
+ addToIndex(leftByJoin, leftOn(row), lk);
557
+ }
558
+ const after = op === "delete" ? new Map : leftOutputs(lk, row);
559
+ return reconcileLeft(lk, after);
560
+ },
561
+ applyRight: ({ op, row }) => {
562
+ const rk = rightKey(row);
563
+ const existing = rights.get(rk);
564
+ const affected = new Set;
565
+ const addAffected = (joinValue) => {
566
+ for (const lk of leftByJoin.get(joinValue) ?? []) {
567
+ affected.add(lk);
568
+ }
569
+ };
570
+ if (existing !== undefined) {
571
+ addAffected(rightOn(existing));
572
+ removeFromIndex(rightByJoin, rightOn(existing), rk);
573
+ }
574
+ if (op === "delete") {
575
+ rights.delete(rk);
576
+ } else {
577
+ rights.set(rk, row);
578
+ addToIndex(rightByJoin, rightOn(row), rk);
579
+ addAffected(rightOn(row));
580
+ }
581
+ const diff = { added: [], removed: [], changed: [] };
582
+ for (const lk of affected) {
583
+ const left = lefts.get(lk);
584
+ if (left !== undefined) {
585
+ mergeInto(diff, reconcileLeft(lk, leftOutputs(lk, left)));
586
+ }
587
+ }
588
+ return diff;
589
+ },
590
+ rows: () => [...output.values()],
591
+ size: () => output.size
592
+ };
593
+ };
594
+
595
+ // src/engine/materializedView.ts
596
+ var emptyDiff = () => ({
597
+ added: [],
598
+ removed: [],
599
+ changed: []
600
+ });
601
+ var shallowEqual2 = (a, b) => {
602
+ if (a === b) {
603
+ return true;
604
+ }
605
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
606
+ return false;
607
+ }
608
+ const aKeys = Object.keys(a);
609
+ const bKeys = Object.keys(b);
610
+ if (aKeys.length !== bKeys.length) {
611
+ return false;
612
+ }
613
+ return aKeys.every((key) => a[key] === b[key]);
614
+ };
615
+ var isEmptyViewDiff = (diff) => diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0;
616
+ var createMaterializedView = (options) => {
617
+ const { key, match } = options;
618
+ const equals = options.equals ?? shallowEqual2;
619
+ const set = new Map;
620
+ return {
621
+ hydrate: (rows) => {
622
+ set.clear();
623
+ for (const row of rows) {
624
+ set.set(key(row), row);
625
+ }
626
+ },
627
+ reset: (rows) => {
628
+ const next = new Map;
629
+ const added = [];
630
+ const changed = [];
631
+ for (const row of rows) {
632
+ const rowKey = key(row);
633
+ next.set(rowKey, row);
634
+ const previous = set.get(rowKey);
635
+ if (previous === undefined) {
636
+ added.push(row);
637
+ } else if (!equals(previous, row)) {
638
+ changed.push(row);
639
+ }
640
+ }
641
+ const removed = [];
642
+ for (const [rowKey, previous] of set) {
643
+ if (!next.has(rowKey)) {
644
+ removed.push(previous);
645
+ }
646
+ }
647
+ set.clear();
648
+ for (const [rowKey, row] of next) {
649
+ set.set(rowKey, row);
650
+ }
651
+ return { added, removed, changed };
652
+ },
653
+ apply: ({ op, row }) => {
654
+ const rowKey = key(row);
655
+ const existing = set.get(rowKey);
656
+ if (op === "delete") {
657
+ if (existing === undefined) {
658
+ return emptyDiff();
659
+ }
660
+ set.delete(rowKey);
661
+ return { added: [], removed: [existing], changed: [] };
662
+ }
663
+ if (match(row)) {
664
+ set.set(rowKey, row);
665
+ return existing === undefined ? { added: [row], removed: [], changed: [] } : { added: [], removed: [], changed: [row] };
666
+ }
667
+ if (existing !== undefined) {
668
+ set.delete(rowKey);
669
+ return { added: [], removed: [existing], changed: [] };
670
+ }
671
+ return emptyDiff();
672
+ },
673
+ rows: () => [...set.values()],
674
+ size: () => set.size
675
+ };
676
+ };
677
+
678
+ // src/engine/retry.ts
679
+ var PG_RETRY_CODES = new Set(["40001", "40P01"]);
680
+ var isSerializationFailure = (error) => {
681
+ if (error === null || typeof error !== "object")
682
+ return false;
683
+ const code = error.code;
684
+ if (typeof code === "string" && PG_RETRY_CODES.has(code))
685
+ return true;
686
+ const cause = error.cause;
687
+ if (cause !== undefined)
688
+ return isSerializationFailure(cause);
689
+ return false;
690
+ };
691
+ var exponentialBackoff = (options = {}) => (attempt) => {
692
+ const base = options.baseMs ?? 25;
693
+ const factor = options.factor ?? 2;
694
+ const max = options.maxMs ?? 1000;
695
+ const jitter = options.jitter ?? 0.2;
696
+ const raw = Math.min(max, base * factor ** Math.max(0, attempt - 1));
697
+ const spread = raw * jitter;
698
+ return raw + (Math.random() * 2 - 1) * spread;
699
+ };
700
+
701
+ class RetriesExhaustedError extends Error {
702
+ attempts;
703
+ elapsedMs;
704
+ cause;
705
+ constructor(attempts, elapsedMs, cause) {
706
+ const message = cause instanceof Error ? cause.message : String(cause);
707
+ super(`retries exhausted after ${attempts} attempts (${elapsedMs}ms): ${message}`);
708
+ this.name = "RetriesExhaustedError";
709
+ this.attempts = attempts;
710
+ this.elapsedMs = elapsedMs;
711
+ this.cause = cause;
712
+ }
713
+ }
714
+
715
+ // src/engine/sandbox.ts
716
+ var isolatedJscModule;
717
+ var loadIsolatedJsc = async () => {
718
+ if (isolatedJscModule !== undefined)
719
+ return isolatedJscModule;
720
+ try {
721
+ isolatedJscModule = await import("@absolutejs/isolated-jsc");
722
+ return isolatedJscModule;
723
+ } catch (error) {
724
+ throw new Error('sandboxedHandler requires the optional peer "@absolutejs/isolated-jsc". Install it with: bun add @absolutejs/isolated-jsc', { cause: error });
725
+ }
726
+ };
727
+ var wrap = (source) => `
728
+ (async () => {
729
+ const userFn = (${source});
730
+ if (typeof userFn !== 'function') {
731
+ throw new Error(
732
+ 'sandboxedHandler must evaluate to (args, ctx, actions) => result; got ' +
733
+ typeof userFn
734
+ );
735
+ }
736
+ const actions = {
737
+ insert: __syncActionInsert,
738
+ update: __syncActionUpdate,
739
+ delete: __syncActionDelete,
740
+ change: __syncActionChange
741
+ };
742
+ return await userFn(args, ctx, actions);
743
+ })()
744
+ `;
745
+ var compile = async (source, config) => {
746
+ const { createIsolate } = await loadIsolatedJsc();
747
+ const isolate = await createIsolate({
748
+ memoryLimit: config.memoryLimit ?? 32
749
+ });
750
+ const script = await isolate.compileScript(wrap(source));
751
+ return { isolate, script, timeoutMs: config.timeout ?? 5000 };
752
+ };
753
+ var makeSandboxedHandler = (source, config = {}) => {
754
+ let pending;
755
+ const getCompiled = async () => {
756
+ if (pending !== undefined) {
757
+ const compiled = await pending;
758
+ if (!compiled.isolate.isDisposed)
759
+ return compiled;
760
+ pending = undefined;
761
+ }
762
+ pending = compile(source, config);
763
+ return pending;
764
+ };
765
+ return async (args, ctx, actions) => {
766
+ const { Reference } = await loadIsolatedJsc();
767
+ const compiled = await getCompiled();
768
+ const context = await compiled.isolate.createContext();
769
+ try {
770
+ await context.setGlobal("args", args);
771
+ await context.setGlobal("ctx", ctx);
772
+ await context.setGlobal("__syncActionInsert", new Reference((table, data) => actions.insert(table, data)));
773
+ await context.setGlobal("__syncActionUpdate", new Reference((table, data) => actions.update(table, data)));
774
+ await context.setGlobal("__syncActionDelete", new Reference((table, row) => actions.delete(table, row)));
775
+ await context.setGlobal("__syncActionChange", new Reference((collection, change) => actions.change(collection, change)));
776
+ return await compiled.script.run(context, {
777
+ timeout: compiled.timeoutMs
778
+ });
779
+ } finally {
780
+ await context.dispose().catch(() => {});
781
+ }
782
+ };
783
+ };
784
+
785
+ // src/engine/search.ts
786
+ var SEARCH_SCORE_FIELD = "_score";
787
+ var defineSearchCollection = (definition) => ({
788
+ ...definition,
789
+ kind: "search"
790
+ });
791
+
792
+ // src/engine/syncEngine.ts
793
+ class UnauthorizedError extends Error {
794
+ constructor(subject) {
795
+ super(`Not authorized: ${subject}`);
796
+ this.name = "UnauthorizedError";
797
+ }
798
+ }
799
+
800
+ class SchemaError extends Error {
801
+ constructor(table, fieldName) {
802
+ super(`Schema violation on "${table}": invalid field "${fieldName}"`);
803
+ this.name = "SchemaError";
804
+ }
805
+ }
806
+
807
+ class MissedChangesError extends Error {
808
+ requestedSince;
809
+ availableSince;
810
+ constructor(requestedSince, availableSince) {
811
+ super(`Change log no longer covers version ${requestedSince}; oldest available is ${availableSince}. ` + `Re-bootstrap and resume from ${availableSince}.`);
812
+ this.name = "MissedChangesError";
813
+ this.requestedSince = requestedSince;
814
+ this.availableSince = availableSince;
815
+ }
816
+ }
817
+
818
+ class CdcConsumerSlowError extends Error {
819
+ maxBuffer;
820
+ lastDeliveredVersion;
821
+ constructor(maxBuffer, lastDeliveredVersion) {
822
+ super(`CDC stream buffer overflowed (max ${maxBuffer}); consumer fell behind. ` + `Last delivered version: ${lastDeliveredVersion}. Resubscribe with since=${lastDeliveredVersion}.`);
823
+ this.name = "CdcConsumerSlowError";
824
+ this.maxBuffer = maxBuffer;
825
+ this.lastDeliveredVersion = lastDeliveredVersion;
826
+ }
827
+ }
828
+ var defaultKey = (row) => row.id;
829
+ var shallowEqual3 = (a, b) => {
830
+ if (a === b) {
831
+ return true;
832
+ }
833
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
834
+ return false;
835
+ }
836
+ const aKeys = Object.keys(a);
837
+ const bKeys = Object.keys(b);
838
+ return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
839
+ };
840
+ var subKeyIds = new WeakMap;
841
+ var subKeyCounter = 0;
842
+ var stableValueKey = (value) => {
843
+ if (value === undefined)
844
+ return "u";
845
+ if (value === null)
846
+ return "n";
847
+ const tag = typeof value;
848
+ if (tag === "string")
849
+ return `s:${value}`;
850
+ if (tag === "number" || tag === "boolean" || tag === "bigint") {
851
+ return `${tag[0]}:${String(value)}`;
852
+ }
853
+ if (tag !== "object")
854
+ return `${tag[0]}:fn`;
855
+ try {
856
+ return `o:${JSON.stringify(value, (_k, v) => {
857
+ if (v === null || typeof v !== "object" || Array.isArray(v))
858
+ return v;
859
+ const record = v;
860
+ const sorted = {};
861
+ for (const key of Object.keys(record).sort()) {
862
+ sorted[key] = record[key];
863
+ }
864
+ return sorted;
865
+ })}`;
866
+ } catch {
867
+ const obj = value;
868
+ let id = subKeyIds.get(obj);
869
+ if (id === undefined) {
870
+ subKeyCounter += 1;
871
+ id = `i${subKeyCounter}`;
872
+ subKeyIds.set(obj, id);
873
+ }
874
+ return `i:${id}`;
875
+ }
876
+ };
877
+ var stableSubKey = (collection, params, ctx) => `${collection}|${stableValueKey(params)}|${stableValueKey(ctx)}`;
878
+ var equalsIgnoringScore = (a, b) => {
879
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
880
+ return a === b;
881
+ }
882
+ const strip = (value) => Object.keys(value).filter((k) => k !== SEARCH_SCORE_FIELD);
883
+ const aKeys = strip(a);
884
+ const bKeys = strip(b);
885
+ return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
886
+ };
887
+ var createSyncEngine = (options = {}) => {
888
+ const registry = new Map;
889
+ const mutations = new Map;
890
+ const sandboxRunners = new Map;
891
+ const writers = new Map;
892
+ const readers = new Map;
893
+ const schedules = new Map;
894
+ const permissions = new Map;
895
+ for (const [table, rules] of Object.entries(options.permissions ?? {})) {
896
+ permissions.set(table, rules);
897
+ }
898
+ const readRuleFor = (table) => permissions.get(table)?.read;
899
+ const writeRuleFor = (table, op) => {
900
+ const rules = permissions.get(table);
901
+ return rules?.[op] ?? rules?.write;
902
+ };
903
+ const schemas = new Map;
904
+ for (const [table, schema] of Object.entries(options.schemas ?? {})) {
905
+ schemas.set(table, schema);
906
+ }
907
+ const crdtFields = new Map;
908
+ const validateWrite = (table, op, row) => {
909
+ const schema = schemas.get(table);
910
+ if (schema === undefined || typeof row !== "object" || row === null) {
911
+ return;
912
+ }
913
+ const record = row;
914
+ for (const [fieldName, validate] of Object.entries(schema.fields)) {
915
+ const present = fieldName in record;
916
+ if (op === "update" && !present) {
917
+ continue;
918
+ }
919
+ if (!validate(record[fieldName])) {
920
+ throw new SchemaError(table, fieldName);
921
+ }
922
+ }
923
+ };
924
+ const migrateRow = (table, row) => {
925
+ const migrate = schemas.get(table)?.migrate;
926
+ return migrate ? migrate(row) : row;
927
+ };
928
+ const reactiveSubs = new Set;
929
+ const searchSubs = new Set;
930
+ const searchIndexes = new Map;
931
+ const active = new Map;
932
+ const tableIndex = new Map;
933
+ const changeLogSize = options.changeLogSize ?? 1024;
934
+ const changeLog = [];
935
+ let version = 0;
936
+ const reactiveCacheMax = options.reactiveCache?.max ?? 256;
937
+ const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
938
+ const cachedReruns = new Map;
939
+ const touchCacheEntry = (key, entry) => {
940
+ cachedReruns.delete(key);
941
+ cachedReruns.set(key, entry);
942
+ };
943
+ const readCacheEntry = (key) => {
944
+ if (reactiveCacheMax <= 0)
945
+ return;
946
+ const entry = cachedReruns.get(key);
947
+ if (entry === undefined)
948
+ return;
949
+ if (reactiveCacheTtlMs > 0 && entry.expiresAt < Date.now()) {
950
+ cachedReruns.delete(key);
951
+ return;
952
+ }
953
+ touchCacheEntry(key, entry);
954
+ return entry;
955
+ };
956
+ const writeCacheEntry = (entry) => {
957
+ if (reactiveCacheMax <= 0)
958
+ return;
959
+ cachedReruns.set(entry.rerunKey, entry);
960
+ while (cachedReruns.size > reactiveCacheMax) {
961
+ const oldest = cachedReruns.keys().next().value;
962
+ if (oldest === undefined)
963
+ break;
964
+ cachedReruns.delete(oldest);
965
+ }
966
+ };
967
+ const activityListeners = new Set;
968
+ const emitActivity = (event) => {
969
+ for (const listener of activityListeners) {
970
+ listener(event);
971
+ }
972
+ };
973
+ const streamSubscribers = new Set;
974
+ const runInTransaction = options.transaction;
975
+ const instanceId = globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
976
+ let clusterBus;
977
+ const broadcast = (changes) => {
978
+ if (clusterBus !== undefined && changes.length > 0) {
979
+ clusterBus.publish({ changes, origin: instanceId });
980
+ }
981
+ };
982
+ const subsFor = (collection) => {
983
+ let set = active.get(collection);
984
+ if (set === undefined) {
985
+ set = new Set;
986
+ active.set(collection, set);
987
+ }
988
+ return set;
989
+ };
990
+ const addTableIndex = (table, name) => {
991
+ let set = tableIndex.get(table);
992
+ if (set === undefined) {
993
+ set = new Set;
994
+ tableIndex.set(table, set);
995
+ }
996
+ set.add(name);
997
+ };
998
+ const sideChange = (change, match) => change.op !== "delete" && match !== undefined && !match(change.row) ? { op: "delete", row: change.row } : change;
999
+ const EMPTY_DIFF = {
1000
+ added: [],
1001
+ removed: [],
1002
+ changed: []
1003
+ };
1004
+ const subscriptionDiff = async (subscription, table, change) => {
1005
+ if (subscription.kind === "graph") {
1006
+ return subscription.instance.applyChange(table, change);
1007
+ }
1008
+ if (subscription.kind === "join") {
1009
+ const js = subscription.join;
1010
+ if (table === js.leftTable) {
1011
+ return js.op.applyLeft(sideChange(change, js.leftMatch));
1012
+ }
1013
+ if (table === js.rightTable) {
1014
+ return js.op.applyRight(sideChange(change, js.rightMatch));
1015
+ }
1016
+ return EMPTY_DIFF;
1017
+ }
1018
+ if (subscription.kind === "reactive") {
1019
+ return EMPTY_DIFF;
1020
+ }
1021
+ if (subscription.kind === "search") {
1022
+ return EMPTY_DIFF;
1023
+ }
1024
+ if (subscription.incremental) {
1025
+ try {
1026
+ return subscription.view.apply(change);
1027
+ } catch {
1028
+ return subscription.view.reset(await subscription.rehydrate());
1029
+ }
1030
+ }
1031
+ return subscription.view.reset(await subscription.rehydrate());
1032
+ };
1033
+ const subscriptionsForTable = function* (table) {
1034
+ const names = tableIndex.get(table);
1035
+ if (names === undefined) {
1036
+ return;
1037
+ }
1038
+ for (const name of names) {
1039
+ const set = active.get(name);
1040
+ if (set === undefined) {
1041
+ continue;
1042
+ }
1043
+ yield* set;
1044
+ }
1045
+ };
1046
+ const mergeViewDiffs = (diffs, key) => {
1047
+ const net = new Map;
1048
+ for (const diff of diffs) {
1049
+ for (const row of diff.removed) {
1050
+ const previous = net.get(key(row));
1051
+ if (previous?.state === "added") {
1052
+ net.delete(key(row));
1053
+ } else {
1054
+ net.set(key(row), { state: "removed", row });
1055
+ }
1056
+ }
1057
+ for (const row of diff.added) {
1058
+ const previous = net.get(key(row));
1059
+ net.set(key(row), {
1060
+ state: previous?.state === "removed" ? "changed" : "added",
1061
+ row
1062
+ });
1063
+ }
1064
+ for (const row of diff.changed) {
1065
+ const previous = net.get(key(row));
1066
+ net.set(key(row), {
1067
+ state: previous?.state === "added" ? "added" : "changed",
1068
+ row
1069
+ });
1070
+ }
1071
+ }
1072
+ const added = [];
1073
+ const changed = [];
1074
+ const removed = [];
1075
+ for (const { state, row } of net.values()) {
1076
+ if (state === "added") {
1077
+ added.push(row);
1078
+ } else if (state === "changed") {
1079
+ changed.push(row);
1080
+ } else {
1081
+ removed.push(row);
1082
+ }
1083
+ }
1084
+ return { added, changed, removed };
1085
+ };
1086
+ const depKey = (table, key) => `${table} ${key}`;
1087
+ const changedKeyFor = (table, change) => readers.get(table)?.key?.(change.row);
1088
+ const makeReadHandle = (ctx, readTables, readKeys, rangeDeps, applyRules = true) => {
1089
+ const readerFor = (table) => {
1090
+ const reader = readers.get(table);
1091
+ if (reader === undefined) {
1092
+ throw new Error(`No reader registered for table "${table}" \u2014 register one with engine.registerReader`);
1093
+ }
1094
+ return reader;
1095
+ };
1096
+ const ruleFor = (table) => applyRules ? readRuleFor(table) : undefined;
1097
+ return {
1098
+ all: async (table) => {
1099
+ readTables.add(table);
1100
+ const rows = [...await readerFor(table).all(ctx)].map((row) => migrateRow(table, row));
1101
+ const rule = ruleFor(table);
1102
+ return rule ? rows.filter((row) => rule(ctx, row)) : rows;
1103
+ },
1104
+ get: async (table, key) => {
1105
+ const reader = readerFor(table);
1106
+ if (reader.get === undefined) {
1107
+ throw new Error(`Reader for table "${table}" has no get(); use db.all() or add get`);
1108
+ }
1109
+ if (reader.key !== undefined) {
1110
+ readKeys.add(depKey(table, key));
1111
+ } else {
1112
+ readTables.add(table);
1113
+ }
1114
+ const raw = await reader.get(key, ctx);
1115
+ const row = raw === undefined ? undefined : migrateRow(table, raw);
1116
+ const rule = ruleFor(table);
1117
+ return rule && row !== undefined && !rule(ctx, row) ? undefined : row;
1118
+ },
1119
+ where: async (table, predicate) => {
1120
+ const reader = readerFor(table);
1121
+ const rule = ruleFor(table);
1122
+ const effective = rule ? (row) => predicate(row) && rule(ctx, row) : predicate;
1123
+ const matched = [...await reader.all(ctx)].map((row) => migrateRow(table, row)).filter(effective);
1124
+ if (reader.key !== undefined) {
1125
+ const key = reader.key;
1126
+ rangeDeps.push({
1127
+ table,
1128
+ predicate: effective,
1129
+ keys: new Set(matched.map(key))
1130
+ });
1131
+ } else {
1132
+ readTables.add(table);
1133
+ }
1134
+ return matched;
1135
+ }
1136
+ };
1137
+ };
1138
+ const writerFor = (table) => {
1139
+ const writer = writers.get(table);
1140
+ if (writer === undefined) {
1141
+ throw new Error(`No writer registered for table "${table}" \u2014 register one with engine.registerWriter, or use actions.change`);
1142
+ }
1143
+ return writer;
1144
+ };
1145
+ const readExisting = async (table, value, ctx) => {
1146
+ const reader = readers.get(table);
1147
+ if (reader?.get === undefined) {
1148
+ return;
1149
+ }
1150
+ const id = reader.key ? reader.key(value) : value.id;
1151
+ return id === undefined ? undefined : reader.get(id, ctx);
1152
+ };
1153
+ const authorizeWrite = async (table, op, value, ctx) => {
1154
+ const rule = writeRuleFor(table, op);
1155
+ if (rule === undefined) {
1156
+ return;
1157
+ }
1158
+ let subject = value;
1159
+ if (op !== "insert") {
1160
+ const existing = await readExisting(table, value, ctx);
1161
+ if (existing !== undefined) {
1162
+ subject = existing;
1163
+ }
1164
+ }
1165
+ if (!rule(ctx, subject)) {
1166
+ throw new UnauthorizedError(`${op} on table "${table}"`);
1167
+ }
1168
+ };
1169
+ const mergeCrdtFields = async (table, op, data, ctx) => {
1170
+ const fields = crdtFields.get(table);
1171
+ if (fields === undefined || data === null || typeof data !== "object") {
1172
+ return data;
1173
+ }
1174
+ const incoming = data;
1175
+ const existing = op === "update" ? await readExisting(table, data, ctx) : undefined;
1176
+ const base = existing !== null && typeof existing === "object" ? existing : undefined;
1177
+ const merged = { ...incoming };
1178
+ for (const [field, adapter] of Object.entries(fields)) {
1179
+ if (incoming[field] === undefined) {
1180
+ continue;
1181
+ }
1182
+ merged[field] = adapter.merge(base?.[field] ?? adapter.empty(), incoming[field]);
1183
+ }
1184
+ return merged;
1185
+ };
1186
+ const makeActions = (tx, ctx, enforce) => {
1187
+ const buffered = [];
1188
+ const actions = {
1189
+ change: (collection, change) => {
1190
+ buffered.push({
1191
+ table: collection,
1192
+ change
1193
+ });
1194
+ return Promise.resolve();
1195
+ },
1196
+ insert: async (table, data) => {
1197
+ validateWrite(table, "insert", data);
1198
+ if (enforce) {
1199
+ await authorizeWrite(table, "insert", data, ctx);
1200
+ }
1201
+ const merged = await mergeCrdtFields(table, "insert", data, ctx);
1202
+ const row = await writerFor(table).insert(merged, ctx, tx);
1203
+ buffered.push({ table, change: { op: "insert", row } });
1204
+ return row;
1205
+ },
1206
+ update: async (table, data) => {
1207
+ validateWrite(table, "update", data);
1208
+ if (enforce) {
1209
+ await authorizeWrite(table, "update", data, ctx);
1210
+ }
1211
+ const merged = await mergeCrdtFields(table, "update", data, ctx);
1212
+ const row = await writerFor(table).update(merged, ctx, tx);
1213
+ buffered.push({ table, change: { op: "update", row } });
1214
+ return row;
1215
+ },
1216
+ delete: async (table, row) => {
1217
+ if (enforce) {
1218
+ await authorizeWrite(table, "delete", row, ctx);
1219
+ }
1220
+ await writerFor(table).delete(row, ctx, tx);
1221
+ buffered.push({ table, change: { op: "delete", row } });
1222
+ }
1223
+ };
1224
+ return { actions, buffered };
1225
+ };
1226
+ const diffRerun = (sub, rows, equals = shallowEqual3) => {
1227
+ const next = new Map;
1228
+ for (const row of rows) {
1229
+ next.set(sub.key(row), row);
1230
+ }
1231
+ const added = [];
1232
+ const removed = [];
1233
+ const changed = [];
1234
+ for (const [rowKey, row] of next) {
1235
+ const previous = sub.current.get(rowKey);
1236
+ if (previous === undefined) {
1237
+ added.push(row);
1238
+ } else if (!equals(previous, row)) {
1239
+ changed.push(row);
1240
+ }
1241
+ }
1242
+ for (const [rowKey, row] of sub.current) {
1243
+ if (!next.has(rowKey)) {
1244
+ removed.push(row);
1245
+ }
1246
+ }
1247
+ sub.current = next;
1248
+ return { added, removed, changed };
1249
+ };
1250
+ const inRange = (dep, change) => dep.table === change.table && (change.key !== undefined && dep.keys.has(change.key) || dep.predicate(change.row));
1251
+ const readSetOverlaps = (readTables, readKeys, rangeDeps, changes) => changes.some((change) => readTables.has(change.table) || change.key !== undefined && readKeys.has(depKey(change.table, change.key)) || rangeDeps.some((dep) => inRange(dep, change)));
1252
+ const isReactiveAffected = (sub, changes) => readSetOverlaps(sub.readTables, sub.readKeys, sub.rangeDeps, changes);
1253
+ const invalidateCacheForChanges = (changes) => {
1254
+ if (cachedReruns.size === 0)
1255
+ return;
1256
+ for (const [key, entry] of cachedReruns) {
1257
+ if (readSetOverlaps(entry.readTables, entry.readKeys, entry.rangeDeps, changes)) {
1258
+ cachedReruns.delete(key);
1259
+ }
1260
+ }
1261
+ };
1262
+ const reactivePairs = async (changes) => {
1263
+ invalidateCacheForChanges(changes);
1264
+ const pairs = [];
1265
+ const sharedRuns = new Map;
1266
+ for (const sub of reactiveSubs) {
1267
+ if (!isReactiveAffected(sub, changes)) {
1268
+ continue;
1269
+ }
1270
+ let runPromise = sharedRuns.get(sub.rerunKey);
1271
+ if (runPromise === undefined) {
1272
+ runPromise = sub.rerun();
1273
+ sharedRuns.set(sub.rerunKey, runPromise);
1274
+ }
1275
+ const { rows, readTables, readKeys, rangeDeps } = await runPromise;
1276
+ sub.readTables = readTables;
1277
+ sub.readKeys = readKeys;
1278
+ sub.rangeDeps = rangeDeps;
1279
+ const diff = diffRerun(sub, rows);
1280
+ if (!isEmptyViewDiff(diff)) {
1281
+ pairs.push([sub, diff]);
1282
+ }
1283
+ }
1284
+ for (const [key, runPromise] of sharedRuns) {
1285
+ runPromise.then(({ rows, readTables, readKeys, rangeDeps }) => {
1286
+ writeCacheEntry({
1287
+ expiresAt: Date.now() + reactiveCacheTtlMs,
1288
+ rangeDeps,
1289
+ readKeys,
1290
+ readTables,
1291
+ rerunKey: key,
1292
+ rows,
1293
+ version
1294
+ });
1295
+ }).catch(() => {});
1296
+ }
1297
+ return pairs;
1298
+ };
1299
+ const ensureSearchIndex = async (definition) => {
1300
+ let entry = searchIndexes.get(definition.name);
1301
+ if (entry === undefined) {
1302
+ entry = { index: definition.index(), definition, hydrated: false };
1303
+ searchIndexes.set(definition.name, entry);
1304
+ }
1305
+ if (!entry.hydrated) {
1306
+ for (const row of await definition.source()) {
1307
+ entry.index.add(row);
1308
+ }
1309
+ entry.hydrated = true;
1310
+ }
1311
+ return entry;
1312
+ };
1313
+ const searchPairs = (changes) => {
1314
+ const touched = new Set;
1315
+ for (const { table, change } of changes) {
1316
+ for (const entry of searchIndexes.values()) {
1317
+ if (!entry.hydrated || entry.definition.table !== table) {
1318
+ continue;
1319
+ }
1320
+ if (change.op === "delete") {
1321
+ entry.index.remove(entry.definition.key(change.row));
1322
+ } else {
1323
+ entry.index.add(change.row);
1324
+ }
1325
+ touched.add(entry.definition.name);
1326
+ }
1327
+ }
1328
+ const pairs = [];
1329
+ for (const sub of searchSubs) {
1330
+ if (!touched.has(sub.collection)) {
1331
+ continue;
1332
+ }
1333
+ const diff = diffRerun(sub, sub.rerun(), equalsIgnoringScore);
1334
+ if (!isEmptyViewDiff(diff)) {
1335
+ pairs.push([sub, diff]);
1336
+ }
1337
+ }
1338
+ return pairs;
1339
+ };
1340
+ const logChange = (changeVersion, entry) => {
1341
+ changeLog.push(entry);
1342
+ if (changeLog.length > changeLogSize) {
1343
+ changeLog.shift();
1344
+ }
1345
+ for (const subscriber of streamSubscribers) {
1346
+ subscriber(entry);
1347
+ }
1348
+ };
1349
+ const applyChange = async (table, change, shouldBroadcast = true) => {
1350
+ version += 1;
1351
+ const changeVersion = version;
1352
+ logChange(changeVersion, { version: changeVersion, table, change });
1353
+ emitActivity({
1354
+ type: "change",
1355
+ at: Date.now(),
1356
+ table,
1357
+ op: change.op,
1358
+ version: changeVersion
1359
+ });
1360
+ const emissions = [];
1361
+ for (const subscription of subscriptionsForTable(table)) {
1362
+ const diff = await subscriptionDiff(subscription, table, change);
1363
+ if (!isEmptyViewDiff(diff)) {
1364
+ emissions.push([subscription, diff]);
1365
+ }
1366
+ }
1367
+ emissions.push(...await reactivePairs([
1368
+ { table, key: changedKeyFor(table, change), row: change.row }
1369
+ ]));
1370
+ emissions.push(...searchPairs([{ table, change }]));
1371
+ for (const [subscription, diff] of emissions) {
1372
+ subscription.onDiff(diff, changeVersion);
1373
+ }
1374
+ if (shouldBroadcast) {
1375
+ broadcast([{ table, change }]);
1376
+ }
1377
+ };
1378
+ const applyChangeBatch = async (changes, shouldBroadcast = true) => {
1379
+ if (changes.length === 0) {
1380
+ return;
1381
+ }
1382
+ version += 1;
1383
+ const batchVersion = version;
1384
+ const perSubscription = new Map;
1385
+ const reactiveChanges = [];
1386
+ for (const { table, change } of changes) {
1387
+ logChange(batchVersion, { version: batchVersion, table, change });
1388
+ emitActivity({
1389
+ type: "change",
1390
+ at: Date.now(),
1391
+ table,
1392
+ op: change.op,
1393
+ version: batchVersion
1394
+ });
1395
+ reactiveChanges.push({
1396
+ table,
1397
+ key: changedKeyFor(table, change),
1398
+ row: change.row
1399
+ });
1400
+ for (const subscription of subscriptionsForTable(table)) {
1401
+ const diff = await subscriptionDiff(subscription, table, change);
1402
+ const list = perSubscription.get(subscription);
1403
+ if (list === undefined) {
1404
+ perSubscription.set(subscription, [diff]);
1405
+ } else {
1406
+ list.push(diff);
1407
+ }
1408
+ }
1409
+ }
1410
+ const emissions = [];
1411
+ for (const [subscription, diffs] of perSubscription) {
1412
+ const merged = diffs.length === 1 ? diffs[0] : mergeViewDiffs(diffs, subscription.key);
1413
+ if (!isEmptyViewDiff(merged)) {
1414
+ emissions.push([subscription, merged]);
1415
+ }
1416
+ }
1417
+ emissions.push(...await reactivePairs(reactiveChanges));
1418
+ emissions.push(...searchPairs(changes));
1419
+ for (const [subscription, diff] of emissions) {
1420
+ subscription.onDiff(diff, batchVersion);
1421
+ }
1422
+ if (shouldBroadcast) {
1423
+ broadcast(changes);
1424
+ }
1425
+ };
1426
+ const canResume = (since, incremental) => {
1427
+ if (!incremental) {
1428
+ return false;
1429
+ }
1430
+ if (since >= version) {
1431
+ return true;
1432
+ }
1433
+ const oldest = changeLog[0];
1434
+ return oldest !== undefined && oldest.version <= since + 1;
1435
+ };
1436
+ const buildCatchup = (since, tables, key, match) => {
1437
+ const latest = new Map;
1438
+ for (const entry of changeLog) {
1439
+ if (entry.version <= since || !tables.includes(entry.table)) {
1440
+ continue;
1441
+ }
1442
+ const row = entry.change.row;
1443
+ const present = entry.change.op !== "delete" && match(row) ? "upsert" : "remove";
1444
+ latest.set(key(row), { op: present, row });
1445
+ }
1446
+ const changed = [];
1447
+ const removed = [];
1448
+ for (const { op, row } of latest.values()) {
1449
+ (op === "upsert" ? changed : removed).push(row);
1450
+ }
1451
+ return { added: [], removed, changed };
1452
+ };
1453
+ const subscribeJoin = async (collection, definition, params, ctx, onDiff, set) => {
1454
+ if (definition.authorize !== undefined) {
1455
+ const allowed = await definition.authorize(params, ctx);
1456
+ if (!allowed) {
1457
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1458
+ }
1459
+ }
1460
+ const { left, right } = definition;
1461
+ const op = createEquiJoin({
1462
+ leftKey: left.key,
1463
+ rightKey: right.key,
1464
+ leftOn: left.on,
1465
+ rightOn: right.on,
1466
+ select: definition.select
1467
+ });
1468
+ op.hydrate([...await left.hydrate(params, ctx)], [...await right.hydrate(params, ctx)]);
1469
+ const atVersion = version;
1470
+ const subscription = {
1471
+ kind: "join",
1472
+ collection,
1473
+ join: {
1474
+ op,
1475
+ leftTable: left.table,
1476
+ rightTable: right.table,
1477
+ leftMatch: left.match ? (row) => left.match(row, params, ctx) : undefined,
1478
+ rightMatch: right.match ? (row) => right.match(row, params, ctx) : undefined
1479
+ },
1480
+ key: definition.key,
1481
+ onDiff
1482
+ };
1483
+ set.add(subscription);
1484
+ return {
1485
+ initial: op.rows(),
1486
+ version: atVersion,
1487
+ unsubscribe: () => {
1488
+ set.delete(subscription);
1489
+ }
1490
+ };
1491
+ };
1492
+ const subscribeGraph = async (collection, definition, params, ctx, onDiff, set) => {
1493
+ if (definition.authorize !== undefined) {
1494
+ const allowed = await definition.authorize(params, ctx);
1495
+ if (!allowed) {
1496
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1497
+ }
1498
+ }
1499
+ const instance = definition.query.instantiate(params, ctx);
1500
+ const initial = await instance.hydrate();
1501
+ const atVersion = version;
1502
+ const subscription = {
1503
+ kind: "graph",
1504
+ collection,
1505
+ instance,
1506
+ key: definition.key,
1507
+ onDiff
1508
+ };
1509
+ set.add(subscription);
1510
+ return {
1511
+ initial,
1512
+ version: atVersion,
1513
+ unsubscribe: () => {
1514
+ set.delete(subscription);
1515
+ }
1516
+ };
1517
+ };
1518
+ const subscribeReactive = async (collection, definition, params, ctx, onDiff, set) => {
1519
+ if (definition.authorize !== undefined) {
1520
+ const allowed = await definition.authorize(params, ctx);
1521
+ if (!allowed) {
1522
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1523
+ }
1524
+ }
1525
+ const rerun = async () => {
1526
+ const readTables = new Set;
1527
+ const readKeys = new Set;
1528
+ const rangeDeps = [];
1529
+ const db = makeReadHandle(ctx, readTables, readKeys, rangeDeps);
1530
+ const rows = [...await definition.run({ ctx, db, params })];
1531
+ return { rangeDeps, readKeys, readTables, rows };
1532
+ };
1533
+ const rerunKey = stableSubKey(collection, params, ctx);
1534
+ const cached = readCacheEntry(rerunKey);
1535
+ const first = cached !== undefined ? {
1536
+ rangeDeps: cached.rangeDeps,
1537
+ readKeys: cached.readKeys,
1538
+ readTables: cached.readTables,
1539
+ rows: cached.rows
1540
+ } : await rerun();
1541
+ if (cached === undefined) {
1542
+ writeCacheEntry({
1543
+ expiresAt: Date.now() + reactiveCacheTtlMs,
1544
+ rangeDeps: first.rangeDeps,
1545
+ readKeys: first.readKeys,
1546
+ readTables: first.readTables,
1547
+ rerunKey,
1548
+ rows: first.rows,
1549
+ version
1550
+ });
1551
+ }
1552
+ const current = new Map;
1553
+ for (const row of first.rows) {
1554
+ current.set(definition.key(row), row);
1555
+ }
1556
+ const atVersion = version;
1557
+ const subscription = {
1558
+ kind: "reactive",
1559
+ collection,
1560
+ key: definition.key,
1561
+ rerun,
1562
+ rerunKey,
1563
+ current,
1564
+ readTables: first.readTables,
1565
+ readKeys: first.readKeys,
1566
+ rangeDeps: first.rangeDeps,
1567
+ onDiff
1568
+ };
1569
+ set.add(subscription);
1570
+ reactiveSubs.add(subscription);
1571
+ return {
1572
+ initial: first.rows,
1573
+ version: atVersion,
1574
+ unsubscribe: () => {
1575
+ set.delete(subscription);
1576
+ reactiveSubs.delete(subscription);
1577
+ }
1578
+ };
1579
+ };
1580
+ const subscribeSearch = async (collection, definition, params, ctx, onDiff, set) => {
1581
+ const query = params;
1582
+ if (definition.authorize !== undefined) {
1583
+ const allowed = await definition.authorize(query, ctx);
1584
+ if (!allowed) {
1585
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1586
+ }
1587
+ }
1588
+ const entry = await ensureSearchIndex(definition);
1589
+ const limit = definition.limit ?? 20;
1590
+ const readRule = readRuleFor(definition.table);
1591
+ const rerun = () => {
1592
+ const candidates = entry.index.search(query, readRule ? limit * 5 : limit);
1593
+ const visible = readRule ? candidates.filter((hit) => readRule(ctx, hit.row)) : candidates;
1594
+ return visible.slice(0, limit).map((hit) => ({
1595
+ ...hit.row,
1596
+ [SEARCH_SCORE_FIELD]: hit.score
1597
+ }));
1598
+ };
1599
+ const initial = rerun();
1600
+ const current = new Map;
1601
+ for (const row of initial) {
1602
+ current.set(definition.key(row), row);
1603
+ }
1604
+ const atVersion = version;
1605
+ const subscription = {
1606
+ kind: "search",
1607
+ collection,
1608
+ key: definition.key,
1609
+ rerun,
1610
+ current,
1611
+ onDiff
1612
+ };
1613
+ set.add(subscription);
1614
+ searchSubs.add(subscription);
1615
+ return {
1616
+ initial,
1617
+ version: atVersion,
1618
+ unsubscribe: () => {
1619
+ set.delete(subscription);
1620
+ searchSubs.delete(subscription);
1621
+ }
1622
+ };
1623
+ };
1624
+ return {
1625
+ register: (collection) => {
1626
+ registry.set(collection.name, collection);
1627
+ for (const table of collection.tables ?? [collection.name]) {
1628
+ addTableIndex(table, collection.name);
1629
+ }
1630
+ },
1631
+ registerJoin: (collection) => {
1632
+ registry.set(collection.name, collection);
1633
+ addTableIndex(collection.left.table, collection.name);
1634
+ addTableIndex(collection.right.table, collection.name);
1635
+ },
1636
+ registerGraph: (collection) => {
1637
+ registry.set(collection.name, collection);
1638
+ for (const table of collection.query.tables()) {
1639
+ addTableIndex(table, collection.name);
1640
+ }
1641
+ },
1642
+ registerSearch: (collection) => {
1643
+ registry.set(collection.name, collection);
1644
+ },
1645
+ subscribe: async ({ collection, params, ctx, onDiff, since }) => {
1646
+ const registered = registry.get(collection);
1647
+ if (registered === undefined) {
1648
+ throw new Error(`Unknown collection "${collection}"`);
1649
+ }
1650
+ const typedOnDiff = onDiff;
1651
+ const subscribeSet = subsFor(collection);
1652
+ const registeredKind = registered.kind;
1653
+ if (registeredKind === "join") {
1654
+ const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1655
+ return joined;
1656
+ }
1657
+ if (registeredKind === "graph") {
1658
+ const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1659
+ return graphed;
1660
+ }
1661
+ if (registeredKind === "reactive") {
1662
+ const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1663
+ return reactived;
1664
+ }
1665
+ if (registeredKind === "search") {
1666
+ const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1667
+ return searched;
1668
+ }
1669
+ const definition = registered;
1670
+ if (definition.authorize !== undefined) {
1671
+ const allowed = await definition.authorize(params, ctx);
1672
+ if (!allowed) {
1673
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1674
+ }
1675
+ }
1676
+ const key = definition.key ?? defaultKey;
1677
+ const match = definition.match;
1678
+ const tables = definition.tables ?? [collection];
1679
+ const scopedTable = tables.length === 1 ? tables[0] : undefined;
1680
+ const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1681
+ const rehydrate = async () => {
1682
+ const raw = [...await definition.hydrate(params, ctx)];
1683
+ const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1684
+ return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1685
+ };
1686
+ const incremental = match !== undefined && tables.length === 1;
1687
+ const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
1688
+ const view = createMaterializedView({
1689
+ key,
1690
+ match: boundMatch
1691
+ });
1692
+ const resuming = since !== undefined && canResume(since, incremental);
1693
+ view.hydrate([...await rehydrate()]);
1694
+ const atVersion = version;
1695
+ const subscription = {
1696
+ kind: "view",
1697
+ collection,
1698
+ view,
1699
+ incremental,
1700
+ rehydrate,
1701
+ key,
1702
+ onDiff: typedOnDiff
1703
+ };
1704
+ subscribeSet.add(subscription);
1705
+ const unsubscribe = () => {
1706
+ subscribeSet.delete(subscription);
1707
+ };
1708
+ if (resuming) {
1709
+ return {
1710
+ initial: [],
1711
+ catchup: buildCatchup(since, tables, key, boundMatch),
1712
+ version: atVersion,
1713
+ unsubscribe
1714
+ };
1715
+ }
1716
+ return {
1717
+ initial: view.rows(),
1718
+ version: atVersion,
1719
+ unsubscribe
1720
+ };
1721
+ },
1722
+ hydrate: async (collection, params, ctx) => {
1723
+ const definition = registry.get(collection);
1724
+ if (definition === undefined) {
1725
+ throw new Error(`Unknown collection "${collection}"`);
1726
+ }
1727
+ if (definition.authorize !== undefined) {
1728
+ const allowed = await definition.authorize(params, ctx);
1729
+ if (!allowed) {
1730
+ throw new UnauthorizedError(`hydrate collection "${collection}"`);
1731
+ }
1732
+ }
1733
+ const raw = [...await definition.hydrate(params, ctx)];
1734
+ const tables = definition.tables ?? [collection];
1735
+ const scopedTable = tables.length === 1 ? tables[0] : undefined;
1736
+ const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1737
+ const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1738
+ return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1739
+ },
1740
+ applyChange: (table, change) => applyChange(table, change),
1741
+ connectSource: async (source) => {
1742
+ await source.start((table, change) => applyChange(table, change));
1743
+ return async () => {
1744
+ await source.stop();
1745
+ };
1746
+ },
1747
+ connectCluster: async (bus) => {
1748
+ const unsubscribe = await bus.subscribe((message) => {
1749
+ if (message.origin === instanceId) {
1750
+ return;
1751
+ }
1752
+ applyChangeBatch(message.changes, false);
1753
+ });
1754
+ clusterBus = bus;
1755
+ return async () => {
1756
+ clusterBus = undefined;
1757
+ await unsubscribe();
1758
+ };
1759
+ },
1760
+ subscriptionCount: (collection) => {
1761
+ if (collection !== undefined) {
1762
+ return active.get(collection)?.size ?? 0;
1763
+ }
1764
+ let total = 0;
1765
+ for (const set of active.values()) {
1766
+ total += set.size;
1767
+ }
1768
+ return total;
1769
+ },
1770
+ registerMutation: (mutation) => {
1771
+ if (mutation.handler === undefined && mutation.sandboxedHandler === undefined) {
1772
+ throw new Error(`Mutation "${mutation.name}" must define either \`handler\` or \`sandboxedHandler\``);
1773
+ }
1774
+ if (mutation.handler !== undefined && mutation.sandboxedHandler !== undefined) {
1775
+ throw new Error(`Mutation "${mutation.name}" defines both \`handler\` and \`sandboxedHandler\` \u2014 pick one`);
1776
+ }
1777
+ mutations.set(mutation.name, mutation);
1778
+ if (mutation.sandboxedHandler !== undefined) {
1779
+ sandboxRunners.set(mutation.name, makeSandboxedHandler(mutation.sandboxedHandler, mutation.sandbox));
1780
+ }
1781
+ },
1782
+ registerWriter: (table, writer) => {
1783
+ writers.set(table, writer);
1784
+ },
1785
+ registerReactive: (query) => {
1786
+ registry.set(query.name, query);
1787
+ },
1788
+ registerReader: (table, reader) => {
1789
+ readers.set(table, reader);
1790
+ },
1791
+ registerPermissions: (table, rules) => {
1792
+ permissions.set(table, rules);
1793
+ },
1794
+ registerSchema: (table, schema) => {
1795
+ schemas.set(table, schema);
1796
+ },
1797
+ registerCrdt: (table, fields) => {
1798
+ crdtFields.set(table, fields);
1799
+ const name = `${table}:merge`;
1800
+ mutations.set(name, {
1801
+ handler: async (args, ctx, actions) => {
1802
+ const existing = await readExisting(table, args, ctx);
1803
+ return existing === undefined ? actions.insert(table, args) : actions.update(table, args);
1804
+ },
1805
+ name
1806
+ });
1807
+ },
1808
+ migrate: (table, row) => migrateRow(table, row),
1809
+ runMutation: async (name, args, ctx) => {
1810
+ const mutation = mutations.get(name);
1811
+ if (mutation === undefined) {
1812
+ throw new Error(`Unknown mutation "${name}"`);
1813
+ }
1814
+ if (mutation.authorize !== undefined) {
1815
+ const allowed = await mutation.authorize(args, ctx);
1816
+ if (!allowed) {
1817
+ throw new UnauthorizedError(`run mutation "${name}"`);
1818
+ }
1819
+ }
1820
+ const sandboxRunner = sandboxRunners.get(name);
1821
+ const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
1822
+ const runHandler = async (tx) => {
1823
+ const { actions, buffered } = makeActions(tx, ctx, true);
1824
+ const result = await invokeHandler(args, ctx, actions);
1825
+ return { buffered, result };
1826
+ };
1827
+ const retry = mutation.retry;
1828
+ const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
1829
+ const isRetryable = retry?.isRetryable ?? isSerializationFailure;
1830
+ const computeDelay = retry?.backoff ?? exponentialBackoff();
1831
+ const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
1832
+ const startedAt = Date.now();
1833
+ let lastError;
1834
+ let attemptsMade = 0;
1835
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
1836
+ attemptsMade = attempt;
1837
+ try {
1838
+ const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1839
+ await applyChangeBatch(buffered);
1840
+ emitActivity({
1841
+ type: "mutation",
1842
+ at: Date.now(),
1843
+ name,
1844
+ status: "ok"
1845
+ });
1846
+ return result;
1847
+ } catch (error) {
1848
+ lastError = error;
1849
+ const elapsedMs = Date.now() - startedAt;
1850
+ const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
1851
+ if (!canRetry)
1852
+ break;
1853
+ const rawDelay = computeDelay(attempt);
1854
+ const remaining = maxElapsedMs - elapsedMs;
1855
+ if (remaining <= 0)
1856
+ break;
1857
+ const delayMs = Math.max(0, Math.min(rawDelay, remaining));
1858
+ emitActivity({
1859
+ type: "mutationRetry",
1860
+ at: Date.now(),
1861
+ name,
1862
+ attempt,
1863
+ delayMs,
1864
+ errorName: error instanceof Error ? error.name : "Error",
1865
+ errorMessage: error instanceof Error ? error.message : String(error)
1866
+ });
1867
+ if (delayMs > 0) {
1868
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1869
+ }
1870
+ }
1871
+ }
1872
+ emitActivity({
1873
+ type: "mutation",
1874
+ at: Date.now(),
1875
+ name,
1876
+ status: "error"
1877
+ });
1878
+ if (attemptsMade > 1) {
1879
+ throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
1880
+ }
1881
+ throw lastError;
1882
+ },
1883
+ registerSchedule: (schedule) => {
1884
+ schedules.set(schedule.name, schedule);
1885
+ },
1886
+ listSchedules: () => [...schedules.values()],
1887
+ runSchedule: async (name) => {
1888
+ const schedule = schedules.get(name);
1889
+ if (schedule === undefined) {
1890
+ throw new Error(`Unknown schedule "${name}"`);
1891
+ }
1892
+ const runHandler = async (tx) => {
1893
+ const { actions, buffered: buffered2 } = makeActions(tx, {}, false);
1894
+ const db = makeReadHandle({}, new Set, new Set, [], false);
1895
+ await schedule.run({ actions, db });
1896
+ return buffered2;
1897
+ };
1898
+ const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1899
+ await applyChangeBatch(buffered);
1900
+ },
1901
+ inspect: () => {
1902
+ const collections = [...registry.entries()].map(([name, def]) => {
1903
+ const kind = def.kind ?? "view";
1904
+ let tables = [];
1905
+ if (kind === "join") {
1906
+ const join = def;
1907
+ tables = [join.left.table, join.right.table];
1908
+ } else if (kind === "graph") {
1909
+ tables = def.query.tables();
1910
+ } else if (kind === "search") {
1911
+ tables = [
1912
+ def.table
1913
+ ];
1914
+ } else if (kind === "view") {
1915
+ tables = def.tables ?? [name];
1916
+ }
1917
+ return {
1918
+ name,
1919
+ kind,
1920
+ tables,
1921
+ subscriptions: active.get(name)?.size ?? 0
1922
+ };
1923
+ });
1924
+ const DEVTOOLS_RECENT = 50;
1925
+ return {
1926
+ version,
1927
+ collections,
1928
+ mutations: [...mutations.keys()],
1929
+ schedules: [...schedules.values()].map((schedule) => ({
1930
+ name: schedule.name,
1931
+ pattern: schedule.pattern
1932
+ })),
1933
+ readers: [...readers.keys()],
1934
+ writers: [...writers.keys()],
1935
+ recentChanges: changeLog.slice(-DEVTOOLS_RECENT).map((entry) => ({
1936
+ version: entry.version,
1937
+ table: entry.table,
1938
+ op: entry.change.op
1939
+ }))
1940
+ };
1941
+ },
1942
+ onActivity: (listener) => {
1943
+ activityListeners.add(listener);
1944
+ return () => {
1945
+ activityListeners.delete(listener);
1946
+ };
1947
+ },
1948
+ streamChanges: ({
1949
+ since = 0,
1950
+ signal,
1951
+ maxBuffer = 1e4
1952
+ } = {}) => {
1953
+ const oldest = changeLog[0];
1954
+ if (since > 0 && oldest !== undefined && oldest.version > since + 1) {
1955
+ const err = new MissedChangesError(since, oldest.version);
1956
+ return {
1957
+ [Symbol.asyncIterator]() {
1958
+ return {
1959
+ next: () => Promise.reject(err)
1960
+ };
1961
+ }
1962
+ };
1963
+ }
1964
+ const buffer = [];
1965
+ let waiter = null;
1966
+ let overflow = false;
1967
+ const wake = () => {
1968
+ if (waiter !== null) {
1969
+ const resume = waiter;
1970
+ waiter = null;
1971
+ resume();
1972
+ }
1973
+ };
1974
+ const subscriber = (entry) => {
1975
+ if (buffer.length >= maxBuffer) {
1976
+ overflow = true;
1977
+ wake();
1978
+ return;
1979
+ }
1980
+ buffer.push(entry);
1981
+ wake();
1982
+ };
1983
+ streamSubscribers.add(subscriber);
1984
+ const onAbort = () => wake();
1985
+ signal?.addEventListener("abort", onAbort, { once: true });
1986
+ let lastDelivered = since;
1987
+ return {
1988
+ async* [Symbol.asyncIterator]() {
1989
+ try {
1990
+ const history = [...changeLog];
1991
+ const headVersion = history.length > 0 ? history[history.length - 1].version : since;
1992
+ for (const entry of history) {
1993
+ if (signal?.aborted)
1994
+ return;
1995
+ if (entry.version > since) {
1996
+ lastDelivered = entry.version;
1997
+ yield entry;
1998
+ }
1999
+ }
2000
+ while (!signal?.aborted) {
2001
+ while (buffer.length > 0) {
2002
+ const entry = buffer.shift();
2003
+ if (entry.version > headVersion) {
2004
+ lastDelivered = entry.version;
2005
+ yield entry;
2006
+ }
2007
+ }
2008
+ if (overflow) {
2009
+ throw new CdcConsumerSlowError(maxBuffer, lastDelivered);
2010
+ }
2011
+ if (signal?.aborted)
2012
+ return;
2013
+ await new Promise((resolve) => {
2014
+ waiter = resolve;
2015
+ });
2016
+ }
2017
+ } finally {
2018
+ streamSubscribers.delete(subscriber);
2019
+ signal?.removeEventListener("abort", onAbort);
2020
+ }
2021
+ }
2022
+ };
2023
+ }
2024
+ };
2025
+ };
2026
+
2027
+ // src/engine/cdc.ts
2028
+ var parseSince = (query, lastEventId) => {
2029
+ const raw = query.since ?? lastEventId ?? "0";
2030
+ const parsed = Number(raw);
2031
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 0;
2032
+ };
2033
+ var encodeEvent = (event, id, data) => {
2034
+ const parts = [];
2035
+ if (id !== null)
2036
+ parts.push(`id: ${id}`);
2037
+ parts.push(`event: ${event}`);
2038
+ parts.push(`data: ${JSON.stringify(data)}`);
2039
+ return `${parts.join(`
2040
+ `)}
2041
+
2042
+ `;
2043
+ };
2044
+ var syncCdc = ({
2045
+ engine,
2046
+ path = "/sync/cdc",
2047
+ heartbeatMs = 25000,
2048
+ maxBuffer = 1e4
2049
+ }) => new Elysia3({ name: "@absolutejs/sync/cdc" }).get(path, (context) => {
2050
+ const lastEventId = context.request.headers.get("last-event-id");
2051
+ const since = parseSince(context.query, lastEventId);
2052
+ const encoder = new TextEncoder;
2053
+ const stream = new ReadableStream({
2054
+ async start(controller) {
2055
+ const write = (chunk) => {
2056
+ try {
2057
+ controller.enqueue(encoder.encode(chunk));
2058
+ } catch {}
2059
+ };
2060
+ write(encodeEvent("open", null, {
2061
+ since,
2062
+ at: Date.now()
2063
+ }));
2064
+ const heartbeat = setInterval(() => write(`: ping
2065
+
2066
+ `), heartbeatMs);
2067
+ try {
2068
+ for await (const entry of engine.streamChanges({
2069
+ since,
2070
+ signal: context.request.signal,
2071
+ maxBuffer
2072
+ })) {
2073
+ write(encodeEvent("change", entry.version, entry));
2074
+ }
2075
+ } catch (error) {
2076
+ if (error instanceof MissedChangesError) {
2077
+ write(encodeEvent("error", null, {
2078
+ name: "MissedChangesError",
2079
+ message: error.message,
2080
+ requestedSince: error.requestedSince,
2081
+ availableSince: error.availableSince
2082
+ }));
2083
+ } else if (error instanceof CdcConsumerSlowError) {
2084
+ write(encodeEvent("error", null, {
2085
+ name: "CdcConsumerSlowError",
2086
+ message: error.message,
2087
+ lastDeliveredVersion: error.lastDeliveredVersion
2088
+ }));
2089
+ } else {
2090
+ write(encodeEvent("error", null, {
2091
+ name: error instanceof Error ? error.name : "Error",
2092
+ message: error instanceof Error ? error.message : String(error)
2093
+ }));
2094
+ }
2095
+ } finally {
2096
+ clearInterval(heartbeat);
2097
+ try {
2098
+ controller.close();
2099
+ } catch {}
2100
+ }
2101
+ }
2102
+ });
2103
+ return new Response(stream, {
2104
+ headers: {
2105
+ "cache-control": "no-cache, no-transform",
2106
+ connection: "keep-alive",
2107
+ "content-type": "text/event-stream"
2108
+ }
2109
+ });
2110
+ });
2111
+ // src/devtools.ts
2112
+ import { Elysia as Elysia4 } from "elysia";
428
2113
  var dashboardHtml = (streamPath) => `<!doctype html>
429
2114
  <html lang="en"><head><meta charset="utf-8" />
430
2115
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -494,7 +2179,7 @@ var syncDevtools = ({
494
2179
  snapshotMs = 2000
495
2180
  }) => {
496
2181
  const streamPath = `${path}/stream`;
497
- return new Elysia3({ name: "@absolutejs/sync/devtools" }).get(path, () => new Response(dashboardHtml(streamPath), {
2182
+ return new Elysia4({ name: "@absolutejs/sync/devtools" }).get(path, () => new Response(dashboardHtml(streamPath), {
498
2183
  headers: { "content-type": "text/html; charset=utf-8" }
499
2184
  })).get(streamPath, (context) => {
500
2185
  const encoder = new TextEncoder;
@@ -608,11 +2293,12 @@ var createPresenceHub = () => {
608
2293
  export {
609
2294
  syncSocket,
610
2295
  syncDevtools,
2296
+ syncCdc,
611
2297
  sync,
612
2298
  createWriteBehindCache,
613
2299
  createReactiveHub,
614
2300
  createPresenceHub
615
2301
  };
616
2302
 
617
- //# debugId=03518D3B105D1D6864756E2164756E21
2303
+ //# debugId=50A2A8778F250B8064756E2164756E21
618
2304
  //# sourceMappingURL=index.js.map