@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/dist/index.js
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
jsonSerializer,
|
|
4
4
|
validate,
|
|
5
|
-
|
|
5
|
+
validateSync,
|
|
6
|
+
SuperLineError,
|
|
7
|
+
andFilters,
|
|
8
|
+
orFilters,
|
|
9
|
+
matchesFilter,
|
|
10
|
+
isCrdtCollection
|
|
6
11
|
} from "@super-line/core";
|
|
7
12
|
|
|
8
13
|
// src/conn.ts
|
|
@@ -181,27 +186,43 @@ var CONN = "c:";
|
|
|
181
186
|
var USER = "u:";
|
|
182
187
|
var REPLY = "reply:";
|
|
183
188
|
var PLUGIN = "x:";
|
|
184
|
-
var
|
|
189
|
+
var CDOC = "d:";
|
|
185
190
|
var SERVER_ORIGIN = "server";
|
|
186
191
|
function createSuperLineServer(contract, opts) {
|
|
187
192
|
const c = contract;
|
|
188
193
|
const serializer = opts.serializer ?? jsonSerializer;
|
|
189
194
|
const adapter = opts.adapter ?? createInMemoryAdapter();
|
|
190
|
-
const storeMap = opts.stores ?? {};
|
|
191
195
|
const plugins = opts.plugins ?? [];
|
|
192
196
|
const pluginNames = /* @__PURE__ */ new Set();
|
|
193
197
|
for (const p of plugins) {
|
|
194
198
|
if (pluginNames.has(p.name)) throw new Error(`Duplicate plugin name: ${p.name}`);
|
|
195
199
|
pluginNames.add(p.name);
|
|
196
200
|
}
|
|
201
|
+
const collectionStore = opts.collections;
|
|
202
|
+
const collectionDefs = c.collections ?? {};
|
|
203
|
+
const collectionPolicies = { ...opts.policies };
|
|
197
204
|
for (const p of plugins) {
|
|
198
|
-
if (!p.
|
|
199
|
-
for (const [name,
|
|
200
|
-
if (name in
|
|
201
|
-
throw new Error(
|
|
202
|
-
|
|
205
|
+
if (!p.policies) continue;
|
|
206
|
+
for (const [name, policy] of Object.entries(p.policies)) {
|
|
207
|
+
if (!(name in collectionDefs))
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Plugin '${p.name}' policy references unknown collection '${name}' \u2014 register its contract fragment on defineContract({ plugins })`
|
|
210
|
+
);
|
|
211
|
+
if (name in collectionPolicies)
|
|
212
|
+
throw new Error(`Plugin '${p.name}' policy for collection '${name}' collides with an existing policy`);
|
|
213
|
+
collectionPolicies[name] = policy;
|
|
203
214
|
}
|
|
204
215
|
}
|
|
216
|
+
const checkReferences = opts.checkReferences ?? false;
|
|
217
|
+
const collIsRelay = collectionStore?.clustering === "relay";
|
|
218
|
+
const crdtStore = opts.crdtCollections;
|
|
219
|
+
const crdtDefOf = (n) => {
|
|
220
|
+
const d = collectionDefs[n];
|
|
221
|
+
return d && isCrdtCollection(d) ? d : void 0;
|
|
222
|
+
};
|
|
223
|
+
const COLL_CHANNEL = "cbatch";
|
|
224
|
+
const collSubscribers = /* @__PURE__ */ new Map();
|
|
225
|
+
const connColl = /* @__PURE__ */ new Map();
|
|
205
226
|
const reserved = [];
|
|
206
227
|
for (const p of plugins) {
|
|
207
228
|
if (!p.connection) continue;
|
|
@@ -295,8 +316,12 @@ function createSuperLineServer(contract, opts) {
|
|
|
295
316
|
handlePersonal(channel, payload);
|
|
296
317
|
return;
|
|
297
318
|
}
|
|
298
|
-
if (channel.startsWith(
|
|
299
|
-
|
|
319
|
+
if (channel.startsWith(CDOC)) {
|
|
320
|
+
handleCrdtRelay(channel, payload);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (channel === COLL_CHANNEL) {
|
|
324
|
+
handleCollectionRelay(payload);
|
|
300
325
|
return;
|
|
301
326
|
}
|
|
302
327
|
if (channel.startsWith(PLUGIN)) {
|
|
@@ -309,6 +334,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
309
334
|
if (busSet) deliverBus(payload, busSet);
|
|
310
335
|
});
|
|
311
336
|
void adapter.subscribe(replyChannel);
|
|
337
|
+
if (collectionStore && collIsRelay) void adapter.subscribe(COLL_CHANNEL);
|
|
312
338
|
function handlePersonal(channel, payload) {
|
|
313
339
|
const set = members.get(channel);
|
|
314
340
|
if (!set) return;
|
|
@@ -542,6 +568,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
542
568
|
raw.onClose((code) => {
|
|
543
569
|
conns.delete(conn);
|
|
544
570
|
for (const channel of conn.channels) leaveChannel(conn, channel);
|
|
571
|
+
collUnsubAll(conn);
|
|
545
572
|
void adapter.presence?.del(conn.id);
|
|
546
573
|
const goneUserId = opts.identify?.(conn);
|
|
547
574
|
emitTap({
|
|
@@ -576,10 +603,12 @@ function createSuperLineServer(contract, opts) {
|
|
|
576
603
|
else if (frame.t === "unsub") {
|
|
577
604
|
const ns = topicNamespace(conn.role, frame.c);
|
|
578
605
|
if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
|
|
579
|
-
} else if (frame.t === "
|
|
580
|
-
else if (frame.t === "
|
|
581
|
-
else if (frame.t === "
|
|
582
|
-
else if (frame.t === "
|
|
606
|
+
} else if (frame.t === "csub") await handleCollectionSub(conn, frame);
|
|
607
|
+
else if (frame.t === "cuns") handleCollectionUnsub(conn, frame);
|
|
608
|
+
else if (frame.t === "cbat") await handleCollectionBatch(conn, frame);
|
|
609
|
+
else if (frame.t === "cdopen") await handleCrdtOpen(conn, frame);
|
|
610
|
+
else if (frame.t === "cdwr") await handleCrdtWrite(conn, frame);
|
|
611
|
+
else if (frame.t === "cdclose") handleCrdtClose(conn, frame);
|
|
583
612
|
else if (frame.t === "sres") {
|
|
584
613
|
await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
|
|
585
614
|
} else if (frame.t === "serr") {
|
|
@@ -672,90 +701,77 @@ function createSuperLineServer(contract, opts) {
|
|
|
672
701
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
673
702
|
});
|
|
674
703
|
}
|
|
675
|
-
function
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return
|
|
679
|
-
}
|
|
680
|
-
async function
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
await dispatchOp(conn, frame.i, { kind: "subscribe", name: `
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
704
|
+
function crdtMissing(conn, i, n) {
|
|
705
|
+
if (crdtStore && crdtDefOf(n)) return false;
|
|
706
|
+
conn.send({ t: "err", i, code: "NOT_FOUND", m: `Unknown CRDT collection: ${n}` });
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
async function handleCrdtOpen(conn, frame) {
|
|
710
|
+
if (crdtMissing(conn, frame.i, frame.n)) return;
|
|
711
|
+
const store = crdtStore;
|
|
712
|
+
await dispatchOp(conn, frame.i, { kind: "subscribe", name: `collection:${frame.n}/${frame.id}`, conn }, async () => {
|
|
713
|
+
const state = await store.read(frame.n, frame.id);
|
|
714
|
+
if (state === void 0) throw new SuperLineError("NOT_FOUND", `No document: ${frame.n}/${frame.id}`);
|
|
686
715
|
const principal = conn.principal ?? conn.id;
|
|
687
|
-
|
|
716
|
+
const policy = collectionPolicies[frame.n];
|
|
717
|
+
if (!policy?.read) throw new SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
|
|
718
|
+
const replica = store.open(frame.n, frame.id);
|
|
719
|
+
const snapshot = replica.getSnapshot();
|
|
720
|
+
replica.close();
|
|
721
|
+
if (!await policy.read(principal, frame.id, snapshot, conn.ctx))
|
|
688
722
|
throw new SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
|
|
689
|
-
await joinChannel(conn,
|
|
690
|
-
|
|
691
|
-
conn.send({ t: "res", i: frame.i, d: resource.data });
|
|
723
|
+
await joinChannel(conn, CDOC + frame.n + ":" + frame.id);
|
|
724
|
+
conn.send({ t: "res", i: frame.i, d: state });
|
|
692
725
|
});
|
|
693
726
|
}
|
|
694
|
-
async function
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (!resource) throw new SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
727
|
+
async function handleCrdtWrite(conn, frame) {
|
|
728
|
+
if (crdtMissing(conn, frame.i, frame.n)) return;
|
|
729
|
+
const store = crdtStore;
|
|
730
|
+
const def = crdtDefOf(frame.n);
|
|
731
|
+
await dispatchOp(conn, frame.i, { kind: "request", name: `collection:${frame.n}/${frame.id}`, conn }, async () => {
|
|
700
732
|
const principal = conn.principal ?? conn.id;
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
async function handleStoreWrite(conn, frame) {
|
|
707
|
-
const store = storeOrErr(conn, frame.i, frame.n);
|
|
708
|
-
if (!store) return;
|
|
709
|
-
await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
|
|
710
|
-
const resource = await store.read(frame.id);
|
|
711
|
-
if (!resource) throw new SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
712
|
-
const principal = conn.principal ?? conn.id;
|
|
713
|
-
if (!resource.accessRules[principal]?.write)
|
|
733
|
+
const policy = collectionPolicies[frame.n];
|
|
734
|
+
if (!policy?.write) throw new SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
|
|
735
|
+
if (!await policy.write(principal, frame.id, conn.ctx))
|
|
714
736
|
throw new SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
|
|
715
|
-
await store.apply(
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
id: frame.id,
|
|
721
|
-
origin: frame.o,
|
|
722
|
-
connId: conn.id,
|
|
723
|
-
data: frame.u
|
|
724
|
-
});
|
|
737
|
+
await store.apply(
|
|
738
|
+
{ n: frame.n, id: frame.id, update: frame.u, origin: frame.o },
|
|
739
|
+
def.crdt,
|
|
740
|
+
(snapshot) => validateSync(def.schema, snapshot)
|
|
741
|
+
);
|
|
725
742
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
726
743
|
});
|
|
727
744
|
}
|
|
728
|
-
function
|
|
729
|
-
if (!
|
|
730
|
-
leaveChannel(conn,
|
|
731
|
-
if (taps.length) emitTap({ type: "store.unsubscribe", connId: conn.id, store: frame.n, id: frame.id });
|
|
745
|
+
function handleCrdtClose(conn, frame) {
|
|
746
|
+
if (!crdtDefOf(frame.n)) return;
|
|
747
|
+
leaveChannel(conn, CDOC + frame.n + ":" + frame.id);
|
|
732
748
|
}
|
|
733
|
-
|
|
734
|
-
const isSelf =
|
|
735
|
-
|
|
736
|
-
const channel =
|
|
749
|
+
if (crdtStore) {
|
|
750
|
+
const isSelf = crdtStore.clustering === "self";
|
|
751
|
+
crdtStore.onChange((change) => {
|
|
752
|
+
const channel = CDOC + change.n + ":" + change.id;
|
|
737
753
|
if (isSelf) {
|
|
738
754
|
const set = members.get(channel);
|
|
739
755
|
if (!set) return;
|
|
740
|
-
const payload = serializer.encode({ t: "
|
|
756
|
+
const payload = serializer.encode({ t: "cdchg", n: change.n, id: change.id, u: change.update, o: change.origin });
|
|
741
757
|
for (const conn of set) conn.sendRaw(payload);
|
|
742
758
|
return;
|
|
743
759
|
}
|
|
744
760
|
if (relaying) return;
|
|
745
761
|
void adapter.publish(
|
|
746
762
|
channel,
|
|
747
|
-
serializer.encode({ t: "
|
|
763
|
+
serializer.encode({ t: "cdchg", n: change.n, id: change.id, u: change.update, o: change.origin, nd: instanceId })
|
|
748
764
|
);
|
|
749
765
|
});
|
|
750
766
|
if (isSelf)
|
|
751
|
-
|
|
752
|
-
const set = members.get(
|
|
767
|
+
crdtStore.onDelete?.((n, id) => {
|
|
768
|
+
const set = members.get(CDOC + n + ":" + id);
|
|
753
769
|
if (!set) return;
|
|
754
|
-
const payload = serializer.encode({ t: "
|
|
770
|
+
const payload = serializer.encode({ t: "cddel", n, id });
|
|
755
771
|
for (const conn of set) conn.sendRaw(payload);
|
|
756
772
|
});
|
|
757
773
|
}
|
|
758
|
-
function
|
|
774
|
+
function handleCrdtRelay(channel, payload) {
|
|
759
775
|
const set = members.get(channel);
|
|
760
776
|
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
761
777
|
let frame;
|
|
@@ -765,20 +781,153 @@ function createSuperLineServer(contract, opts) {
|
|
|
765
781
|
return;
|
|
766
782
|
}
|
|
767
783
|
if (frame.nd === instanceId) return;
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if (
|
|
771
|
-
|
|
784
|
+
if (!crdtStore || crdtStore.clustering !== "relay") return;
|
|
785
|
+
const def = crdtDefOf(frame.n);
|
|
786
|
+
if (!def) return;
|
|
787
|
+
if (frame.t === "cddel") {
|
|
788
|
+
try {
|
|
789
|
+
void crdtStore.delete(frame.n, frame.id);
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
772
792
|
return;
|
|
773
793
|
}
|
|
774
794
|
relaying = true;
|
|
775
795
|
try {
|
|
776
|
-
void
|
|
796
|
+
void crdtStore.apply({ n: frame.n, id: frame.id, update: frame.u, origin: frame.o }, def.crdt, () => {
|
|
797
|
+
});
|
|
777
798
|
} catch {
|
|
778
799
|
} finally {
|
|
779
800
|
relaying = false;
|
|
780
801
|
}
|
|
781
802
|
}
|
|
803
|
+
const collConnState = (conn) => {
|
|
804
|
+
let s = connColl.get(conn);
|
|
805
|
+
if (!s) connColl.set(conn, s = { subs: /* @__PURE__ */ new Map(), policy: /* @__PURE__ */ new Map() });
|
|
806
|
+
return s;
|
|
807
|
+
};
|
|
808
|
+
function collUnsubAll(conn) {
|
|
809
|
+
connColl.delete(conn);
|
|
810
|
+
for (const set of collSubscribers.values()) set.delete(conn);
|
|
811
|
+
}
|
|
812
|
+
async function handleCollectionSub(conn, frame) {
|
|
813
|
+
if (!collectionStore || !collectionDefs[frame.n]) {
|
|
814
|
+
conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown collection: ${frame.n}` });
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
await dispatchOp(conn, frame.i, { kind: "subscribe", name: `collection:${frame.n}`, conn }, async () => {
|
|
818
|
+
const principal = conn.principal ?? conn.id;
|
|
819
|
+
const policy = collectionPolicies[frame.n];
|
|
820
|
+
if (!policy?.read) throw new SuperLineError("FORBIDDEN", `Read denied: ${frame.n}`);
|
|
821
|
+
const policyFilter = await policy.read(principal, conn.ctx);
|
|
822
|
+
const eff = andFilters(policyFilter, frame.q.filter);
|
|
823
|
+
const rows = await collectionStore.snapshot(frame.n, { ...frame.q, filter: eff });
|
|
824
|
+
const state = collConnState(conn);
|
|
825
|
+
let subs = state.subs.get(frame.n);
|
|
826
|
+
if (!subs) state.subs.set(frame.n, subs = /* @__PURE__ */ new Map());
|
|
827
|
+
subs.set(frame.s, frame.q);
|
|
828
|
+
state.policy.set(frame.n, policyFilter);
|
|
829
|
+
let set = collSubscribers.get(frame.n);
|
|
830
|
+
if (!set) collSubscribers.set(frame.n, set = /* @__PURE__ */ new Set());
|
|
831
|
+
set.add(conn);
|
|
832
|
+
conn.send({ t: "res", i: frame.i, d: rows });
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
function handleCollectionUnsub(conn, frame) {
|
|
836
|
+
const state = connColl.get(conn);
|
|
837
|
+
const subs = state?.subs.get(frame.n);
|
|
838
|
+
if (!state || !subs) return;
|
|
839
|
+
subs.delete(frame.s);
|
|
840
|
+
if (subs.size === 0) {
|
|
841
|
+
state.subs.delete(frame.n);
|
|
842
|
+
state.policy.delete(frame.n);
|
|
843
|
+
collSubscribers.get(frame.n)?.delete(conn);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async function resolveCollectionOps(ops, principal, ctx) {
|
|
847
|
+
const store = collectionStore;
|
|
848
|
+
if (!store) throw new SuperLineError("NOT_FOUND", "No collection backend configured");
|
|
849
|
+
const out = [];
|
|
850
|
+
for (const op of ops) {
|
|
851
|
+
const def = collectionDefs[op.n];
|
|
852
|
+
if (!def) throw new SuperLineError("NOT_FOUND", `Unknown collection: ${op.n}`);
|
|
853
|
+
if (isCrdtCollection(def)) throw new SuperLineError("NOT_FOUND", `Collection ${op.n} is a CRDT document collection \u2014 use collection(n).open(id), not a row batch`);
|
|
854
|
+
const policy = collectionPolicies[op.n];
|
|
855
|
+
if (!policy?.write) throw new SuperLineError("FORBIDDEN", `Write denied: ${op.n}`);
|
|
856
|
+
const prev = await store.read(op.n, op.id);
|
|
857
|
+
if (op.op === "delete") {
|
|
858
|
+
if (!await policy.write(principal, "delete", void 0, prev, ctx))
|
|
859
|
+
throw new SuperLineError("FORBIDDEN", `Write denied: ${op.n}/${op.id}`);
|
|
860
|
+
out.push({ op: "delete", n: op.n, id: op.id });
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
const row = await validate(def.schema, op.d);
|
|
864
|
+
const key = row[def.key];
|
|
865
|
+
if (typeof key !== "string")
|
|
866
|
+
throw new SuperLineError("VALIDATION", `Collection ${op.n} row is missing string key '${def.key}'`);
|
|
867
|
+
if (key !== op.id) throw new SuperLineError("VALIDATION", `Row key '${key}' does not match op id '${op.id}'`);
|
|
868
|
+
if (checkReferences && def.references) {
|
|
869
|
+
for (const [field, refCollection] of Object.entries(def.references)) {
|
|
870
|
+
const ref = row[field];
|
|
871
|
+
if (ref === void 0 || ref === null) continue;
|
|
872
|
+
if (await store.read(refCollection, String(ref)) === void 0)
|
|
873
|
+
throw new SuperLineError("VALIDATION", `Dangling reference: ${op.n}.${field} \u2192 ${refCollection}/${String(ref)} does not exist`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const kind = op.op;
|
|
877
|
+
if (!await policy.write(principal, kind, row, prev, ctx))
|
|
878
|
+
throw new SuperLineError("FORBIDDEN", `Write denied: ${op.n}/${op.id}`);
|
|
879
|
+
out.push({ op: kind, n: op.n, id: op.id, row });
|
|
880
|
+
}
|
|
881
|
+
return out;
|
|
882
|
+
}
|
|
883
|
+
async function commitCollectionBatch(ops, origin, relay) {
|
|
884
|
+
if (ops.length === 0 || !collectionStore) return;
|
|
885
|
+
await collectionStore.apply(ops, origin);
|
|
886
|
+
if (relay && collIsRelay)
|
|
887
|
+
void adapter.publish(COLL_CHANNEL, serializer.encode({ ops, origin, nd: instanceId }));
|
|
888
|
+
}
|
|
889
|
+
async function handleCollectionBatch(conn, frame) {
|
|
890
|
+
if (!collectionStore) {
|
|
891
|
+
conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: "No collection backend configured" });
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
await dispatchOp(conn, frame.i, { kind: "request", name: "collection:batch", conn }, async () => {
|
|
895
|
+
const principal = conn.principal ?? conn.id;
|
|
896
|
+
const resolved = await resolveCollectionOps(frame.ops, principal, conn.ctx);
|
|
897
|
+
await commitCollectionBatch(resolved, principal, true);
|
|
898
|
+
conn.send({ t: "res", i: frame.i, d: null });
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
function routeRowChange(change) {
|
|
902
|
+
const conns2 = collSubscribers.get(change.n);
|
|
903
|
+
if (!conns2 || conns2.size === 0) return;
|
|
904
|
+
for (const conn of conns2) {
|
|
905
|
+
const state = connColl.get(conn);
|
|
906
|
+
const subs = state?.subs.get(change.n);
|
|
907
|
+
if (!state || !subs || subs.size === 0) continue;
|
|
908
|
+
const eff = andFilters(state.policy.get(change.n), orFilters([...subs.values()].map((q) => q.filter)));
|
|
909
|
+
const prevlessDelete = change.k === "delete" && change.prev === void 0;
|
|
910
|
+
const inPrev = prevlessDelete || change.prev !== void 0 && matchesFilter(eff, change.prev);
|
|
911
|
+
const inNext = change.next !== void 0 && matchesFilter(eff, change.next);
|
|
912
|
+
if (!inPrev && !inNext) continue;
|
|
913
|
+
conn.send({ t: "cchg", n: change.n, k: change.k, id: change.id, d: change.next });
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function handleCollectionRelay(payload) {
|
|
917
|
+
let env;
|
|
918
|
+
try {
|
|
919
|
+
env = serializer.decode(payload);
|
|
920
|
+
} catch {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (env.nd === instanceId) return;
|
|
924
|
+
if (!collectionStore || !collIsRelay) return;
|
|
925
|
+
try {
|
|
926
|
+
void collectionStore.apply(env.ops, env.origin);
|
|
927
|
+
} catch {
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (collectionStore) collectionStore.onChange(routeRowChange);
|
|
782
931
|
function room(name) {
|
|
783
932
|
const channel = ROOM + name;
|
|
784
933
|
return {
|
|
@@ -923,78 +1072,6 @@ function createSuperLineServer(contract, opts) {
|
|
|
923
1072
|
void adapter.publish(CONN + id, serializer.encode(env));
|
|
924
1073
|
});
|
|
925
1074
|
}
|
|
926
|
-
const storeApi = {};
|
|
927
|
-
for (const [name, store] of Object.entries(storeMap)) {
|
|
928
|
-
const readOrThrow = async (id) => {
|
|
929
|
-
const r = await store.read(id);
|
|
930
|
-
if (!r) throw new SuperLineError("NOT_FOUND", `No resource: ${name}/${id}`);
|
|
931
|
-
return r;
|
|
932
|
-
};
|
|
933
|
-
storeApi[name] = {
|
|
934
|
-
async create(id, data, accessRules) {
|
|
935
|
-
await store.create(id, data, accessRules);
|
|
936
|
-
if (taps.length) emitTap({ type: "store.create", store: name, id });
|
|
937
|
-
},
|
|
938
|
-
read(id) {
|
|
939
|
-
return Promise.resolve(store.read(id));
|
|
940
|
-
},
|
|
941
|
-
async write(id, data, opts2) {
|
|
942
|
-
const origin = opts2?.origin ?? SERVER_ORIGIN;
|
|
943
|
-
await store.apply({ id, update: data, origin });
|
|
944
|
-
if (taps.length) emitTap({ type: "store.write", store: name, id, origin, data });
|
|
945
|
-
},
|
|
946
|
-
async grant(id, principal, perms) {
|
|
947
|
-
const r = await readOrThrow(id);
|
|
948
|
-
await store.setAccess(id, { ...r.accessRules, [principal]: perms });
|
|
949
|
-
if (taps.length) emitTap({ type: "store.grant", store: name, id, principal, perms });
|
|
950
|
-
},
|
|
951
|
-
async revoke(id, principal) {
|
|
952
|
-
const r = await readOrThrow(id);
|
|
953
|
-
const next = { ...r.accessRules };
|
|
954
|
-
delete next[principal];
|
|
955
|
-
await store.setAccess(id, next);
|
|
956
|
-
if (taps.length) emitTap({ type: "store.revoke", store: name, id, principal });
|
|
957
|
-
},
|
|
958
|
-
async delete(id) {
|
|
959
|
-
await store.delete(id);
|
|
960
|
-
if (store.clustering !== "self")
|
|
961
|
-
void adapter.publish(
|
|
962
|
-
STORE + name + ":" + id,
|
|
963
|
-
serializer.encode({ t: "sdel", n: name, id, nd: instanceId })
|
|
964
|
-
);
|
|
965
|
-
if (taps.length) emitTap({ type: "store.delete", store: name, id });
|
|
966
|
-
},
|
|
967
|
-
list(opts2) {
|
|
968
|
-
return Promise.resolve(store.list(opts2));
|
|
969
|
-
},
|
|
970
|
-
searchPrincipals(opts2) {
|
|
971
|
-
return Promise.resolve(store.searchPrincipals(opts2));
|
|
972
|
-
},
|
|
973
|
-
open(id, openOpts) {
|
|
974
|
-
if (!store.open)
|
|
975
|
-
throw new SuperLineError("UNSUPPORTED", `Store ${name} does not support reactive open()`);
|
|
976
|
-
const replica = store.open(id, openOpts);
|
|
977
|
-
if (!taps.length) return replica;
|
|
978
|
-
const origin = openOpts?.origin ?? SERVER_ORIGIN;
|
|
979
|
-
const emit = (data) => emitTap({ type: "store.write", store: name, id, origin, data });
|
|
980
|
-
return {
|
|
981
|
-
...replica,
|
|
982
|
-
set: (d) => {
|
|
983
|
-
replica.set(d);
|
|
984
|
-
emit(d);
|
|
985
|
-
},
|
|
986
|
-
update: (p) => {
|
|
987
|
-
replica.update(p);
|
|
988
|
-
emit(p);
|
|
989
|
-
},
|
|
990
|
-
delete: (path) => {
|
|
991
|
-
replica.delete(path);
|
|
992
|
-
emit({ delete: path });
|
|
993
|
-
}
|
|
994
|
-
};
|
|
995
|
-
}
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
1075
|
const pluginDisposers = [];
|
|
999
1076
|
const pluginHandlers = {};
|
|
1000
1077
|
const api = {
|
|
@@ -1095,9 +1172,67 @@ function createSuperLineServer(contract, opts) {
|
|
|
1095
1172
|
}
|
|
1096
1173
|
};
|
|
1097
1174
|
},
|
|
1098
|
-
|
|
1099
|
-
const
|
|
1100
|
-
if (!
|
|
1175
|
+
collection(name) {
|
|
1176
|
+
const def = collectionDefs[name];
|
|
1177
|
+
if (!def) throw new SuperLineError("NOT_FOUND", `Collection not declared: ${name}`);
|
|
1178
|
+
if (isCrdtCollection(def)) {
|
|
1179
|
+
if (!crdtStore) throw new SuperLineError("NOT_FOUND", "No CRDT collection backend configured");
|
|
1180
|
+
const cstore = crdtStore;
|
|
1181
|
+
const handle2 = {
|
|
1182
|
+
async create(id, data) {
|
|
1183
|
+
const v = await validate(def.schema, data);
|
|
1184
|
+
await cstore.create(name, id, v, def.crdt);
|
|
1185
|
+
},
|
|
1186
|
+
open(id, o) {
|
|
1187
|
+
return cstore.open(name, id, { ...o, doc: def.crdt });
|
|
1188
|
+
},
|
|
1189
|
+
async read(id) {
|
|
1190
|
+
const state = await cstore.read(name, id);
|
|
1191
|
+
if (state === void 0) return void 0;
|
|
1192
|
+
const r = cstore.open(name, id, { doc: def.crdt });
|
|
1193
|
+
const s = r.getSnapshot();
|
|
1194
|
+
r.close();
|
|
1195
|
+
return s;
|
|
1196
|
+
},
|
|
1197
|
+
async delete(id) {
|
|
1198
|
+
await cstore.delete(name, id);
|
|
1199
|
+
if (cstore.clustering !== "self")
|
|
1200
|
+
void adapter.publish(CDOC + name + ":" + id, serializer.encode({ t: "cddel", n: name, id, nd: instanceId }));
|
|
1201
|
+
},
|
|
1202
|
+
list(o) {
|
|
1203
|
+
return Promise.resolve(cstore.list(name, o));
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
return handle2;
|
|
1207
|
+
}
|
|
1208
|
+
if (!collectionStore) throw new SuperLineError("NOT_FOUND", "No collection backend configured");
|
|
1209
|
+
const store = collectionStore;
|
|
1210
|
+
const resolve = async (row) => {
|
|
1211
|
+
const v = await validate(def.schema, row);
|
|
1212
|
+
const key = v[def.key];
|
|
1213
|
+
if (typeof key !== "string")
|
|
1214
|
+
throw new SuperLineError("VALIDATION", `Collection ${name} row is missing string key '${def.key}'`);
|
|
1215
|
+
return { id: key, row: v };
|
|
1216
|
+
};
|
|
1217
|
+
const handle = {
|
|
1218
|
+
async insert(row) {
|
|
1219
|
+
const { id, row: v } = await resolve(row);
|
|
1220
|
+
await commitCollectionBatch([{ op: "insert", n: name, id, row: v }], SERVER_ORIGIN, true);
|
|
1221
|
+
},
|
|
1222
|
+
async update(row) {
|
|
1223
|
+
const { id, row: v } = await resolve(row);
|
|
1224
|
+
await commitCollectionBatch([{ op: "update", n: name, id, row: v }], SERVER_ORIGIN, true);
|
|
1225
|
+
},
|
|
1226
|
+
async delete(id) {
|
|
1227
|
+
await commitCollectionBatch([{ op: "delete", n: name, id }], SERVER_ORIGIN, true);
|
|
1228
|
+
},
|
|
1229
|
+
read(id) {
|
|
1230
|
+
return Promise.resolve(store.read(name, id));
|
|
1231
|
+
},
|
|
1232
|
+
snapshot(query) {
|
|
1233
|
+
return Promise.resolve(store.snapshot(name, query ?? {}));
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1101
1236
|
return handle;
|
|
1102
1237
|
},
|
|
1103
1238
|
async close() {
|
|
@@ -1156,8 +1291,14 @@ function createSuperLineServer(contract, opts) {
|
|
|
1156
1291
|
}
|
|
1157
1292
|
};
|
|
1158
1293
|
},
|
|
1159
|
-
|
|
1160
|
-
|
|
1294
|
+
collection: (name) => api.collection(name),
|
|
1295
|
+
collectionInfos: () => (
|
|
1296
|
+
// CRDT document collections surface with a synthetic `id` key + no references — the inspector's
|
|
1297
|
+
// queryCollection synthesizes doc-rows for them (they're open-by-id, not row-queryable).
|
|
1298
|
+
Object.entries(collectionDefs).map(
|
|
1299
|
+
([name, def]) => isCrdtCollection(def) ? { name, key: "id", references: {} } : { name, key: def.key, references: def.references ?? {} }
|
|
1300
|
+
)
|
|
1301
|
+
),
|
|
1161
1302
|
describe: (conn) => buildDescriptor(conn),
|
|
1162
1303
|
connectionById: (id) => Promise.resolve(presenceOrThrow().get(id)),
|
|
1163
1304
|
channel: (name) => pluginChannel(pluginName, name)
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@super-line/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Realtime data-bus server for super-line — requests, events, rooms, topics,
|
|
5
|
+
"description": "Realtime data-bus server for super-line — requests, events, rooms, topics, collections, pluggable transports & adapters.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Mert",
|
|
8
8
|
"keywords": [
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@super-line/core": "^0.
|
|
50
|
+
"@super-line/core": "^0.11.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@chainsafe/libp2p-noise": "^17.0.0",
|
|
@@ -68,15 +68,15 @@
|
|
|
68
68
|
"zeromq": "^6.0.0",
|
|
69
69
|
"zod": "^3.24.1",
|
|
70
70
|
"zod-to-json-schema": "^3.25.2",
|
|
71
|
-
"@super-line/client": "0.8.0",
|
|
72
71
|
"@super-line/adapter-zeromq": "0.5.0",
|
|
73
|
-
"@super-line/adapter-rabbitmq": "0.5.0",
|
|
74
72
|
"@super-line/adapter-redis": "0.5.0",
|
|
75
|
-
"@super-line/
|
|
76
|
-
"@super-line/
|
|
77
|
-
"@super-line/
|
|
73
|
+
"@super-line/adapter-libp2p": "0.6.0",
|
|
74
|
+
"@super-line/client": "0.9.0",
|
|
75
|
+
"@super-line/plugin-inspector": "0.2.0",
|
|
76
|
+
"@super-line/adapter-rabbitmq": "0.5.0",
|
|
78
77
|
"@super-line/transport-loopback": "0.5.0",
|
|
79
|
-
"@super-line/
|
|
78
|
+
"@super-line/transport-http": "0.5.0",
|
|
79
|
+
"@super-line/transport-websocket": "0.6.0"
|
|
80
80
|
},
|
|
81
81
|
"optionalDependencies": {
|
|
82
82
|
"@standard-community/standard-json": "^0.3.5"
|