@super-line/server 0.10.1 → 0.11.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/README.md +1 -1
- package/dist/index.cjs +289 -153
- package/dist/index.d.cts +126 -57
- package/dist/index.d.ts +126 -57
- package/dist/index.js +295 -154
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ replica.delete('title') // surgical key delete (the only way to remove
|
|
|
76
76
|
replica.close()
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
`open` requires a store with reactive support (throws `UNSUPPORTED` otherwise). CRDT stores (`store-sync`, `store-sync-
|
|
79
|
+
`open` requires a store with reactive support (throws `UNSUPPORTED` otherwise). CRDT stores (`store-sync`, `store-sync-pglite`) and the LWW stores both back it.
|
|
80
80
|
|
|
81
81
|
### Control Center inspector
|
|
82
82
|
|
package/dist/index.cjs
CHANGED
|
@@ -205,27 +205,43 @@ var CONN = "c:";
|
|
|
205
205
|
var USER = "u:";
|
|
206
206
|
var REPLY = "reply:";
|
|
207
207
|
var PLUGIN = "x:";
|
|
208
|
-
var
|
|
208
|
+
var CDOC = "d:";
|
|
209
209
|
var SERVER_ORIGIN = "server";
|
|
210
210
|
function createSuperLineServer(contract, opts) {
|
|
211
211
|
const c = contract;
|
|
212
212
|
const serializer = opts.serializer ?? import_core.jsonSerializer;
|
|
213
213
|
const adapter = opts.adapter ?? createInMemoryAdapter();
|
|
214
|
-
const storeMap = opts.stores ?? {};
|
|
215
214
|
const plugins = opts.plugins ?? [];
|
|
216
215
|
const pluginNames = /* @__PURE__ */ new Set();
|
|
217
216
|
for (const p of plugins) {
|
|
218
217
|
if (pluginNames.has(p.name)) throw new Error(`Duplicate plugin name: ${p.name}`);
|
|
219
218
|
pluginNames.add(p.name);
|
|
220
219
|
}
|
|
220
|
+
const collectionStore = opts.collections;
|
|
221
|
+
const collectionDefs = c.collections ?? {};
|
|
222
|
+
const collectionPolicies = { ...opts.policies };
|
|
221
223
|
for (const p of plugins) {
|
|
222
|
-
if (!p.
|
|
223
|
-
for (const [name,
|
|
224
|
-
if (name in
|
|
225
|
-
throw new Error(
|
|
226
|
-
|
|
224
|
+
if (!p.policies) continue;
|
|
225
|
+
for (const [name, policy] of Object.entries(p.policies)) {
|
|
226
|
+
if (!(name in collectionDefs))
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Plugin '${p.name}' policy references unknown collection '${name}' \u2014 register its contract fragment on defineContract({ plugins })`
|
|
229
|
+
);
|
|
230
|
+
if (name in collectionPolicies)
|
|
231
|
+
throw new Error(`Plugin '${p.name}' policy for collection '${name}' collides with an existing policy`);
|
|
232
|
+
collectionPolicies[name] = policy;
|
|
227
233
|
}
|
|
228
234
|
}
|
|
235
|
+
const checkReferences = opts.checkReferences ?? false;
|
|
236
|
+
const collIsRelay = collectionStore?.clustering === "relay";
|
|
237
|
+
const crdtStore = opts.crdtCollections;
|
|
238
|
+
const crdtDefOf = (n) => {
|
|
239
|
+
const d = collectionDefs[n];
|
|
240
|
+
return d && (0, import_core.isCrdtCollection)(d) ? d : void 0;
|
|
241
|
+
};
|
|
242
|
+
const COLL_CHANNEL = "cbatch";
|
|
243
|
+
const collSubscribers = /* @__PURE__ */ new Map();
|
|
244
|
+
const connColl = /* @__PURE__ */ new Map();
|
|
229
245
|
const reserved = [];
|
|
230
246
|
for (const p of plugins) {
|
|
231
247
|
if (!p.connection) continue;
|
|
@@ -319,8 +335,12 @@ function createSuperLineServer(contract, opts) {
|
|
|
319
335
|
handlePersonal(channel, payload);
|
|
320
336
|
return;
|
|
321
337
|
}
|
|
322
|
-
if (channel.startsWith(
|
|
323
|
-
|
|
338
|
+
if (channel.startsWith(CDOC)) {
|
|
339
|
+
handleCrdtRelay(channel, payload);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (channel === COLL_CHANNEL) {
|
|
343
|
+
handleCollectionRelay(payload);
|
|
324
344
|
return;
|
|
325
345
|
}
|
|
326
346
|
if (channel.startsWith(PLUGIN)) {
|
|
@@ -333,6 +353,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
333
353
|
if (busSet) deliverBus(payload, busSet);
|
|
334
354
|
});
|
|
335
355
|
void adapter.subscribe(replyChannel);
|
|
356
|
+
if (collectionStore && collIsRelay) void adapter.subscribe(COLL_CHANNEL);
|
|
336
357
|
function handlePersonal(channel, payload) {
|
|
337
358
|
const set = members.get(channel);
|
|
338
359
|
if (!set) return;
|
|
@@ -566,6 +587,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
566
587
|
raw.onClose((code) => {
|
|
567
588
|
conns.delete(conn);
|
|
568
589
|
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
590
|
+
collUnsubAll(conn);
|
|
569
591
|
void adapter.presence?.del(conn.id);
|
|
570
592
|
const goneUserId = opts.identify?.(conn);
|
|
571
593
|
emitTap({
|
|
@@ -600,10 +622,12 @@ function createSuperLineServer(contract, opts) {
|
|
|
600
622
|
else if (frame.t === "unsub") {
|
|
601
623
|
const ns = topicNamespace(conn.role, frame.c);
|
|
602
624
|
if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
|
|
603
|
-
} else if (frame.t === "
|
|
604
|
-
else if (frame.t === "
|
|
605
|
-
else if (frame.t === "
|
|
606
|
-
else if (frame.t === "
|
|
625
|
+
} else if (frame.t === "csub") await handleCollectionSub(conn, frame);
|
|
626
|
+
else if (frame.t === "cuns") handleCollectionUnsub(conn, frame);
|
|
627
|
+
else if (frame.t === "cbat") await handleCollectionBatch(conn, frame);
|
|
628
|
+
else if (frame.t === "cdopen") await handleCrdtOpen(conn, frame);
|
|
629
|
+
else if (frame.t === "cdwr") await handleCrdtWrite(conn, frame);
|
|
630
|
+
else if (frame.t === "cdclose") handleCrdtClose(conn, frame);
|
|
607
631
|
else if (frame.t === "sres") {
|
|
608
632
|
await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
|
|
609
633
|
} else if (frame.t === "serr") {
|
|
@@ -696,90 +720,77 @@ function createSuperLineServer(contract, opts) {
|
|
|
696
720
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
697
721
|
});
|
|
698
722
|
}
|
|
699
|
-
function
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
return
|
|
703
|
-
}
|
|
704
|
-
async function
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
await dispatchOp(conn, frame.i, { kind: "subscribe", name: `
|
|
708
|
-
const
|
|
709
|
-
if (
|
|
723
|
+
function crdtMissing(conn, i, n) {
|
|
724
|
+
if (crdtStore && crdtDefOf(n)) return false;
|
|
725
|
+
conn.send({ t: "err", i, code: "NOT_FOUND", m: `Unknown CRDT collection: ${n}` });
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
async function handleCrdtOpen(conn, frame) {
|
|
729
|
+
if (crdtMissing(conn, frame.i, frame.n)) return;
|
|
730
|
+
const store = crdtStore;
|
|
731
|
+
await dispatchOp(conn, frame.i, { kind: "subscribe", name: `collection:${frame.n}/${frame.id}`, conn }, async () => {
|
|
732
|
+
const state = await store.read(frame.n, frame.id);
|
|
733
|
+
if (state === void 0) throw new import_core.SuperLineError("NOT_FOUND", `No document: ${frame.n}/${frame.id}`);
|
|
710
734
|
const principal = conn.principal ?? conn.id;
|
|
711
|
-
|
|
735
|
+
const policy = collectionPolicies[frame.n];
|
|
736
|
+
if (!policy?.read) throw new import_core.SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
|
|
737
|
+
const replica = store.open(frame.n, frame.id);
|
|
738
|
+
const snapshot = replica.getSnapshot();
|
|
739
|
+
replica.close();
|
|
740
|
+
if (!await policy.read(principal, frame.id, snapshot, conn.ctx))
|
|
712
741
|
throw new import_core.SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
|
|
713
|
-
await joinChannel(conn,
|
|
714
|
-
|
|
715
|
-
conn.send({ t: "res", i: frame.i, d: resource.data });
|
|
742
|
+
await joinChannel(conn, CDOC + frame.n + ":" + frame.id);
|
|
743
|
+
conn.send({ t: "res", i: frame.i, d: state });
|
|
716
744
|
});
|
|
717
745
|
}
|
|
718
|
-
async function
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (!resource) throw new import_core.SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
746
|
+
async function handleCrdtWrite(conn, frame) {
|
|
747
|
+
if (crdtMissing(conn, frame.i, frame.n)) return;
|
|
748
|
+
const store = crdtStore;
|
|
749
|
+
const def = crdtDefOf(frame.n);
|
|
750
|
+
await dispatchOp(conn, frame.i, { kind: "request", name: `collection:${frame.n}/${frame.id}`, conn }, async () => {
|
|
724
751
|
const principal = conn.principal ?? conn.id;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
async function handleStoreWrite(conn, frame) {
|
|
731
|
-
const store = storeOrErr(conn, frame.i, frame.n);
|
|
732
|
-
if (!store) return;
|
|
733
|
-
await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
|
|
734
|
-
const resource = await store.read(frame.id);
|
|
735
|
-
if (!resource) throw new import_core.SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
736
|
-
const principal = conn.principal ?? conn.id;
|
|
737
|
-
if (!resource.accessRules[principal]?.write)
|
|
752
|
+
const policy = collectionPolicies[frame.n];
|
|
753
|
+
if (!policy?.write) throw new import_core.SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
|
|
754
|
+
if (!await policy.write(principal, frame.id, conn.ctx))
|
|
738
755
|
throw new import_core.SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
|
|
739
|
-
await store.apply(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
id: frame.id,
|
|
745
|
-
origin: frame.o,
|
|
746
|
-
connId: conn.id,
|
|
747
|
-
data: frame.u
|
|
748
|
-
});
|
|
756
|
+
await store.apply(
|
|
757
|
+
{ n: frame.n, id: frame.id, update: frame.u, origin: frame.o },
|
|
758
|
+
def.crdt,
|
|
759
|
+
(snapshot) => (0, import_core.validateSync)(def.schema, snapshot)
|
|
760
|
+
);
|
|
749
761
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
750
762
|
});
|
|
751
763
|
}
|
|
752
|
-
function
|
|
753
|
-
if (!
|
|
754
|
-
leaveChannel(conn,
|
|
755
|
-
if (taps.length) emitTap({ type: "store.unsubscribe", connId: conn.id, store: frame.n, id: frame.id });
|
|
764
|
+
function handleCrdtClose(conn, frame) {
|
|
765
|
+
if (!crdtDefOf(frame.n)) return;
|
|
766
|
+
leaveChannel(conn, CDOC + frame.n + ":" + frame.id);
|
|
756
767
|
}
|
|
757
|
-
|
|
758
|
-
const isSelf =
|
|
759
|
-
|
|
760
|
-
const channel =
|
|
768
|
+
if (crdtStore) {
|
|
769
|
+
const isSelf = crdtStore.clustering === "self";
|
|
770
|
+
crdtStore.onChange((change) => {
|
|
771
|
+
const channel = CDOC + change.n + ":" + change.id;
|
|
761
772
|
if (isSelf) {
|
|
762
773
|
const set = members.get(channel);
|
|
763
774
|
if (!set) return;
|
|
764
|
-
const payload = serializer.encode({ t: "
|
|
775
|
+
const payload = serializer.encode({ t: "cdchg", n: change.n, id: change.id, u: change.update, o: change.origin });
|
|
765
776
|
for (const conn of set) conn.sendRaw(payload);
|
|
766
777
|
return;
|
|
767
778
|
}
|
|
768
779
|
if (relaying) return;
|
|
769
780
|
void adapter.publish(
|
|
770
781
|
channel,
|
|
771
|
-
serializer.encode({ t: "
|
|
782
|
+
serializer.encode({ t: "cdchg", n: change.n, id: change.id, u: change.update, o: change.origin, nd: instanceId })
|
|
772
783
|
);
|
|
773
784
|
});
|
|
774
785
|
if (isSelf)
|
|
775
|
-
|
|
776
|
-
const set = members.get(
|
|
786
|
+
crdtStore.onDelete?.((n, id) => {
|
|
787
|
+
const set = members.get(CDOC + n + ":" + id);
|
|
777
788
|
if (!set) return;
|
|
778
|
-
const payload = serializer.encode({ t: "
|
|
789
|
+
const payload = serializer.encode({ t: "cddel", n, id });
|
|
779
790
|
for (const conn of set) conn.sendRaw(payload);
|
|
780
791
|
});
|
|
781
792
|
}
|
|
782
|
-
function
|
|
793
|
+
function handleCrdtRelay(channel, payload) {
|
|
783
794
|
const set = members.get(channel);
|
|
784
795
|
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
785
796
|
let frame;
|
|
@@ -789,20 +800,153 @@ function createSuperLineServer(contract, opts) {
|
|
|
789
800
|
return;
|
|
790
801
|
}
|
|
791
802
|
if (frame.nd === instanceId) return;
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
if (
|
|
795
|
-
|
|
803
|
+
if (!crdtStore || crdtStore.clustering !== "relay") return;
|
|
804
|
+
const def = crdtDefOf(frame.n);
|
|
805
|
+
if (!def) return;
|
|
806
|
+
if (frame.t === "cddel") {
|
|
807
|
+
try {
|
|
808
|
+
void crdtStore.delete(frame.n, frame.id);
|
|
809
|
+
} catch {
|
|
810
|
+
}
|
|
796
811
|
return;
|
|
797
812
|
}
|
|
798
813
|
relaying = true;
|
|
799
814
|
try {
|
|
800
|
-
void
|
|
815
|
+
void crdtStore.apply({ n: frame.n, id: frame.id, update: frame.u, origin: frame.o }, def.crdt, () => {
|
|
816
|
+
});
|
|
801
817
|
} catch {
|
|
802
818
|
} finally {
|
|
803
819
|
relaying = false;
|
|
804
820
|
}
|
|
805
821
|
}
|
|
822
|
+
const collConnState = (conn) => {
|
|
823
|
+
let s = connColl.get(conn);
|
|
824
|
+
if (!s) connColl.set(conn, s = { subs: /* @__PURE__ */ new Map(), policy: /* @__PURE__ */ new Map() });
|
|
825
|
+
return s;
|
|
826
|
+
};
|
|
827
|
+
function collUnsubAll(conn) {
|
|
828
|
+
connColl.delete(conn);
|
|
829
|
+
for (const set of collSubscribers.values()) set.delete(conn);
|
|
830
|
+
}
|
|
831
|
+
async function handleCollectionSub(conn, frame) {
|
|
832
|
+
if (!collectionStore || !collectionDefs[frame.n]) {
|
|
833
|
+
conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown collection: ${frame.n}` });
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
await dispatchOp(conn, frame.i, { kind: "subscribe", name: `collection:${frame.n}`, conn }, async () => {
|
|
837
|
+
const principal = conn.principal ?? conn.id;
|
|
838
|
+
const policy = collectionPolicies[frame.n];
|
|
839
|
+
if (!policy?.read) throw new import_core.SuperLineError("FORBIDDEN", `Read denied: ${frame.n}`);
|
|
840
|
+
const policyFilter = await policy.read(principal, conn.ctx);
|
|
841
|
+
const eff = (0, import_core.andFilters)(policyFilter, frame.q.filter);
|
|
842
|
+
const rows = await collectionStore.snapshot(frame.n, { ...frame.q, filter: eff });
|
|
843
|
+
const state = collConnState(conn);
|
|
844
|
+
let subs = state.subs.get(frame.n);
|
|
845
|
+
if (!subs) state.subs.set(frame.n, subs = /* @__PURE__ */ new Map());
|
|
846
|
+
subs.set(frame.s, frame.q);
|
|
847
|
+
state.policy.set(frame.n, policyFilter);
|
|
848
|
+
let set = collSubscribers.get(frame.n);
|
|
849
|
+
if (!set) collSubscribers.set(frame.n, set = /* @__PURE__ */ new Set());
|
|
850
|
+
set.add(conn);
|
|
851
|
+
conn.send({ t: "res", i: frame.i, d: rows });
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
function handleCollectionUnsub(conn, frame) {
|
|
855
|
+
const state = connColl.get(conn);
|
|
856
|
+
const subs = state?.subs.get(frame.n);
|
|
857
|
+
if (!state || !subs) return;
|
|
858
|
+
subs.delete(frame.s);
|
|
859
|
+
if (subs.size === 0) {
|
|
860
|
+
state.subs.delete(frame.n);
|
|
861
|
+
state.policy.delete(frame.n);
|
|
862
|
+
collSubscribers.get(frame.n)?.delete(conn);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function resolveCollectionOps(ops, principal, ctx) {
|
|
866
|
+
const store = collectionStore;
|
|
867
|
+
if (!store) throw new import_core.SuperLineError("NOT_FOUND", "No collection backend configured");
|
|
868
|
+
const out = [];
|
|
869
|
+
for (const op of ops) {
|
|
870
|
+
const def = collectionDefs[op.n];
|
|
871
|
+
if (!def) throw new import_core.SuperLineError("NOT_FOUND", `Unknown collection: ${op.n}`);
|
|
872
|
+
if ((0, import_core.isCrdtCollection)(def)) throw new import_core.SuperLineError("NOT_FOUND", `Collection ${op.n} is a CRDT document collection \u2014 use collection(n).open(id), not a row batch`);
|
|
873
|
+
const policy = collectionPolicies[op.n];
|
|
874
|
+
if (!policy?.write) throw new import_core.SuperLineError("FORBIDDEN", `Write denied: ${op.n}`);
|
|
875
|
+
const prev = await store.read(op.n, op.id);
|
|
876
|
+
if (op.op === "delete") {
|
|
877
|
+
if (!await policy.write(principal, "delete", void 0, prev, ctx))
|
|
878
|
+
throw new import_core.SuperLineError("FORBIDDEN", `Write denied: ${op.n}/${op.id}`);
|
|
879
|
+
out.push({ op: "delete", n: op.n, id: op.id });
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
const row = await (0, import_core.validate)(def.schema, op.d);
|
|
883
|
+
const key = row[def.key];
|
|
884
|
+
if (typeof key !== "string")
|
|
885
|
+
throw new import_core.SuperLineError("VALIDATION", `Collection ${op.n} row is missing string key '${def.key}'`);
|
|
886
|
+
if (key !== op.id) throw new import_core.SuperLineError("VALIDATION", `Row key '${key}' does not match op id '${op.id}'`);
|
|
887
|
+
if (checkReferences && def.references) {
|
|
888
|
+
for (const [field, refCollection] of Object.entries(def.references)) {
|
|
889
|
+
const ref = row[field];
|
|
890
|
+
if (ref === void 0 || ref === null) continue;
|
|
891
|
+
if (await store.read(refCollection, String(ref)) === void 0)
|
|
892
|
+
throw new import_core.SuperLineError("VALIDATION", `Dangling reference: ${op.n}.${field} \u2192 ${refCollection}/${String(ref)} does not exist`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const kind = op.op;
|
|
896
|
+
if (!await policy.write(principal, kind, row, prev, ctx))
|
|
897
|
+
throw new import_core.SuperLineError("FORBIDDEN", `Write denied: ${op.n}/${op.id}`);
|
|
898
|
+
out.push({ op: kind, n: op.n, id: op.id, row });
|
|
899
|
+
}
|
|
900
|
+
return out;
|
|
901
|
+
}
|
|
902
|
+
async function commitCollectionBatch(ops, origin, relay) {
|
|
903
|
+
if (ops.length === 0 || !collectionStore) return;
|
|
904
|
+
await collectionStore.apply(ops, origin);
|
|
905
|
+
if (relay && collIsRelay)
|
|
906
|
+
void adapter.publish(COLL_CHANNEL, serializer.encode({ ops, origin, nd: instanceId }));
|
|
907
|
+
}
|
|
908
|
+
async function handleCollectionBatch(conn, frame) {
|
|
909
|
+
if (!collectionStore) {
|
|
910
|
+
conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: "No collection backend configured" });
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
await dispatchOp(conn, frame.i, { kind: "request", name: "collection:batch", conn }, async () => {
|
|
914
|
+
const principal = conn.principal ?? conn.id;
|
|
915
|
+
const resolved = await resolveCollectionOps(frame.ops, principal, conn.ctx);
|
|
916
|
+
await commitCollectionBatch(resolved, principal, true);
|
|
917
|
+
conn.send({ t: "res", i: frame.i, d: null });
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
function routeRowChange(change) {
|
|
921
|
+
const conns2 = collSubscribers.get(change.n);
|
|
922
|
+
if (!conns2 || conns2.size === 0) return;
|
|
923
|
+
for (const conn of conns2) {
|
|
924
|
+
const state = connColl.get(conn);
|
|
925
|
+
const subs = state?.subs.get(change.n);
|
|
926
|
+
if (!state || !subs || subs.size === 0) continue;
|
|
927
|
+
const eff = (0, import_core.andFilters)(state.policy.get(change.n), (0, import_core.orFilters)([...subs.values()].map((q) => q.filter)));
|
|
928
|
+
const prevlessDelete = change.k === "delete" && change.prev === void 0;
|
|
929
|
+
const inPrev = prevlessDelete || change.prev !== void 0 && (0, import_core.matchesFilter)(eff, change.prev);
|
|
930
|
+
const inNext = change.next !== void 0 && (0, import_core.matchesFilter)(eff, change.next);
|
|
931
|
+
if (!inPrev && !inNext) continue;
|
|
932
|
+
conn.send({ t: "cchg", n: change.n, k: change.k, id: change.id, d: change.next });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function handleCollectionRelay(payload) {
|
|
936
|
+
let env;
|
|
937
|
+
try {
|
|
938
|
+
env = serializer.decode(payload);
|
|
939
|
+
} catch {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (env.nd === instanceId) return;
|
|
943
|
+
if (!collectionStore || !collIsRelay) return;
|
|
944
|
+
try {
|
|
945
|
+
void collectionStore.apply(env.ops, env.origin);
|
|
946
|
+
} catch {
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (collectionStore) collectionStore.onChange(routeRowChange);
|
|
806
950
|
function room(name) {
|
|
807
951
|
const channel = ROOM + name;
|
|
808
952
|
return {
|
|
@@ -947,78 +1091,6 @@ function createSuperLineServer(contract, opts) {
|
|
|
947
1091
|
void adapter.publish(CONN + id, serializer.encode(env));
|
|
948
1092
|
});
|
|
949
1093
|
}
|
|
950
|
-
const storeApi = {};
|
|
951
|
-
for (const [name, store] of Object.entries(storeMap)) {
|
|
952
|
-
const readOrThrow = async (id) => {
|
|
953
|
-
const r = await store.read(id);
|
|
954
|
-
if (!r) throw new import_core.SuperLineError("NOT_FOUND", `No resource: ${name}/${id}`);
|
|
955
|
-
return r;
|
|
956
|
-
};
|
|
957
|
-
storeApi[name] = {
|
|
958
|
-
async create(id, data, accessRules) {
|
|
959
|
-
await store.create(id, data, accessRules);
|
|
960
|
-
if (taps.length) emitTap({ type: "store.create", store: name, id });
|
|
961
|
-
},
|
|
962
|
-
read(id) {
|
|
963
|
-
return Promise.resolve(store.read(id));
|
|
964
|
-
},
|
|
965
|
-
async write(id, data, opts2) {
|
|
966
|
-
const origin = opts2?.origin ?? SERVER_ORIGIN;
|
|
967
|
-
await store.apply({ id, update: data, origin });
|
|
968
|
-
if (taps.length) emitTap({ type: "store.write", store: name, id, origin, data });
|
|
969
|
-
},
|
|
970
|
-
async grant(id, principal, perms) {
|
|
971
|
-
const r = await readOrThrow(id);
|
|
972
|
-
await store.setAccess(id, { ...r.accessRules, [principal]: perms });
|
|
973
|
-
if (taps.length) emitTap({ type: "store.grant", store: name, id, principal, perms });
|
|
974
|
-
},
|
|
975
|
-
async revoke(id, principal) {
|
|
976
|
-
const r = await readOrThrow(id);
|
|
977
|
-
const next = { ...r.accessRules };
|
|
978
|
-
delete next[principal];
|
|
979
|
-
await store.setAccess(id, next);
|
|
980
|
-
if (taps.length) emitTap({ type: "store.revoke", store: name, id, principal });
|
|
981
|
-
},
|
|
982
|
-
async delete(id) {
|
|
983
|
-
await store.delete(id);
|
|
984
|
-
if (store.clustering !== "self")
|
|
985
|
-
void adapter.publish(
|
|
986
|
-
STORE + name + ":" + id,
|
|
987
|
-
serializer.encode({ t: "sdel", n: name, id, nd: instanceId })
|
|
988
|
-
);
|
|
989
|
-
if (taps.length) emitTap({ type: "store.delete", store: name, id });
|
|
990
|
-
},
|
|
991
|
-
list(opts2) {
|
|
992
|
-
return Promise.resolve(store.list(opts2));
|
|
993
|
-
},
|
|
994
|
-
searchPrincipals(opts2) {
|
|
995
|
-
return Promise.resolve(store.searchPrincipals(opts2));
|
|
996
|
-
},
|
|
997
|
-
open(id, openOpts) {
|
|
998
|
-
if (!store.open)
|
|
999
|
-
throw new import_core.SuperLineError("UNSUPPORTED", `Store ${name} does not support reactive open()`);
|
|
1000
|
-
const replica = store.open(id, openOpts);
|
|
1001
|
-
if (!taps.length) return replica;
|
|
1002
|
-
const origin = openOpts?.origin ?? SERVER_ORIGIN;
|
|
1003
|
-
const emit = (data) => emitTap({ type: "store.write", store: name, id, origin, data });
|
|
1004
|
-
return {
|
|
1005
|
-
...replica,
|
|
1006
|
-
set: (d) => {
|
|
1007
|
-
replica.set(d);
|
|
1008
|
-
emit(d);
|
|
1009
|
-
},
|
|
1010
|
-
update: (p) => {
|
|
1011
|
-
replica.update(p);
|
|
1012
|
-
emit(p);
|
|
1013
|
-
},
|
|
1014
|
-
delete: (path) => {
|
|
1015
|
-
replica.delete(path);
|
|
1016
|
-
emit({ delete: path });
|
|
1017
|
-
}
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
1094
|
const pluginDisposers = [];
|
|
1023
1095
|
const pluginHandlers = {};
|
|
1024
1096
|
const api = {
|
|
@@ -1119,9 +1191,67 @@ function createSuperLineServer(contract, opts) {
|
|
|
1119
1191
|
}
|
|
1120
1192
|
};
|
|
1121
1193
|
},
|
|
1122
|
-
|
|
1123
|
-
const
|
|
1124
|
-
if (!
|
|
1194
|
+
collection(name) {
|
|
1195
|
+
const def = collectionDefs[name];
|
|
1196
|
+
if (!def) throw new import_core.SuperLineError("NOT_FOUND", `Collection not declared: ${name}`);
|
|
1197
|
+
if ((0, import_core.isCrdtCollection)(def)) {
|
|
1198
|
+
if (!crdtStore) throw new import_core.SuperLineError("NOT_FOUND", "No CRDT collection backend configured");
|
|
1199
|
+
const cstore = crdtStore;
|
|
1200
|
+
const handle2 = {
|
|
1201
|
+
async create(id, data) {
|
|
1202
|
+
const v = await (0, import_core.validate)(def.schema, data);
|
|
1203
|
+
await cstore.create(name, id, v, def.crdt);
|
|
1204
|
+
},
|
|
1205
|
+
open(id, o) {
|
|
1206
|
+
return cstore.open(name, id, { ...o, doc: def.crdt });
|
|
1207
|
+
},
|
|
1208
|
+
async read(id) {
|
|
1209
|
+
const state = await cstore.read(name, id);
|
|
1210
|
+
if (state === void 0) return void 0;
|
|
1211
|
+
const r = cstore.open(name, id, { doc: def.crdt });
|
|
1212
|
+
const s = r.getSnapshot();
|
|
1213
|
+
r.close();
|
|
1214
|
+
return s;
|
|
1215
|
+
},
|
|
1216
|
+
async delete(id) {
|
|
1217
|
+
await cstore.delete(name, id);
|
|
1218
|
+
if (cstore.clustering !== "self")
|
|
1219
|
+
void adapter.publish(CDOC + name + ":" + id, serializer.encode({ t: "cddel", n: name, id, nd: instanceId }));
|
|
1220
|
+
},
|
|
1221
|
+
list(o) {
|
|
1222
|
+
return Promise.resolve(cstore.list(name, o));
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
return handle2;
|
|
1226
|
+
}
|
|
1227
|
+
if (!collectionStore) throw new import_core.SuperLineError("NOT_FOUND", "No collection backend configured");
|
|
1228
|
+
const store = collectionStore;
|
|
1229
|
+
const resolve = async (row) => {
|
|
1230
|
+
const v = await (0, import_core.validate)(def.schema, row);
|
|
1231
|
+
const key = v[def.key];
|
|
1232
|
+
if (typeof key !== "string")
|
|
1233
|
+
throw new import_core.SuperLineError("VALIDATION", `Collection ${name} row is missing string key '${def.key}'`);
|
|
1234
|
+
return { id: key, row: v };
|
|
1235
|
+
};
|
|
1236
|
+
const handle = {
|
|
1237
|
+
async insert(row) {
|
|
1238
|
+
const { id, row: v } = await resolve(row);
|
|
1239
|
+
await commitCollectionBatch([{ op: "insert", n: name, id, row: v }], SERVER_ORIGIN, true);
|
|
1240
|
+
},
|
|
1241
|
+
async update(row) {
|
|
1242
|
+
const { id, row: v } = await resolve(row);
|
|
1243
|
+
await commitCollectionBatch([{ op: "update", n: name, id, row: v }], SERVER_ORIGIN, true);
|
|
1244
|
+
},
|
|
1245
|
+
async delete(id) {
|
|
1246
|
+
await commitCollectionBatch([{ op: "delete", n: name, id }], SERVER_ORIGIN, true);
|
|
1247
|
+
},
|
|
1248
|
+
read(id) {
|
|
1249
|
+
return Promise.resolve(store.read(name, id));
|
|
1250
|
+
},
|
|
1251
|
+
snapshot(query) {
|
|
1252
|
+
return Promise.resolve(store.snapshot(name, query ?? {}));
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1125
1255
|
return handle;
|
|
1126
1256
|
},
|
|
1127
1257
|
async close() {
|
|
@@ -1180,8 +1310,14 @@ function createSuperLineServer(contract, opts) {
|
|
|
1180
1310
|
}
|
|
1181
1311
|
};
|
|
1182
1312
|
},
|
|
1183
|
-
|
|
1184
|
-
|
|
1313
|
+
collection: (name) => api.collection(name),
|
|
1314
|
+
collectionInfos: () => (
|
|
1315
|
+
// CRDT document collections surface with a synthetic `id` key + no references — the inspector's
|
|
1316
|
+
// queryCollection synthesizes doc-rows for them (they're open-by-id, not row-queryable).
|
|
1317
|
+
Object.entries(collectionDefs).map(
|
|
1318
|
+
([name, def]) => (0, import_core.isCrdtCollection)(def) ? { name, key: "id", references: {} } : { name, key: def.key, references: def.references ?? {} }
|
|
1319
|
+
)
|
|
1320
|
+
),
|
|
1185
1321
|
describe: (conn) => buildDescriptor(conn),
|
|
1186
1322
|
connectionById: (id) => Promise.resolve(presenceOrThrow().get(id)),
|
|
1187
1323
|
channel: (name) => pluginChannel(pluginName, name)
|