@super-line/server 0.10.2 → 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 +108 -162
- package/dist/index.d.cts +50 -65
- package/dist/index.d.ts +50 -65
- package/dist/index.js +111 -163
- package/package.json +8 -8
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,18 @@ 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
|
}
|
|
221
|
-
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;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
220
|
const collectionStore = opts.collections;
|
|
230
221
|
const collectionDefs = c.collections ?? {};
|
|
231
222
|
const collectionPolicies = { ...opts.policies };
|
|
@@ -243,6 +234,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
243
234
|
}
|
|
244
235
|
const checkReferences = opts.checkReferences ?? false;
|
|
245
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
|
+
};
|
|
246
242
|
const COLL_CHANNEL = "cbatch";
|
|
247
243
|
const collSubscribers = /* @__PURE__ */ new Map();
|
|
248
244
|
const connColl = /* @__PURE__ */ new Map();
|
|
@@ -339,8 +335,8 @@ function createSuperLineServer(contract, opts) {
|
|
|
339
335
|
handlePersonal(channel, payload);
|
|
340
336
|
return;
|
|
341
337
|
}
|
|
342
|
-
if (channel.startsWith(
|
|
343
|
-
|
|
338
|
+
if (channel.startsWith(CDOC)) {
|
|
339
|
+
handleCrdtRelay(channel, payload);
|
|
344
340
|
return;
|
|
345
341
|
}
|
|
346
342
|
if (channel === COLL_CHANNEL) {
|
|
@@ -626,13 +622,12 @@ function createSuperLineServer(contract, opts) {
|
|
|
626
622
|
else if (frame.t === "unsub") {
|
|
627
623
|
const ns = topicNamespace(conn.role, frame.c);
|
|
628
624
|
if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
|
|
629
|
-
} else if (frame.t === "
|
|
630
|
-
else if (frame.t === "srd") await handleStoreRead(conn, frame);
|
|
631
|
-
else if (frame.t === "swr") await handleStoreWrite(conn, frame);
|
|
632
|
-
else if (frame.t === "sclose") handleStoreClose(conn, frame);
|
|
633
|
-
else if (frame.t === "csub") await handleCollectionSub(conn, frame);
|
|
625
|
+
} else if (frame.t === "csub") await handleCollectionSub(conn, frame);
|
|
634
626
|
else if (frame.t === "cuns") handleCollectionUnsub(conn, frame);
|
|
635
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);
|
|
636
631
|
else if (frame.t === "sres") {
|
|
637
632
|
await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
|
|
638
633
|
} else if (frame.t === "serr") {
|
|
@@ -725,90 +720,77 @@ function createSuperLineServer(contract, opts) {
|
|
|
725
720
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
726
721
|
});
|
|
727
722
|
}
|
|
728
|
-
function
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
return
|
|
732
|
-
}
|
|
733
|
-
async function
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
await dispatchOp(conn, frame.i, { kind: "subscribe", name: `
|
|
737
|
-
const
|
|
738
|
-
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}`);
|
|
739
734
|
const principal = conn.principal ?? conn.id;
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
async function handleStoreRead(conn, frame) {
|
|
748
|
-
const store = storeOrErr(conn, frame.i, frame.n);
|
|
749
|
-
if (!store) return;
|
|
750
|
-
await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
|
|
751
|
-
const resource = await store.read(frame.id);
|
|
752
|
-
if (!resource) throw new import_core.SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
753
|
-
const principal = conn.principal ?? conn.id;
|
|
754
|
-
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))
|
|
755
741
|
throw new import_core.SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
|
|
756
|
-
conn.
|
|
742
|
+
await joinChannel(conn, CDOC + frame.n + ":" + frame.id);
|
|
743
|
+
conn.send({ t: "res", i: frame.i, d: state });
|
|
757
744
|
});
|
|
758
745
|
}
|
|
759
|
-
async function
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
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 () => {
|
|
765
751
|
const principal = conn.principal ?? conn.id;
|
|
766
|
-
|
|
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))
|
|
767
755
|
throw new import_core.SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
|
|
768
|
-
await store.apply(
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
id: frame.id,
|
|
774
|
-
origin: frame.o,
|
|
775
|
-
connId: conn.id,
|
|
776
|
-
data: frame.u
|
|
777
|
-
});
|
|
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
|
+
);
|
|
778
761
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
779
762
|
});
|
|
780
763
|
}
|
|
781
|
-
function
|
|
782
|
-
if (!
|
|
783
|
-
leaveChannel(conn,
|
|
784
|
-
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);
|
|
785
767
|
}
|
|
786
|
-
|
|
787
|
-
const isSelf =
|
|
788
|
-
|
|
789
|
-
const channel =
|
|
768
|
+
if (crdtStore) {
|
|
769
|
+
const isSelf = crdtStore.clustering === "self";
|
|
770
|
+
crdtStore.onChange((change) => {
|
|
771
|
+
const channel = CDOC + change.n + ":" + change.id;
|
|
790
772
|
if (isSelf) {
|
|
791
773
|
const set = members.get(channel);
|
|
792
774
|
if (!set) return;
|
|
793
|
-
const payload = serializer.encode({ t: "
|
|
775
|
+
const payload = serializer.encode({ t: "cdchg", n: change.n, id: change.id, u: change.update, o: change.origin });
|
|
794
776
|
for (const conn of set) conn.sendRaw(payload);
|
|
795
777
|
return;
|
|
796
778
|
}
|
|
797
779
|
if (relaying) return;
|
|
798
780
|
void adapter.publish(
|
|
799
781
|
channel,
|
|
800
|
-
serializer.encode({ t: "
|
|
782
|
+
serializer.encode({ t: "cdchg", n: change.n, id: change.id, u: change.update, o: change.origin, nd: instanceId })
|
|
801
783
|
);
|
|
802
784
|
});
|
|
803
785
|
if (isSelf)
|
|
804
|
-
|
|
805
|
-
const set = members.get(
|
|
786
|
+
crdtStore.onDelete?.((n, id) => {
|
|
787
|
+
const set = members.get(CDOC + n + ":" + id);
|
|
806
788
|
if (!set) return;
|
|
807
|
-
const payload = serializer.encode({ t: "
|
|
789
|
+
const payload = serializer.encode({ t: "cddel", n, id });
|
|
808
790
|
for (const conn of set) conn.sendRaw(payload);
|
|
809
791
|
});
|
|
810
792
|
}
|
|
811
|
-
function
|
|
793
|
+
function handleCrdtRelay(channel, payload) {
|
|
812
794
|
const set = members.get(channel);
|
|
813
795
|
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
814
796
|
let frame;
|
|
@@ -818,15 +800,20 @@ function createSuperLineServer(contract, opts) {
|
|
|
818
800
|
return;
|
|
819
801
|
}
|
|
820
802
|
if (frame.nd === instanceId) return;
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
if (
|
|
824
|
-
|
|
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
|
+
}
|
|
825
811
|
return;
|
|
826
812
|
}
|
|
827
813
|
relaying = true;
|
|
828
814
|
try {
|
|
829
|
-
void
|
|
815
|
+
void crdtStore.apply({ n: frame.n, id: frame.id, update: frame.u, origin: frame.o }, def.crdt, () => {
|
|
816
|
+
});
|
|
830
817
|
} catch {
|
|
831
818
|
} finally {
|
|
832
819
|
relaying = false;
|
|
@@ -882,6 +869,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
882
869
|
for (const op of ops) {
|
|
883
870
|
const def = collectionDefs[op.n];
|
|
884
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`);
|
|
885
873
|
const policy = collectionPolicies[op.n];
|
|
886
874
|
if (!policy?.write) throw new import_core.SuperLineError("FORBIDDEN", `Write denied: ${op.n}`);
|
|
887
875
|
const prev = await store.read(op.n, op.id);
|
|
@@ -1103,78 +1091,6 @@ function createSuperLineServer(contract, opts) {
|
|
|
1103
1091
|
void adapter.publish(CONN + id, serializer.encode(env));
|
|
1104
1092
|
});
|
|
1105
1093
|
}
|
|
1106
|
-
const storeApi = {};
|
|
1107
|
-
for (const [name, store] of Object.entries(storeMap)) {
|
|
1108
|
-
const readOrThrow = async (id) => {
|
|
1109
|
-
const r = await store.read(id);
|
|
1110
|
-
if (!r) throw new import_core.SuperLineError("NOT_FOUND", `No resource: ${name}/${id}`);
|
|
1111
|
-
return r;
|
|
1112
|
-
};
|
|
1113
|
-
storeApi[name] = {
|
|
1114
|
-
async create(id, data, accessRules) {
|
|
1115
|
-
await store.create(id, data, accessRules);
|
|
1116
|
-
if (taps.length) emitTap({ type: "store.create", store: name, id });
|
|
1117
|
-
},
|
|
1118
|
-
read(id) {
|
|
1119
|
-
return Promise.resolve(store.read(id));
|
|
1120
|
-
},
|
|
1121
|
-
async write(id, data, opts2) {
|
|
1122
|
-
const origin = opts2?.origin ?? SERVER_ORIGIN;
|
|
1123
|
-
await store.apply({ id, update: data, origin });
|
|
1124
|
-
if (taps.length) emitTap({ type: "store.write", store: name, id, origin, data });
|
|
1125
|
-
},
|
|
1126
|
-
async grant(id, principal, perms) {
|
|
1127
|
-
const r = await readOrThrow(id);
|
|
1128
|
-
await store.setAccess(id, { ...r.accessRules, [principal]: perms });
|
|
1129
|
-
if (taps.length) emitTap({ type: "store.grant", store: name, id, principal, perms });
|
|
1130
|
-
},
|
|
1131
|
-
async revoke(id, principal) {
|
|
1132
|
-
const r = await readOrThrow(id);
|
|
1133
|
-
const next = { ...r.accessRules };
|
|
1134
|
-
delete next[principal];
|
|
1135
|
-
await store.setAccess(id, next);
|
|
1136
|
-
if (taps.length) emitTap({ type: "store.revoke", store: name, id, principal });
|
|
1137
|
-
},
|
|
1138
|
-
async delete(id) {
|
|
1139
|
-
await store.delete(id);
|
|
1140
|
-
if (store.clustering !== "self")
|
|
1141
|
-
void adapter.publish(
|
|
1142
|
-
STORE + name + ":" + id,
|
|
1143
|
-
serializer.encode({ t: "sdel", n: name, id, nd: instanceId })
|
|
1144
|
-
);
|
|
1145
|
-
if (taps.length) emitTap({ type: "store.delete", store: name, id });
|
|
1146
|
-
},
|
|
1147
|
-
list(opts2) {
|
|
1148
|
-
return Promise.resolve(store.list(opts2));
|
|
1149
|
-
},
|
|
1150
|
-
searchPrincipals(opts2) {
|
|
1151
|
-
return Promise.resolve(store.searchPrincipals(opts2));
|
|
1152
|
-
},
|
|
1153
|
-
open(id, openOpts) {
|
|
1154
|
-
if (!store.open)
|
|
1155
|
-
throw new import_core.SuperLineError("UNSUPPORTED", `Store ${name} does not support reactive open()`);
|
|
1156
|
-
const replica = store.open(id, openOpts);
|
|
1157
|
-
if (!taps.length) return replica;
|
|
1158
|
-
const origin = openOpts?.origin ?? SERVER_ORIGIN;
|
|
1159
|
-
const emit = (data) => emitTap({ type: "store.write", store: name, id, origin, data });
|
|
1160
|
-
return {
|
|
1161
|
-
...replica,
|
|
1162
|
-
set: (d) => {
|
|
1163
|
-
replica.set(d);
|
|
1164
|
-
emit(d);
|
|
1165
|
-
},
|
|
1166
|
-
update: (p) => {
|
|
1167
|
-
replica.update(p);
|
|
1168
|
-
emit(p);
|
|
1169
|
-
},
|
|
1170
|
-
delete: (path) => {
|
|
1171
|
-
replica.delete(path);
|
|
1172
|
-
emit({ delete: path });
|
|
1173
|
-
}
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
1094
|
const pluginDisposers = [];
|
|
1179
1095
|
const pluginHandlers = {};
|
|
1180
1096
|
const api = {
|
|
@@ -1275,15 +1191,40 @@ function createSuperLineServer(contract, opts) {
|
|
|
1275
1191
|
}
|
|
1276
1192
|
};
|
|
1277
1193
|
},
|
|
1278
|
-
store(name) {
|
|
1279
|
-
const handle = storeApi[name];
|
|
1280
|
-
if (!handle) throw new import_core.SuperLineError("NOT_FOUND", `Store not configured: ${name}`);
|
|
1281
|
-
return handle;
|
|
1282
|
-
},
|
|
1283
1194
|
collection(name) {
|
|
1284
|
-
if (!collectionStore) throw new import_core.SuperLineError("NOT_FOUND", "No collection backend configured");
|
|
1285
1195
|
const def = collectionDefs[name];
|
|
1286
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");
|
|
1287
1228
|
const store = collectionStore;
|
|
1288
1229
|
const resolve = async (row) => {
|
|
1289
1230
|
const v = await (0, import_core.validate)(def.schema, row);
|
|
@@ -1292,7 +1233,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
1292
1233
|
throw new import_core.SuperLineError("VALIDATION", `Collection ${name} row is missing string key '${def.key}'`);
|
|
1293
1234
|
return { id: key, row: v };
|
|
1294
1235
|
};
|
|
1295
|
-
|
|
1236
|
+
const handle = {
|
|
1296
1237
|
async insert(row) {
|
|
1297
1238
|
const { id, row: v } = await resolve(row);
|
|
1298
1239
|
await commitCollectionBatch([{ op: "insert", n: name, id, row: v }], SERVER_ORIGIN, true);
|
|
@@ -1311,6 +1252,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
1311
1252
|
return Promise.resolve(store.snapshot(name, query ?? {}));
|
|
1312
1253
|
}
|
|
1313
1254
|
};
|
|
1255
|
+
return handle;
|
|
1314
1256
|
},
|
|
1315
1257
|
async close() {
|
|
1316
1258
|
if (closing) return;
|
|
@@ -1368,10 +1310,14 @@ function createSuperLineServer(contract, opts) {
|
|
|
1368
1310
|
}
|
|
1369
1311
|
};
|
|
1370
1312
|
},
|
|
1371
|
-
store: (name) => api.store(name),
|
|
1372
|
-
storeInfos: () => Object.entries(storeMap).map(([name, store]) => ({ name, model: store.model })),
|
|
1373
1313
|
collection: (name) => api.collection(name),
|
|
1374
|
-
collectionInfos: () =>
|
|
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
|
+
),
|
|
1375
1321
|
describe: (conn) => buildDescriptor(conn),
|
|
1376
1322
|
connectionById: (id) => Promise.resolve(presenceOrThrow().get(id)),
|
|
1377
1323
|
channel: (name) => pluginChannel(pluginName, name)
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, Expr, CollectionQuery, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, Directional, TapEvent,
|
|
1
|
+
import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, Expr, CollectionQuery, CrdtServerReplica, DocListOpts, DocSummary, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, Directional, TapEvent, CtsOf, ServerInput, Handshake, SharedRequests, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData, CollectionName, CrdtCollectionName, DocOf, RowOf, ServerTransport, CollectionStore, CrdtCollectionStore } from '@super-line/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A single client connection, passed to handlers as the third argument.
|
|
@@ -104,6 +104,36 @@ interface ServerCollectionHandle<Row = unknown> {
|
|
|
104
104
|
/** Materialize a snapshot (filter/sort/limit); omit the query for the whole collection. Server-side, policy-free. */
|
|
105
105
|
snapshot(query?: CollectionQuery): Promise<Row[]>;
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Access policy for a CRDT document collection (ADR-0007, Q7). Guard-shaped — not the RLS filter shape of
|
|
109
|
+
* {@link CollectionPolicy}, because CRDT collections are opened by id (no subset to filter). **Deny-by-default**.
|
|
110
|
+
* Server co-writes via `srv.collection(n)` bypass both.
|
|
111
|
+
*/
|
|
112
|
+
interface CrdtCollectionPolicy<Ctx = unknown, Doc = unknown> {
|
|
113
|
+
/** May this principal open `id`? Gets the post-merge plaintext `snapshot` (content-based auth like RLS reading a row). */
|
|
114
|
+
read?: (principal: string, id: string, snapshot: Doc | undefined, ctx: Ctx) => Awaitable$1<boolean>;
|
|
115
|
+
/** May this principal write to `id`? (Creation is server-authoritative — Q10 — so there is no `create` op here.) */
|
|
116
|
+
write?: (principal: string, id: string, ctx: Ctx) => Awaitable$1<boolean>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Server-authoritative handle for a CRDT document collection (`srv.collection('scenes')`). Creation is
|
|
120
|
+
* server-only (Q10): `create` validates the initial doc and materializes it; `open` returns a reactive
|
|
121
|
+
* co-writer whose mutations fan out like a relayed client delta. Policy-free (server is authoritative).
|
|
122
|
+
*/
|
|
123
|
+
interface ServerCrdtCollectionHandle<Doc = unknown> {
|
|
124
|
+
/** Create a document with pre-validated initial data. Throws `CONFLICT` if the id exists. */
|
|
125
|
+
create(id: string, data: Doc): Promise<void>;
|
|
126
|
+
/** Open a reactive co-writer over an existing document. `origin` (default `"server"`) attributes its writes. */
|
|
127
|
+
open(id: string, opts?: {
|
|
128
|
+
origin?: string;
|
|
129
|
+
}): CrdtServerReplica;
|
|
130
|
+
/** Read the current plaintext snapshot of a document, or undefined if absent. */
|
|
131
|
+
read(id: string): Promise<Doc | undefined>;
|
|
132
|
+
/** Delete a document (idempotent), fanning the deletion cluster-wide. */
|
|
133
|
+
delete(id: string): Promise<void>;
|
|
134
|
+
/** Enumerate document ids + summaries (no content query). */
|
|
135
|
+
list(opts?: DocListOpts): Promise<DocSummary[]>;
|
|
136
|
+
}
|
|
107
137
|
|
|
108
138
|
/**
|
|
109
139
|
* In-process pub/sub bus. Share one bus across multiple servers to simulate
|
|
@@ -266,46 +296,6 @@ interface RoleLens<C extends Contract, R extends RoleOf<C>> {
|
|
|
266
296
|
/** Publish to a topic in role `R`'s surface (reaches that role's subscribers). */
|
|
267
297
|
publish<T extends keyof RoleTopics<C, R>>(topic: T, data: EmitData<RoleTopics<C, R>[T]>): void;
|
|
268
298
|
}
|
|
269
|
-
/**
|
|
270
|
-
* Server-side handle for one configured Store, reached via `srv.store.<name>`. The server is
|
|
271
|
-
* authoritative: it creates Resources, grants/revokes access, and may co-write. `data` is untyped
|
|
272
|
-
* (stores are off-contract — see ADR-0003); callers assert the shape.
|
|
273
|
-
*/
|
|
274
|
-
interface ServerStoreHandle {
|
|
275
|
-
/** Create a Resource with initial data + access rules (deny-by-default for everyone unlisted). */
|
|
276
|
-
create(id: string, data: unknown, accessRules: AccessRules): Promise<void>;
|
|
277
|
-
/** Read a Resource (data + accessRules), or undefined if absent. */
|
|
278
|
-
read(id: string): Promise<Resource | undefined>;
|
|
279
|
-
/**
|
|
280
|
-
* Server co-write: replace the Resource's value (LWW), fanned out to subscribers stamped with `origin`
|
|
281
|
-
* (default `"server"`) for echo-break + inspector attribution — mirrors {@link ServerStore.open}'s origin.
|
|
282
|
-
*/
|
|
283
|
-
write(id: string, data: unknown, opts?: {
|
|
284
|
-
origin?: string;
|
|
285
|
-
}): Promise<void>;
|
|
286
|
-
/** Grant a principal read/write on a Resource. */
|
|
287
|
-
grant(id: string, principal: string, perms: Perms): Promise<void>;
|
|
288
|
-
/** Revoke a principal's access to a Resource entirely. */
|
|
289
|
-
revoke(id: string, principal: string): Promise<void>;
|
|
290
|
-
/** Delete a Resource. */
|
|
291
|
-
delete(id: string): Promise<void>;
|
|
292
|
-
/**
|
|
293
|
-
* Summaries of this store's Resources, server-side filtered / sorted / paginated per {@link ListOpts}
|
|
294
|
-
* (omit `opts` for every Resource, id-ascending). Forwards to {@link ServerStore.list}.
|
|
295
|
-
*/
|
|
296
|
-
list(opts?: ListOpts): Promise<ResourceSummary[]>;
|
|
297
|
-
/** Distinct principals granted anywhere in this store (store-global). Forwards to {@link ServerStore.searchPrincipals}. */
|
|
298
|
-
searchPrincipals(opts: SearchOpts): Promise<string[]>;
|
|
299
|
-
/**
|
|
300
|
-
* Open a reactive in-process co-writer over a Resource's canonical state — the server half's mirror of
|
|
301
|
-
* `client.store(name).open(id)`. Reactive reads, merging `update`, and surgical `delete(path)` (the only
|
|
302
|
-
* way to remove a key server-side), all server-authoritative and fanned out to subscribers. `origin`
|
|
303
|
-
* (default `"server"`) tags its Changes for inspector attribution. Throws if the store has no `open`.
|
|
304
|
-
*/
|
|
305
|
-
open(id: string, opts?: {
|
|
306
|
-
origin?: string;
|
|
307
|
-
}): ServerReplica;
|
|
308
|
-
}
|
|
309
299
|
/**
|
|
310
300
|
* Handlers a plugin provides for its paired surface `S` (ADR-0004): one per `clientToServer` key,
|
|
311
301
|
* typed from `S`. `ctx`/`conn` are loose — a plugin is written independently of the host's per-role ctx.
|
|
@@ -357,11 +347,6 @@ interface SuperLinePlugin<S extends Directional = {}> {
|
|
|
357
347
|
* throws at construction.
|
|
358
348
|
*/
|
|
359
349
|
handlers?: (ctx: PluginContext) => HandlersFor<S>;
|
|
360
|
-
/**
|
|
361
|
-
* Server halves of Store pairs the plugin contributes, merged into the host's `stores` and reachable
|
|
362
|
-
* via `srv.store(name)`. A name colliding with a host store or another plugin's store throws at construction.
|
|
363
|
-
*/
|
|
364
|
-
stores?: Record<string, ServerStore>;
|
|
365
350
|
/**
|
|
366
351
|
* Row-security policies for the collections the plugin's contract fragment declares (see {@link CollectionPolicy}).
|
|
367
352
|
* Merged into the host's `policies`; a collection already policied by the host or another plugin throws at
|
|
@@ -457,11 +442,7 @@ interface PluginContext {
|
|
|
457
442
|
readonly size: number;
|
|
458
443
|
readonly connections: readonly Conn[];
|
|
459
444
|
};
|
|
460
|
-
/** Server-authoritative handle for a
|
|
461
|
-
store(name: string): ServerStoreHandle;
|
|
462
|
-
/** Configured stores (host + plugin) and their models. */
|
|
463
|
-
storeInfos(): StoreInfo[];
|
|
464
|
-
/** Server-authoritative handle for a contract collection (loosely typed here); throws if none is configured. */
|
|
445
|
+
/** Server-authoritative handle for a contract collection (loosely typed here as the LWW row handle — the surface plugins/inspector use); throws if none is configured. */
|
|
465
446
|
collection(name: string): ServerCollectionHandle;
|
|
466
447
|
/** Declared collections (name + key + advisory references) for the schema graph. */
|
|
467
448
|
collectionInfos(): {
|
|
@@ -511,12 +492,6 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>, P
|
|
|
511
492
|
interval?: number;
|
|
512
493
|
maxMissed?: number;
|
|
513
494
|
} | false;
|
|
514
|
-
/**
|
|
515
|
-
* Pluggable persisted-state Stores, keyed by name (`{ scene: crdtStoreServer(), config: memoryStoreServer() }`).
|
|
516
|
-
* Each is the server half of a Store pair; the client passes the matching client halves. Surfaced as
|
|
517
|
-
* `srv.store.<name>` and `client.store.<name>`. Stores are off-contract and untyped (ADR-0003).
|
|
518
|
-
*/
|
|
519
|
-
stores?: Record<string, ServerStore>;
|
|
520
495
|
/**
|
|
521
496
|
* The single {@link CollectionStore} backend serving every collection this contract declares (typed rows;
|
|
522
497
|
* ADR-0006), e.g. `collections: memoryCollections()`. One backend ⇒ one transaction domain, so a
|
|
@@ -524,11 +499,18 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>, P
|
|
|
524
499
|
*/
|
|
525
500
|
collections?: CollectionStore;
|
|
526
501
|
/**
|
|
527
|
-
*
|
|
528
|
-
*
|
|
502
|
+
* The {@link CrdtCollectionStore} backend serving this contract's CRDT document collections (ADR-0007),
|
|
503
|
+
* e.g. `crdtCollections: crdtMemoryCollections()`. A backend per family: CRDT docs never join a
|
|
504
|
+
* cross-collection atomic batch, so they get their own backend, separate from `collections`.
|
|
505
|
+
*/
|
|
506
|
+
crdtCollections?: CrdtCollectionStore;
|
|
507
|
+
/**
|
|
508
|
+
* Access policies per collection. Deny-by-default: a collection with no policy is server-only. LWW row
|
|
509
|
+
* collections take an RLS-style {@link CollectionPolicy} (read → filter); CRDT document collections take a
|
|
510
|
+
* guard-shaped {@link CrdtCollectionPolicy} (read → bool). Server co-writes via `srv.collection(n)` bypass them.
|
|
529
511
|
*/
|
|
530
512
|
policies?: {
|
|
531
|
-
[N in CollectionName<C>]?: CollectionPolicy<CtxUnion<A>, RowOf<C, N>>;
|
|
513
|
+
[N in CollectionName<C>]?: N extends CrdtCollectionName<C> ? CrdtCollectionPolicy<CtxUnion<A>, DocOf<C, N>> : CollectionPolicy<CtxUnion<A>, RowOf<C, N>>;
|
|
532
514
|
};
|
|
533
515
|
/**
|
|
534
516
|
* Opt-in advisory foreign-key checks (see ADR-0006, decision 8). When true, an insert/update whose
|
|
@@ -575,10 +557,13 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extend
|
|
|
575
557
|
subscribe<T extends keyof SharedTopics<C>>(topic: T, handler: (data: EventData<SharedTopics<C>[T]>, meta: BusMeta) => void): () => void;
|
|
576
558
|
/** Lens for role-scoped sends, e.g. forRole('user').publish('feed', data). */
|
|
577
559
|
forRole<R extends RoleOf<C>>(role: R): RoleLens<C, R>;
|
|
578
|
-
/**
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
560
|
+
/**
|
|
561
|
+
* Server-authoritative handle for a contract collection, typed by the contract: an LWW row collection gives
|
|
562
|
+
* `ServerCollectionHandle` (`insert`/`update`/`batch`), a CRDT document collection gives
|
|
563
|
+
* `ServerCrdtCollectionHandle` (`create`/`open`). Throws `NOT_FOUND` if no matching backend is configured
|
|
564
|
+
* or the name isn't declared.
|
|
565
|
+
*/
|
|
566
|
+
collection<N extends CollectionName<C>>(name: N): N extends CrdtCollectionName<C> ? ServerCrdtCollectionHandle<DocOf<C, N>> : ServerCollectionHandle<RowOf<C, N>>;
|
|
582
567
|
close(): Promise<void>;
|
|
583
568
|
}
|
|
584
569
|
/**
|
|
@@ -605,4 +590,4 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extend
|
|
|
605
590
|
*/
|
|
606
591
|
declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>, const P extends readonly SuperLinePlugin<any>[] = []>(contract: C, opts: SuperLineServerOptions<C, A, P>): SuperLineServer<C, A, HandledKeys<P>>;
|
|
607
592
|
|
|
608
|
-
export { type AuthResult, type BusMeta, type ClusterView, type CollectionPolicy, Conn, type ConnTarget, type HandledKeys, type Handlers, type HandlersFor, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type PluginChannel, type PluginConnection, type PluginContext, type PluginMiddleware, type RoleLens, type Room, type ServerCollectionHandle, type
|
|
593
|
+
export { type AuthResult, type BusMeta, type ClusterView, type CollectionPolicy, Conn, type ConnTarget, type CrdtCollectionPolicy, type HandledKeys, type Handlers, type HandlersFor, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type PluginChannel, type PluginConnection, type PluginContext, type PluginMiddleware, type RoleLens, type Room, type ServerCollectionHandle, type ServerCrdtCollectionHandle, type SubtractHandlers, type SuperLinePlugin, type SuperLineServer, type SuperLineServerOptions, type UserTarget, type WriteOp, createInMemoryAdapter, createSuperLineServer, resolvePrincipal };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, Expr, CollectionQuery, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, Directional, TapEvent,
|
|
1
|
+
import { ServerMessageDef, RawConn, Serializer, ServerFrame, EmitData, Expr, CollectionQuery, CrdtServerReplica, DocListOpts, DocSummary, ConnDescriptor, Adapter, PresenceStore, Contract, RoleOf, NodeStat, SharedEvents, SharedServerRequests, ClientInput, Output, Directional, TapEvent, CtsOf, ServerInput, Handshake, SharedRequests, AnyData, RoleRequests, Events, DataOf, RoleTopics, SharedTopics, EventData, CollectionName, CrdtCollectionName, DocOf, RowOf, ServerTransport, CollectionStore, CrdtCollectionStore } from '@super-line/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A single client connection, passed to handlers as the third argument.
|
|
@@ -104,6 +104,36 @@ interface ServerCollectionHandle<Row = unknown> {
|
|
|
104
104
|
/** Materialize a snapshot (filter/sort/limit); omit the query for the whole collection. Server-side, policy-free. */
|
|
105
105
|
snapshot(query?: CollectionQuery): Promise<Row[]>;
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Access policy for a CRDT document collection (ADR-0007, Q7). Guard-shaped — not the RLS filter shape of
|
|
109
|
+
* {@link CollectionPolicy}, because CRDT collections are opened by id (no subset to filter). **Deny-by-default**.
|
|
110
|
+
* Server co-writes via `srv.collection(n)` bypass both.
|
|
111
|
+
*/
|
|
112
|
+
interface CrdtCollectionPolicy<Ctx = unknown, Doc = unknown> {
|
|
113
|
+
/** May this principal open `id`? Gets the post-merge plaintext `snapshot` (content-based auth like RLS reading a row). */
|
|
114
|
+
read?: (principal: string, id: string, snapshot: Doc | undefined, ctx: Ctx) => Awaitable$1<boolean>;
|
|
115
|
+
/** May this principal write to `id`? (Creation is server-authoritative — Q10 — so there is no `create` op here.) */
|
|
116
|
+
write?: (principal: string, id: string, ctx: Ctx) => Awaitable$1<boolean>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Server-authoritative handle for a CRDT document collection (`srv.collection('scenes')`). Creation is
|
|
120
|
+
* server-only (Q10): `create` validates the initial doc and materializes it; `open` returns a reactive
|
|
121
|
+
* co-writer whose mutations fan out like a relayed client delta. Policy-free (server is authoritative).
|
|
122
|
+
*/
|
|
123
|
+
interface ServerCrdtCollectionHandle<Doc = unknown> {
|
|
124
|
+
/** Create a document with pre-validated initial data. Throws `CONFLICT` if the id exists. */
|
|
125
|
+
create(id: string, data: Doc): Promise<void>;
|
|
126
|
+
/** Open a reactive co-writer over an existing document. `origin` (default `"server"`) attributes its writes. */
|
|
127
|
+
open(id: string, opts?: {
|
|
128
|
+
origin?: string;
|
|
129
|
+
}): CrdtServerReplica;
|
|
130
|
+
/** Read the current plaintext snapshot of a document, or undefined if absent. */
|
|
131
|
+
read(id: string): Promise<Doc | undefined>;
|
|
132
|
+
/** Delete a document (idempotent), fanning the deletion cluster-wide. */
|
|
133
|
+
delete(id: string): Promise<void>;
|
|
134
|
+
/** Enumerate document ids + summaries (no content query). */
|
|
135
|
+
list(opts?: DocListOpts): Promise<DocSummary[]>;
|
|
136
|
+
}
|
|
107
137
|
|
|
108
138
|
/**
|
|
109
139
|
* In-process pub/sub bus. Share one bus across multiple servers to simulate
|
|
@@ -266,46 +296,6 @@ interface RoleLens<C extends Contract, R extends RoleOf<C>> {
|
|
|
266
296
|
/** Publish to a topic in role `R`'s surface (reaches that role's subscribers). */
|
|
267
297
|
publish<T extends keyof RoleTopics<C, R>>(topic: T, data: EmitData<RoleTopics<C, R>[T]>): void;
|
|
268
298
|
}
|
|
269
|
-
/**
|
|
270
|
-
* Server-side handle for one configured Store, reached via `srv.store.<name>`. The server is
|
|
271
|
-
* authoritative: it creates Resources, grants/revokes access, and may co-write. `data` is untyped
|
|
272
|
-
* (stores are off-contract — see ADR-0003); callers assert the shape.
|
|
273
|
-
*/
|
|
274
|
-
interface ServerStoreHandle {
|
|
275
|
-
/** Create a Resource with initial data + access rules (deny-by-default for everyone unlisted). */
|
|
276
|
-
create(id: string, data: unknown, accessRules: AccessRules): Promise<void>;
|
|
277
|
-
/** Read a Resource (data + accessRules), or undefined if absent. */
|
|
278
|
-
read(id: string): Promise<Resource | undefined>;
|
|
279
|
-
/**
|
|
280
|
-
* Server co-write: replace the Resource's value (LWW), fanned out to subscribers stamped with `origin`
|
|
281
|
-
* (default `"server"`) for echo-break + inspector attribution — mirrors {@link ServerStore.open}'s origin.
|
|
282
|
-
*/
|
|
283
|
-
write(id: string, data: unknown, opts?: {
|
|
284
|
-
origin?: string;
|
|
285
|
-
}): Promise<void>;
|
|
286
|
-
/** Grant a principal read/write on a Resource. */
|
|
287
|
-
grant(id: string, principal: string, perms: Perms): Promise<void>;
|
|
288
|
-
/** Revoke a principal's access to a Resource entirely. */
|
|
289
|
-
revoke(id: string, principal: string): Promise<void>;
|
|
290
|
-
/** Delete a Resource. */
|
|
291
|
-
delete(id: string): Promise<void>;
|
|
292
|
-
/**
|
|
293
|
-
* Summaries of this store's Resources, server-side filtered / sorted / paginated per {@link ListOpts}
|
|
294
|
-
* (omit `opts` for every Resource, id-ascending). Forwards to {@link ServerStore.list}.
|
|
295
|
-
*/
|
|
296
|
-
list(opts?: ListOpts): Promise<ResourceSummary[]>;
|
|
297
|
-
/** Distinct principals granted anywhere in this store (store-global). Forwards to {@link ServerStore.searchPrincipals}. */
|
|
298
|
-
searchPrincipals(opts: SearchOpts): Promise<string[]>;
|
|
299
|
-
/**
|
|
300
|
-
* Open a reactive in-process co-writer over a Resource's canonical state — the server half's mirror of
|
|
301
|
-
* `client.store(name).open(id)`. Reactive reads, merging `update`, and surgical `delete(path)` (the only
|
|
302
|
-
* way to remove a key server-side), all server-authoritative and fanned out to subscribers. `origin`
|
|
303
|
-
* (default `"server"`) tags its Changes for inspector attribution. Throws if the store has no `open`.
|
|
304
|
-
*/
|
|
305
|
-
open(id: string, opts?: {
|
|
306
|
-
origin?: string;
|
|
307
|
-
}): ServerReplica;
|
|
308
|
-
}
|
|
309
299
|
/**
|
|
310
300
|
* Handlers a plugin provides for its paired surface `S` (ADR-0004): one per `clientToServer` key,
|
|
311
301
|
* typed from `S`. `ctx`/`conn` are loose — a plugin is written independently of the host's per-role ctx.
|
|
@@ -357,11 +347,6 @@ interface SuperLinePlugin<S extends Directional = {}> {
|
|
|
357
347
|
* throws at construction.
|
|
358
348
|
*/
|
|
359
349
|
handlers?: (ctx: PluginContext) => HandlersFor<S>;
|
|
360
|
-
/**
|
|
361
|
-
* Server halves of Store pairs the plugin contributes, merged into the host's `stores` and reachable
|
|
362
|
-
* via `srv.store(name)`. A name colliding with a host store or another plugin's store throws at construction.
|
|
363
|
-
*/
|
|
364
|
-
stores?: Record<string, ServerStore>;
|
|
365
350
|
/**
|
|
366
351
|
* Row-security policies for the collections the plugin's contract fragment declares (see {@link CollectionPolicy}).
|
|
367
352
|
* Merged into the host's `policies`; a collection already policied by the host or another plugin throws at
|
|
@@ -457,11 +442,7 @@ interface PluginContext {
|
|
|
457
442
|
readonly size: number;
|
|
458
443
|
readonly connections: readonly Conn[];
|
|
459
444
|
};
|
|
460
|
-
/** Server-authoritative handle for a
|
|
461
|
-
store(name: string): ServerStoreHandle;
|
|
462
|
-
/** Configured stores (host + plugin) and their models. */
|
|
463
|
-
storeInfos(): StoreInfo[];
|
|
464
|
-
/** Server-authoritative handle for a contract collection (loosely typed here); throws if none is configured. */
|
|
445
|
+
/** Server-authoritative handle for a contract collection (loosely typed here as the LWW row handle — the surface plugins/inspector use); throws if none is configured. */
|
|
465
446
|
collection(name: string): ServerCollectionHandle;
|
|
466
447
|
/** Declared collections (name + key + advisory references) for the schema graph. */
|
|
467
448
|
collectionInfos(): {
|
|
@@ -511,12 +492,6 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>, P
|
|
|
511
492
|
interval?: number;
|
|
512
493
|
maxMissed?: number;
|
|
513
494
|
} | false;
|
|
514
|
-
/**
|
|
515
|
-
* Pluggable persisted-state Stores, keyed by name (`{ scene: crdtStoreServer(), config: memoryStoreServer() }`).
|
|
516
|
-
* Each is the server half of a Store pair; the client passes the matching client halves. Surfaced as
|
|
517
|
-
* `srv.store.<name>` and `client.store.<name>`. Stores are off-contract and untyped (ADR-0003).
|
|
518
|
-
*/
|
|
519
|
-
stores?: Record<string, ServerStore>;
|
|
520
495
|
/**
|
|
521
496
|
* The single {@link CollectionStore} backend serving every collection this contract declares (typed rows;
|
|
522
497
|
* ADR-0006), e.g. `collections: memoryCollections()`. One backend ⇒ one transaction domain, so a
|
|
@@ -524,11 +499,18 @@ interface SuperLineServerOptions<C extends Contract, A extends AuthResult<C>, P
|
|
|
524
499
|
*/
|
|
525
500
|
collections?: CollectionStore;
|
|
526
501
|
/**
|
|
527
|
-
*
|
|
528
|
-
*
|
|
502
|
+
* The {@link CrdtCollectionStore} backend serving this contract's CRDT document collections (ADR-0007),
|
|
503
|
+
* e.g. `crdtCollections: crdtMemoryCollections()`. A backend per family: CRDT docs never join a
|
|
504
|
+
* cross-collection atomic batch, so they get their own backend, separate from `collections`.
|
|
505
|
+
*/
|
|
506
|
+
crdtCollections?: CrdtCollectionStore;
|
|
507
|
+
/**
|
|
508
|
+
* Access policies per collection. Deny-by-default: a collection with no policy is server-only. LWW row
|
|
509
|
+
* collections take an RLS-style {@link CollectionPolicy} (read → filter); CRDT document collections take a
|
|
510
|
+
* guard-shaped {@link CrdtCollectionPolicy} (read → bool). Server co-writes via `srv.collection(n)` bypass them.
|
|
529
511
|
*/
|
|
530
512
|
policies?: {
|
|
531
|
-
[N in CollectionName<C>]?: CollectionPolicy<CtxUnion<A>, RowOf<C, N>>;
|
|
513
|
+
[N in CollectionName<C>]?: N extends CrdtCollectionName<C> ? CrdtCollectionPolicy<CtxUnion<A>, DocOf<C, N>> : CollectionPolicy<CtxUnion<A>, RowOf<C, N>>;
|
|
532
514
|
};
|
|
533
515
|
/**
|
|
534
516
|
* Opt-in advisory foreign-key checks (see ADR-0006, decision 8). When true, an insert/update whose
|
|
@@ -575,10 +557,13 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extend
|
|
|
575
557
|
subscribe<T extends keyof SharedTopics<C>>(topic: T, handler: (data: EventData<SharedTopics<C>[T]>, meta: BusMeta) => void): () => void;
|
|
576
558
|
/** Lens for role-scoped sends, e.g. forRole('user').publish('feed', data). */
|
|
577
559
|
forRole<R extends RoleOf<C>>(role: R): RoleLens<C, R>;
|
|
578
|
-
/**
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
560
|
+
/**
|
|
561
|
+
* Server-authoritative handle for a contract collection, typed by the contract: an LWW row collection gives
|
|
562
|
+
* `ServerCollectionHandle` (`insert`/`update`/`batch`), a CRDT document collection gives
|
|
563
|
+
* `ServerCrdtCollectionHandle` (`create`/`open`). Throws `NOT_FOUND` if no matching backend is configured
|
|
564
|
+
* or the name isn't declared.
|
|
565
|
+
*/
|
|
566
|
+
collection<N extends CollectionName<C>>(name: N): N extends CrdtCollectionName<C> ? ServerCrdtCollectionHandle<DocOf<C, N>> : ServerCollectionHandle<RowOf<C, N>>;
|
|
582
567
|
close(): Promise<void>;
|
|
583
568
|
}
|
|
584
569
|
/**
|
|
@@ -605,4 +590,4 @@ interface SuperLineServer<C extends Contract, A extends AuthResult<C>, HK extend
|
|
|
605
590
|
*/
|
|
606
591
|
declare function createSuperLineServer<C extends Contract, A extends AuthResult<C>, const P extends readonly SuperLinePlugin<any>[] = []>(contract: C, opts: SuperLineServerOptions<C, A, P>): SuperLineServer<C, A, HandledKeys<P>>;
|
|
607
592
|
|
|
608
|
-
export { type AuthResult, type BusMeta, type ClusterView, type CollectionPolicy, Conn, type ConnTarget, type HandledKeys, type Handlers, type HandlersFor, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type PluginChannel, type PluginConnection, type PluginContext, type PluginMiddleware, type RoleLens, type Room, type ServerCollectionHandle, type
|
|
593
|
+
export { type AuthResult, type BusMeta, type ClusterView, type CollectionPolicy, Conn, type ConnTarget, type CrdtCollectionPolicy, type HandledKeys, type Handlers, type HandlersFor, type LocalView, MemoryBus, type Middleware, type MiddlewareInfo, type PluginChannel, type PluginConnection, type PluginContext, type PluginMiddleware, type RoleLens, type Room, type ServerCollectionHandle, type ServerCrdtCollectionHandle, type SubtractHandlers, type SuperLinePlugin, type SuperLineServer, type SuperLineServerOptions, type UserTarget, type WriteOp, createInMemoryAdapter, createSuperLineServer, resolvePrincipal };
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
jsonSerializer,
|
|
4
4
|
validate,
|
|
5
|
+
validateSync,
|
|
5
6
|
SuperLineError,
|
|
6
7
|
andFilters,
|
|
7
8
|
orFilters,
|
|
8
|
-
matchesFilter
|
|
9
|
+
matchesFilter,
|
|
10
|
+
isCrdtCollection
|
|
9
11
|
} from "@super-line/core";
|
|
10
12
|
|
|
11
13
|
// src/conn.ts
|
|
@@ -184,27 +186,18 @@ var CONN = "c:";
|
|
|
184
186
|
var USER = "u:";
|
|
185
187
|
var REPLY = "reply:";
|
|
186
188
|
var PLUGIN = "x:";
|
|
187
|
-
var
|
|
189
|
+
var CDOC = "d:";
|
|
188
190
|
var SERVER_ORIGIN = "server";
|
|
189
191
|
function createSuperLineServer(contract, opts) {
|
|
190
192
|
const c = contract;
|
|
191
193
|
const serializer = opts.serializer ?? jsonSerializer;
|
|
192
194
|
const adapter = opts.adapter ?? createInMemoryAdapter();
|
|
193
|
-
const storeMap = opts.stores ?? {};
|
|
194
195
|
const plugins = opts.plugins ?? [];
|
|
195
196
|
const pluginNames = /* @__PURE__ */ new Set();
|
|
196
197
|
for (const p of plugins) {
|
|
197
198
|
if (pluginNames.has(p.name)) throw new Error(`Duplicate plugin name: ${p.name}`);
|
|
198
199
|
pluginNames.add(p.name);
|
|
199
200
|
}
|
|
200
|
-
for (const p of plugins) {
|
|
201
|
-
if (!p.stores) continue;
|
|
202
|
-
for (const [name, store] of Object.entries(p.stores)) {
|
|
203
|
-
if (name in storeMap)
|
|
204
|
-
throw new Error(`Plugin '${p.name}' store '${name}' collides with an existing store of that name`);
|
|
205
|
-
storeMap[name] = store;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
201
|
const collectionStore = opts.collections;
|
|
209
202
|
const collectionDefs = c.collections ?? {};
|
|
210
203
|
const collectionPolicies = { ...opts.policies };
|
|
@@ -222,6 +215,11 @@ function createSuperLineServer(contract, opts) {
|
|
|
222
215
|
}
|
|
223
216
|
const checkReferences = opts.checkReferences ?? false;
|
|
224
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
|
+
};
|
|
225
223
|
const COLL_CHANNEL = "cbatch";
|
|
226
224
|
const collSubscribers = /* @__PURE__ */ new Map();
|
|
227
225
|
const connColl = /* @__PURE__ */ new Map();
|
|
@@ -318,8 +316,8 @@ function createSuperLineServer(contract, opts) {
|
|
|
318
316
|
handlePersonal(channel, payload);
|
|
319
317
|
return;
|
|
320
318
|
}
|
|
321
|
-
if (channel.startsWith(
|
|
322
|
-
|
|
319
|
+
if (channel.startsWith(CDOC)) {
|
|
320
|
+
handleCrdtRelay(channel, payload);
|
|
323
321
|
return;
|
|
324
322
|
}
|
|
325
323
|
if (channel === COLL_CHANNEL) {
|
|
@@ -605,13 +603,12 @@ function createSuperLineServer(contract, opts) {
|
|
|
605
603
|
else if (frame.t === "unsub") {
|
|
606
604
|
const ns = topicNamespace(conn.role, frame.c);
|
|
607
605
|
if (ns) leaveChannel(conn, TOPIC + ns + ":" + frame.c);
|
|
608
|
-
} else if (frame.t === "
|
|
609
|
-
else if (frame.t === "srd") await handleStoreRead(conn, frame);
|
|
610
|
-
else if (frame.t === "swr") await handleStoreWrite(conn, frame);
|
|
611
|
-
else if (frame.t === "sclose") handleStoreClose(conn, frame);
|
|
612
|
-
else if (frame.t === "csub") await handleCollectionSub(conn, frame);
|
|
606
|
+
} else if (frame.t === "csub") await handleCollectionSub(conn, frame);
|
|
613
607
|
else if (frame.t === "cuns") handleCollectionUnsub(conn, frame);
|
|
614
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);
|
|
615
612
|
else if (frame.t === "sres") {
|
|
616
613
|
await handleClientReply(conn, frame.i, { ok: true, d: frame.d });
|
|
617
614
|
} else if (frame.t === "serr") {
|
|
@@ -704,90 +701,77 @@ function createSuperLineServer(contract, opts) {
|
|
|
704
701
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
705
702
|
});
|
|
706
703
|
}
|
|
707
|
-
function
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
return
|
|
711
|
-
}
|
|
712
|
-
async function
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
await dispatchOp(conn, frame.i, { kind: "subscribe", name: `
|
|
716
|
-
const
|
|
717
|
-
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}`);
|
|
718
715
|
const principal = conn.principal ?? conn.id;
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
}
|
|
726
|
-
async function handleStoreRead(conn, frame) {
|
|
727
|
-
const store = storeOrErr(conn, frame.i, frame.n);
|
|
728
|
-
if (!store) return;
|
|
729
|
-
await dispatchOp(conn, frame.i, { kind: "request", name: `store:${frame.n}/${frame.id}`, conn }, async () => {
|
|
730
|
-
const resource = await store.read(frame.id);
|
|
731
|
-
if (!resource) throw new SuperLineError("NOT_FOUND", `No resource: ${frame.n}/${frame.id}`);
|
|
732
|
-
const principal = conn.principal ?? conn.id;
|
|
733
|
-
if (!resource.accessRules[principal]?.read)
|
|
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))
|
|
734
722
|
throw new SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
|
|
735
|
-
conn.
|
|
723
|
+
await joinChannel(conn, CDOC + frame.n + ":" + frame.id);
|
|
724
|
+
conn.send({ t: "res", i: frame.i, d: state });
|
|
736
725
|
});
|
|
737
726
|
}
|
|
738
|
-
async function
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
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 () => {
|
|
744
732
|
const principal = conn.principal ?? conn.id;
|
|
745
|
-
|
|
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))
|
|
746
736
|
throw new SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
|
|
747
|
-
await store.apply(
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
id: frame.id,
|
|
753
|
-
origin: frame.o,
|
|
754
|
-
connId: conn.id,
|
|
755
|
-
data: frame.u
|
|
756
|
-
});
|
|
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
|
+
);
|
|
757
742
|
conn.send({ t: "res", i: frame.i, d: null });
|
|
758
743
|
});
|
|
759
744
|
}
|
|
760
|
-
function
|
|
761
|
-
if (!
|
|
762
|
-
leaveChannel(conn,
|
|
763
|
-
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);
|
|
764
748
|
}
|
|
765
|
-
|
|
766
|
-
const isSelf =
|
|
767
|
-
|
|
768
|
-
const channel =
|
|
749
|
+
if (crdtStore) {
|
|
750
|
+
const isSelf = crdtStore.clustering === "self";
|
|
751
|
+
crdtStore.onChange((change) => {
|
|
752
|
+
const channel = CDOC + change.n + ":" + change.id;
|
|
769
753
|
if (isSelf) {
|
|
770
754
|
const set = members.get(channel);
|
|
771
755
|
if (!set) return;
|
|
772
|
-
const payload = serializer.encode({ t: "
|
|
756
|
+
const payload = serializer.encode({ t: "cdchg", n: change.n, id: change.id, u: change.update, o: change.origin });
|
|
773
757
|
for (const conn of set) conn.sendRaw(payload);
|
|
774
758
|
return;
|
|
775
759
|
}
|
|
776
760
|
if (relaying) return;
|
|
777
761
|
void adapter.publish(
|
|
778
762
|
channel,
|
|
779
|
-
serializer.encode({ t: "
|
|
763
|
+
serializer.encode({ t: "cdchg", n: change.n, id: change.id, u: change.update, o: change.origin, nd: instanceId })
|
|
780
764
|
);
|
|
781
765
|
});
|
|
782
766
|
if (isSelf)
|
|
783
|
-
|
|
784
|
-
const set = members.get(
|
|
767
|
+
crdtStore.onDelete?.((n, id) => {
|
|
768
|
+
const set = members.get(CDOC + n + ":" + id);
|
|
785
769
|
if (!set) return;
|
|
786
|
-
const payload = serializer.encode({ t: "
|
|
770
|
+
const payload = serializer.encode({ t: "cddel", n, id });
|
|
787
771
|
for (const conn of set) conn.sendRaw(payload);
|
|
788
772
|
});
|
|
789
773
|
}
|
|
790
|
-
function
|
|
774
|
+
function handleCrdtRelay(channel, payload) {
|
|
791
775
|
const set = members.get(channel);
|
|
792
776
|
if (set) for (const conn of set) conn.sendRaw(payload);
|
|
793
777
|
let frame;
|
|
@@ -797,15 +781,20 @@ function createSuperLineServer(contract, opts) {
|
|
|
797
781
|
return;
|
|
798
782
|
}
|
|
799
783
|
if (frame.nd === instanceId) return;
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (
|
|
803
|
-
|
|
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
|
+
}
|
|
804
792
|
return;
|
|
805
793
|
}
|
|
806
794
|
relaying = true;
|
|
807
795
|
try {
|
|
808
|
-
void
|
|
796
|
+
void crdtStore.apply({ n: frame.n, id: frame.id, update: frame.u, origin: frame.o }, def.crdt, () => {
|
|
797
|
+
});
|
|
809
798
|
} catch {
|
|
810
799
|
} finally {
|
|
811
800
|
relaying = false;
|
|
@@ -861,6 +850,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
861
850
|
for (const op of ops) {
|
|
862
851
|
const def = collectionDefs[op.n];
|
|
863
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`);
|
|
864
854
|
const policy = collectionPolicies[op.n];
|
|
865
855
|
if (!policy?.write) throw new SuperLineError("FORBIDDEN", `Write denied: ${op.n}`);
|
|
866
856
|
const prev = await store.read(op.n, op.id);
|
|
@@ -1082,78 +1072,6 @@ function createSuperLineServer(contract, opts) {
|
|
|
1082
1072
|
void adapter.publish(CONN + id, serializer.encode(env));
|
|
1083
1073
|
});
|
|
1084
1074
|
}
|
|
1085
|
-
const storeApi = {};
|
|
1086
|
-
for (const [name, store] of Object.entries(storeMap)) {
|
|
1087
|
-
const readOrThrow = async (id) => {
|
|
1088
|
-
const r = await store.read(id);
|
|
1089
|
-
if (!r) throw new SuperLineError("NOT_FOUND", `No resource: ${name}/${id}`);
|
|
1090
|
-
return r;
|
|
1091
|
-
};
|
|
1092
|
-
storeApi[name] = {
|
|
1093
|
-
async create(id, data, accessRules) {
|
|
1094
|
-
await store.create(id, data, accessRules);
|
|
1095
|
-
if (taps.length) emitTap({ type: "store.create", store: name, id });
|
|
1096
|
-
},
|
|
1097
|
-
read(id) {
|
|
1098
|
-
return Promise.resolve(store.read(id));
|
|
1099
|
-
},
|
|
1100
|
-
async write(id, data, opts2) {
|
|
1101
|
-
const origin = opts2?.origin ?? SERVER_ORIGIN;
|
|
1102
|
-
await store.apply({ id, update: data, origin });
|
|
1103
|
-
if (taps.length) emitTap({ type: "store.write", store: name, id, origin, data });
|
|
1104
|
-
},
|
|
1105
|
-
async grant(id, principal, perms) {
|
|
1106
|
-
const r = await readOrThrow(id);
|
|
1107
|
-
await store.setAccess(id, { ...r.accessRules, [principal]: perms });
|
|
1108
|
-
if (taps.length) emitTap({ type: "store.grant", store: name, id, principal, perms });
|
|
1109
|
-
},
|
|
1110
|
-
async revoke(id, principal) {
|
|
1111
|
-
const r = await readOrThrow(id);
|
|
1112
|
-
const next = { ...r.accessRules };
|
|
1113
|
-
delete next[principal];
|
|
1114
|
-
await store.setAccess(id, next);
|
|
1115
|
-
if (taps.length) emitTap({ type: "store.revoke", store: name, id, principal });
|
|
1116
|
-
},
|
|
1117
|
-
async delete(id) {
|
|
1118
|
-
await store.delete(id);
|
|
1119
|
-
if (store.clustering !== "self")
|
|
1120
|
-
void adapter.publish(
|
|
1121
|
-
STORE + name + ":" + id,
|
|
1122
|
-
serializer.encode({ t: "sdel", n: name, id, nd: instanceId })
|
|
1123
|
-
);
|
|
1124
|
-
if (taps.length) emitTap({ type: "store.delete", store: name, id });
|
|
1125
|
-
},
|
|
1126
|
-
list(opts2) {
|
|
1127
|
-
return Promise.resolve(store.list(opts2));
|
|
1128
|
-
},
|
|
1129
|
-
searchPrincipals(opts2) {
|
|
1130
|
-
return Promise.resolve(store.searchPrincipals(opts2));
|
|
1131
|
-
},
|
|
1132
|
-
open(id, openOpts) {
|
|
1133
|
-
if (!store.open)
|
|
1134
|
-
throw new SuperLineError("UNSUPPORTED", `Store ${name} does not support reactive open()`);
|
|
1135
|
-
const replica = store.open(id, openOpts);
|
|
1136
|
-
if (!taps.length) return replica;
|
|
1137
|
-
const origin = openOpts?.origin ?? SERVER_ORIGIN;
|
|
1138
|
-
const emit = (data) => emitTap({ type: "store.write", store: name, id, origin, data });
|
|
1139
|
-
return {
|
|
1140
|
-
...replica,
|
|
1141
|
-
set: (d) => {
|
|
1142
|
-
replica.set(d);
|
|
1143
|
-
emit(d);
|
|
1144
|
-
},
|
|
1145
|
-
update: (p) => {
|
|
1146
|
-
replica.update(p);
|
|
1147
|
-
emit(p);
|
|
1148
|
-
},
|
|
1149
|
-
delete: (path) => {
|
|
1150
|
-
replica.delete(path);
|
|
1151
|
-
emit({ delete: path });
|
|
1152
|
-
}
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1157
1075
|
const pluginDisposers = [];
|
|
1158
1076
|
const pluginHandlers = {};
|
|
1159
1077
|
const api = {
|
|
@@ -1254,15 +1172,40 @@ function createSuperLineServer(contract, opts) {
|
|
|
1254
1172
|
}
|
|
1255
1173
|
};
|
|
1256
1174
|
},
|
|
1257
|
-
store(name) {
|
|
1258
|
-
const handle = storeApi[name];
|
|
1259
|
-
if (!handle) throw new SuperLineError("NOT_FOUND", `Store not configured: ${name}`);
|
|
1260
|
-
return handle;
|
|
1261
|
-
},
|
|
1262
1175
|
collection(name) {
|
|
1263
|
-
if (!collectionStore) throw new SuperLineError("NOT_FOUND", "No collection backend configured");
|
|
1264
1176
|
const def = collectionDefs[name];
|
|
1265
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");
|
|
1266
1209
|
const store = collectionStore;
|
|
1267
1210
|
const resolve = async (row) => {
|
|
1268
1211
|
const v = await validate(def.schema, row);
|
|
@@ -1271,7 +1214,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
1271
1214
|
throw new SuperLineError("VALIDATION", `Collection ${name} row is missing string key '${def.key}'`);
|
|
1272
1215
|
return { id: key, row: v };
|
|
1273
1216
|
};
|
|
1274
|
-
|
|
1217
|
+
const handle = {
|
|
1275
1218
|
async insert(row) {
|
|
1276
1219
|
const { id, row: v } = await resolve(row);
|
|
1277
1220
|
await commitCollectionBatch([{ op: "insert", n: name, id, row: v }], SERVER_ORIGIN, true);
|
|
@@ -1290,6 +1233,7 @@ function createSuperLineServer(contract, opts) {
|
|
|
1290
1233
|
return Promise.resolve(store.snapshot(name, query ?? {}));
|
|
1291
1234
|
}
|
|
1292
1235
|
};
|
|
1236
|
+
return handle;
|
|
1293
1237
|
},
|
|
1294
1238
|
async close() {
|
|
1295
1239
|
if (closing) return;
|
|
@@ -1347,10 +1291,14 @@ function createSuperLineServer(contract, opts) {
|
|
|
1347
1291
|
}
|
|
1348
1292
|
};
|
|
1349
1293
|
},
|
|
1350
|
-
store: (name) => api.store(name),
|
|
1351
|
-
storeInfos: () => Object.entries(storeMap).map(([name, store]) => ({ name, model: store.model })),
|
|
1352
1294
|
collection: (name) => api.collection(name),
|
|
1353
|
-
collectionInfos: () =>
|
|
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
|
+
),
|
|
1354
1302
|
describe: (conn) => buildDescriptor(conn),
|
|
1355
1303
|
connectionById: (id) => Promise.resolve(presenceOrThrow().get(id)),
|
|
1356
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/adapter-libp2p": "0.6.0",
|
|
72
71
|
"@super-line/adapter-zeromq": "0.5.0",
|
|
73
72
|
"@super-line/adapter-redis": "0.5.0",
|
|
74
|
-
"@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",
|
|
75
76
|
"@super-line/adapter-rabbitmq": "0.5.0",
|
|
76
|
-
"@super-line/client": "0.8.0",
|
|
77
77
|
"@super-line/transport-loopback": "0.5.0",
|
|
78
|
-
"@super-line/transport-
|
|
79
|
-
"@super-line/transport-
|
|
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"
|