@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 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-libsql`, `store-sync-pglite`) and the LWW stores both back it.
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 STORE = "s:";
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.stores) continue;
223
- for (const [name, store] of Object.entries(p.stores)) {
224
- if (name in storeMap)
225
- throw new Error(`Plugin '${p.name}' store '${name}' collides with an existing store of that name`);
226
- storeMap[name] = store;
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(STORE)) {
323
- handleStoreRelay(channel, payload);
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 === "sopen") await handleStoreOpen(conn, frame);
604
- else if (frame.t === "srd") await handleStoreRead(conn, frame);
605
- else if (frame.t === "swr") await handleStoreWrite(conn, frame);
606
- else if (frame.t === "sclose") handleStoreClose(conn, frame);
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 storeOrErr(conn, id, name) {
700
- const store = storeMap[name];
701
- if (!store) conn.send({ t: "err", i: id, code: "NOT_FOUND", m: `Unknown store: ${name}` });
702
- return store;
703
- }
704
- async function handleStoreOpen(conn, frame) {
705
- const store = storeOrErr(conn, frame.i, frame.n);
706
- if (!store) return;
707
- await dispatchOp(conn, frame.i, { kind: "subscribe", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
708
- const resource = await store.read(frame.id);
709
- if (!resource) throw new import_core.SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
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
- if (!resource.accessRules[principal]?.read)
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, STORE + frame.n + ":" + frame.id);
714
- if (taps.length) emitTap({ type: "store.subscribe", connId: conn.id, store: frame.n, id: frame.id });
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 handleStoreRead(conn, frame) {
719
- const store = storeOrErr(conn, frame.i, frame.n);
720
- if (!store) return;
721
- await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
722
- const resource = await store.read(frame.id);
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
- if (!resource.accessRules[principal]?.read)
726
- throw new import_core.SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
727
- conn.send({ t: "res", i: frame.i, d: resource.data });
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({ id: frame.id, update: frame.u, origin: frame.o });
740
- if (taps.length)
741
- emitTap({
742
- type: "store.write",
743
- store: frame.n,
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 handleStoreClose(conn, frame) {
753
- if (!storeMap[frame.n]) return;
754
- leaveChannel(conn, STORE + frame.n + ":" + frame.id);
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
- for (const [name, store] of Object.entries(storeMap)) {
758
- const isSelf = store.clustering === "self";
759
- store.onChange((change) => {
760
- const channel = STORE + name + ":" + change.id;
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: "sch", n: name, id: change.id, u: change.update, o: change.origin });
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: "sch", n: name, id: change.id, u: change.update, o: change.origin, nd: instanceId })
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
- store.onDelete?.((id) => {
776
- const set = members.get(STORE + name + ":" + id);
786
+ crdtStore.onDelete?.((n, id) => {
787
+ const set = members.get(CDOC + n + ":" + id);
777
788
  if (!set) return;
778
- const payload = serializer.encode({ t: "sdel", n: name, id });
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 handleStoreRelay(channel, payload) {
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
- const store = storeMap[frame.n];
793
- if (!store || store.clustering !== "relay") return;
794
- if (frame.t === "sdel") {
795
- void store.delete(frame.id);
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 store.apply({ id: frame.id, update: frame.u, origin: frame.o });
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
- store(name) {
1123
- const handle = storeApi[name];
1124
- if (!handle) throw new import_core.SuperLineError("NOT_FOUND", `Store not configured: ${name}`);
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
- store: (name) => api.store(name),
1184
- storeInfos: () => Object.entries(storeMap).map(([name, store]) => ({ name, model: store.model })),
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)