doxum 0.1.19 → 0.1.21

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.
Files changed (65) hide show
  1. package/README.md +30 -11
  2. package/dist/{scope-d-L87NeC.js → access-BeSV5ve-.js} +114 -20
  3. package/dist/access-BeSV5ve-.js.map +1 -0
  4. package/dist/{scope-Ezs-Y3TY.cjs → access-Bienf28h.cjs} +149 -19
  5. package/dist/access-Bienf28h.cjs.map +1 -0
  6. package/dist/advanced-BRL5pN0y.cjs +1361 -0
  7. package/dist/advanced-BRL5pN0y.cjs.map +1 -0
  8. package/dist/advanced-COFJtWMT.js +1188 -0
  9. package/dist/advanced-COFJtWMT.js.map +1 -0
  10. package/dist/{advanced-C2fpjbK3.d.ts → advanced-DfKc7TZR.d.cts} +27 -22
  11. package/dist/{advanced-DlOYk1vi.d.cts → advanced-FgdTwu5b.d.ts} +27 -22
  12. package/dist/advanced.cjs +1 -1
  13. package/dist/advanced.d.cts +2 -2
  14. package/dist/advanced.d.ts +2 -2
  15. package/dist/advanced.js +1 -1
  16. package/dist/{contract-XmKnroJq.d.cts → contract-CdmDHRUq.d.cts} +8 -6
  17. package/dist/{contract-CATCCVu5.d.ts → contract-KG5XnQZt.d.ts} +8 -6
  18. package/dist/{driver-CSnhMjbP.js → driver-CLYJOGDQ.js} +2 -2
  19. package/dist/{driver-CSnhMjbP.js.map → driver-CLYJOGDQ.js.map} +1 -1
  20. package/dist/{driver-BCyfqxsl.cjs → driver-Djr3BVvo.cjs} +2 -2
  21. package/dist/{driver-BCyfqxsl.cjs.map → driver-Djr3BVvo.cjs.map} +1 -1
  22. package/dist/index.cjs +1150 -1993
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +2 -2
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +1105 -1948
  27. package/dist/index.js.map +1 -1
  28. package/dist/{integration-CxVhq-KD.js → integration-C4Isp8lJ.js} +3 -2
  29. package/dist/{integration-CxVhq-KD.js.map → integration-C4Isp8lJ.js.map} +1 -1
  30. package/dist/{integration-B7VDgAOJ.cjs → integration-Z6MsfFI7.cjs} +4 -3
  31. package/dist/{integration-B7VDgAOJ.cjs.map → integration-Z6MsfFI7.cjs.map} +1 -1
  32. package/dist/integration.cjs +3 -3
  33. package/dist/integration.d.cts +1 -1
  34. package/dist/integration.d.ts +1 -1
  35. package/dist/integration.js +3 -3
  36. package/dist/{issue-DhrNdQNg.cjs → issue-DjK1xp5K.cjs} +106 -22
  37. package/dist/issue-DjK1xp5K.cjs.map +1 -0
  38. package/dist/{issue-DVaGQGeP.js → issue-W0hT8u8D.js} +81 -15
  39. package/dist/issue-W0hT8u8D.js.map +1 -0
  40. package/dist/local-sync.cjs +2 -2
  41. package/dist/local-sync.d.cts +1 -1
  42. package/dist/local-sync.d.ts +1 -1
  43. package/dist/local-sync.js +2 -2
  44. package/dist/{notification-IQIJyH3Q.js → notification-Bkjau6B6.js} +3 -56
  45. package/dist/notification-Bkjau6B6.js.map +1 -0
  46. package/dist/{notification-CDSUfiip.cjs → notification-DJfouMEb.cjs} +2 -91
  47. package/dist/notification-DJfouMEb.cjs.map +1 -0
  48. package/dist/react.cjs +2 -2
  49. package/dist/react.js +2 -2
  50. package/package.json +1 -1
  51. package/skills/doxum-runtime/SKILL.md +4 -1
  52. package/skills/doxum-runtime/references/guide.en.md +5 -2
  53. package/skills/doxum-runtime/references/guide.zh-CN.md +5 -2
  54. package/skills/doxum-runtime/references/projections.en.md +23 -9
  55. package/skills/doxum-runtime/references/projections.zh-CN.md +18 -6
  56. package/dist/advanced-BYZCjNWL.js +0 -680
  57. package/dist/advanced-BYZCjNWL.js.map +0 -1
  58. package/dist/advanced-CMMNk49G.cjs +0 -763
  59. package/dist/advanced-CMMNk49G.cjs.map +0 -1
  60. package/dist/issue-DVaGQGeP.js.map +0 -1
  61. package/dist/issue-DhrNdQNg.cjs.map +0 -1
  62. package/dist/notification-CDSUfiip.cjs.map +0 -1
  63. package/dist/notification-IQIJyH3Q.js.map +0 -1
  64. package/dist/scope-Ezs-Y3TY.cjs.map +0 -1
  65. package/dist/scope-d-L87NeC.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,96 +1,8 @@
1
- import { A as insert, D as equal, F as remove, L as profile, M as keys, N as matches, _ as resolveTreeContainer, a as compiledShape, c as debugKey, d as overlaps, f as read$1, g as resolveLocated, h as resolveContainer, i as AddressIndex, j as install, l as memberKey, n as fail, o as contains, p as resolveAddress, r as invalidValue, s as createAddressResolver, t as MutationRejected, u as nodeAt, v as resolveValue } from "./issue-DVaGQGeP.js";
2
- import { a as ParseError, c as copyValue, i as snapshot, l as equalValue, n as createAccess, o as checkKey, r as replace, s as checkValue, t as collectionAccess, u as parse } from "./scope-d-L87NeC.js";
3
- import { _ as affected, a as documentReadableOwner, d as accessOf, f as bindRuntimeAccess, g as TransactionRejected, h as DocumentReentrancyError, i as disposeNotification, l as subscribeRoot, m as DocumentDisposedError, n as bindDocumentReadable, o as notify, p as readWith, r as createNotification, s as shareNotification, t as attachProjection, u as subscribeTargets, v as belongs, y as same } from "./notification-IQIJyH3Q.js";
4
- import { n as bindRuntimeDriver, o as decodeChanges, r as disposeRuntimeDriver, s as sealChanges, t as assertRuntimeWritable } from "./driver-CSnhMjbP.js";
5
- import { a as projectionHandles, c as definitionOf, d as isProjection, f as observe, i as createScheduler, l as derive, m as ownerOf, n as assertScope, o as ProjectionDisposedError, p as ownDefinition, r as assertSynchronous, s as ProjectionError, t as incremental, u as input } from "./advanced-BYZCjNWL.js";
6
- //#region core/src/schema.ts
7
- const node = (value) => Object.freeze(value);
8
- const field = (validator) => node({
9
- kind: "field",
10
- ...validator ? { validator } : {}
11
- });
12
- const optional = (value) => {
13
- if (![
14
- "field",
15
- "variant",
16
- "map",
17
- "list",
18
- "tree"
19
- ].includes(value.kind)) throw new TypeError("Only field, variant, map, list and tree nodes support optional presence.");
20
- return node({
21
- ...value,
22
- optional: true
23
- });
24
- };
25
- const object = (shape) => node({
26
- kind: "object",
27
- shape: Object.freeze({ ...shape })
28
- });
29
- const variant = (tag, variants) => node({
30
- kind: "variant",
31
- tag,
32
- variants: Object.freeze({ ...variants })
33
- });
34
- const table = (value, options) => node({
35
- kind: "table",
36
- value,
37
- ...options
38
- });
39
- const map = (value, options) => node({
40
- kind: "map",
41
- value,
42
- ...options
43
- });
44
- const list = (value, config) => node({
45
- kind: "list",
46
- keyOf: config.keyOf,
47
- value
48
- });
49
- const tree = (value) => node({
50
- kind: "tree",
51
- value
52
- });
53
- const paths = /* @__PURE__ */ new WeakMap();
54
- const pathProxy = (nodes, address, owner) => {
55
- const proxy = new Proxy({}, { get: (_target, property) => {
56
- if (typeof property !== "string") return void 0;
57
- if (nodes.every((node) => node.kind === "table" || node.kind === "map") && property === "item") return (id) => {
58
- if (typeof id !== "string") throw new TypeError("Collection keys must be strings.");
59
- for (const node of nodes) if (node.kind === "table" || node.kind === "map") {
60
- const invalid = checkKey(node.key, id, [...address, id]);
61
- if (invalid) throw new TypeError(invalid.message);
62
- }
63
- return pathProxy(nodes.map((node) => node.value), [...address, id], owner);
64
- };
65
- const children = nodes.flatMap((node) => {
66
- if (node.kind === "object") return Object.hasOwn(node.shape, property) ? [node.shape[property]] : [];
67
- if (node.kind !== "variant") return [];
68
- if (property === node.tag) return [field()];
69
- return Object.values(node.variants).flatMap((branch) => Object.hasOwn(branch.shape, property) ? [branch.shape[property]] : []);
70
- });
71
- if (!children.length) throw new TypeError(`Invalid schema path segment: ${property}`);
72
- return pathProxy(children, [...address, property], owner);
73
- } });
74
- paths.set(proxy, {
75
- nodes,
76
- address,
77
- owner
78
- });
79
- return proxy;
80
- };
81
- const compilePath = (owner, kind, pick) => {
82
- const scope = {};
83
- const value = pick(pathProxy([owner], [], scope));
84
- const selected = typeof value === "object" && value !== null ? paths.get(value) : void 0;
85
- if (!selected || selected.owner !== scope) throw new TypeError("Selector must return a path from its callback.");
86
- if (kind === "collection" && !selected.nodes.every((node) => node.kind === "table" || node.kind === "map")) throw new TypeError("Collection selectors require a table or map path.");
87
- return Object.freeze({
88
- kind,
89
- schema: owner,
90
- address: Object.freeze([...selected.address])
91
- });
92
- };
93
- //#endregion
1
+ import { A as install, B as profile, E as equal, F as matches, L as remove, O as indexedKeys, R as toArray, _ as resolveTreeContainer, a as compiledShape, c as debugKey, d as overlaps, f as read$1, g as resolveLocated, h as resolveContainer, i as AddressIndex, k as insert, l as memberKey, n as fail, o as contains, p as resolveAddress, r as invalidValue, s as createAddressResolver, t as MutationRejected, u as nodeAt, v as resolveValue } from "./issue-W0hT8u8D.js";
2
+ import { a as DocumentReentrancyError, c as createAccess, d as ParseError, f as checkKey, g as parse, h as equalValue, i as DocumentDisposedError, l as replace, m as copyValue, n as bindRuntimeAccess, o as TransactionRejected, p as checkValue, r as readWith, s as collectionAccess, t as accessOf, u as snapshot } from "./access-BeSV5ve-.js";
3
+ import { C as list, D as table, E as optional, O as tree, S as field, T as object, _ as createScheduler, a as observe, b as collectionEntryNode, c as projectionRef, d as createCollectionOutput, f as mapRead, g as assertSynchronous, h as assertScope, i as isProjection, k as variant, l as collectionChangeBetween, m as stageCollectionRead, n as derive, o as ownProjection, p as snapshotCollectionView, r as input, s as producerOf, t as incremental, u as collectionView, v as ProjectionDisposedError, w as map, x as compilePath, y as ProjectionError } from "./advanced-COFJtWMT.js";
4
+ import { a as documentReadableOwner, d as affected, f as belongs, i as disposeNotification, l as subscribeRoot, n as bindDocumentReadable, o as notify, r as createNotification, s as shareNotification, t as attachProjection, u as subscribeTargets } from "./notification-Bkjau6B6.js";
5
+ import { n as bindRuntimeDriver, o as decodeChanges, r as disposeRuntimeDriver, s as sealChanges, t as assertRuntimeWritable } from "./driver-CLYJOGDQ.js";
94
6
  //#region core/src/impact.ts
95
7
  const queries = /* @__PURE__ */ new WeakMap();
96
8
  const affectsTarget = (impact, value) => queries.get(impact).affects(value);
@@ -372,7 +284,7 @@ const installMember = (parent, key, present, value) => {
372
284
  });
373
285
  };
374
286
  const orderOf = (node, value) => {
375
- if (node.kind === "list") return value.map(node.keyOf);
287
+ if (node.kind === "list") return toArray(indexedKeys(value, node.keyOf));
376
288
  if (node.kind === "table") return [...value.ids];
377
289
  throw new Error("Order requires a table or list.");
378
290
  };
@@ -514,7 +426,7 @@ const sealMembers = (fact, changes) => {
514
426
  }
515
427
  let order;
516
428
  if (fact.order) {
517
- const current = fact.node.kind === "list" ? keys(fact.value, fact.node.keyOf) : fact.value.ids;
429
+ const current = fact.node.kind === "list" ? indexedKeys(fact.value, fact.node.keyOf) : fact.value.ids;
518
430
  if (!equal(fact.order, current)) {
519
431
  const after = orderOf(fact.node, fact.value);
520
432
  profile.recorder("publishedOrderItems", after.length);
@@ -1103,1945 +1015,1219 @@ const asReadable = (runtime) => {
1103
1015
  //#region core/src/projection/select.ts
1104
1016
  const read = (runtime, selector) => readWith(runtime, selector);
1105
1017
  //#endregion
1106
- //#region core/src/projection/source.ts
1107
- const createSources = (scheduler) => {
1108
- const documents = /* @__PURE__ */ new Map();
1109
- const readables = /* @__PURE__ */ new Map();
1110
- const externalSources = /* @__PURE__ */ new WeakMap();
1111
- const isCollectionSource = (value) => value.kind === "collection";
1112
- const document = (runtime) => {
1113
- scheduler.assertIdle();
1114
- const state = accessOf(runtime);
1115
- if (state.disposed) throw new Error("Document has been disposed.");
1116
- const existing = documents.get(state);
1117
- if (existing) return existing;
1118
- const bindings = [];
1119
- let batch;
1120
- const make = (input) => {
1121
- const collection = "collection" in input ? input.collection : void 0;
1122
- const targets = "targets" in input ? input.targets : [input.collection];
1123
- if (state.disposed) throw new Error("Document has been disposed.");
1124
- targets.forEach((value) => {
1125
- if (!belongs(value, state.schema)) throw new Error("Projection target belongs to another schema.");
1126
- });
1127
- const old = bindings.find((entry) => Boolean(collection) === isCollectionSource(entry.handle) && entry.targets.length === targets.length && entry.targets.every((value, i) => same(value, targets[i])));
1128
- if (old) return old.handle;
1129
- let reset = false;
1130
- let orderChanged = false;
1131
- const keys = /* @__PURE__ */ new Set();
1132
- let baseline;
1133
- let baselineIds = Object.freeze([]);
1134
- let nextBaseline;
1135
- let nextBaselineIds = Object.freeze([]);
1136
- const collectionNode = collection ? nodeAt(state.schema, collection.address, state.document) : void 0;
1137
- const entryNode = collectionNode && (collectionNode.kind === "map" || collectionNode.kind === "table") ? collectionNode.value : void 0;
1138
- const sameEntry = (left, right) => entryNode ? equalValue(entryNode, left, right) : Object.is(left, right);
1139
- const record = {
1140
- consumers: /* @__PURE__ */ new Set(),
1141
- disposed: false,
1142
- fault: void 0,
1143
- revision: runtime.revision,
1144
- reset: () => reset,
1145
- clear: () => {
1146
- if (collection && nextBaseline) {
1147
- baseline = nextBaseline;
1148
- baselineIds = nextBaselineIds;
1149
- }
1150
- reset = false;
1151
- orderChanged = false;
1152
- batch = void 0;
1153
- keys.clear();
1154
- nextBaseline = void 0;
1155
- nextBaselineIds = Object.freeze([]);
1156
- },
1157
- context: (active) => {
1158
- assertScope(active);
1159
- if (state.disposed) throw new Error("Document has been disposed.");
1160
- const context = {
1161
- state,
1162
- active
1163
- };
1164
- const rawRead = collection ? collectionAccess(context, collection.address) : void 0;
1165
- const snapshots = collection ? /* @__PURE__ */ new Map() : void 0;
1166
- const read = collection ? Object.freeze({
1167
- get: (key) => {
1168
- assertScope(active);
1169
- if (!snapshots.has(key)) snapshots.set(key, snapshot(rawRead.get(key)));
1170
- return snapshots.get(key);
1171
- },
1172
- has: (key) => {
1173
- assertScope(active);
1174
- return rawRead.has(key);
1175
- },
1176
- ids: () => {
1177
- assertScope(active);
1178
- return rawRead.ids();
1179
- }
1180
- }) : createAccess(context);
1181
- if (!collection) return Object.freeze({
1182
- kind: "document",
1183
- read,
1184
- revision: runtime.revision(),
1185
- reset: record.reset(),
1186
- cause: batch?.cause
1187
- });
1188
- if (!baseline) {
1189
- const collectionRead = read;
1190
- baseline = /* @__PURE__ */ new Map();
1191
- baselineIds = Object.freeze(collectionAccess(context, collection.address).ids().slice());
1192
- for (const key of baselineIds) {
1193
- const value = collectionRead.get(key);
1194
- baseline.set(key, value);
1195
- }
1196
- }
1197
- const changed = [...keys];
1198
- const added = [];
1199
- const updated = [];
1200
- const removed = [];
1201
- const next = new Map(baseline);
1202
- const collectionRead = read;
1203
- for (const key of changed) {
1204
- const beforePresent = baseline.has(key);
1205
- const afterPresent = collectionRead.has(key);
1206
- const afterValue = afterPresent ? collectionRead.get(key) : void 0;
1207
- if (!beforePresent && afterPresent) {
1208
- added.push({
1209
- key,
1210
- after: afterValue
1211
- });
1212
- next.set(key, afterValue);
1213
- } else if (beforePresent && !afterPresent) {
1214
- removed.push({
1215
- key,
1216
- before: baseline.get(key)
1217
- });
1218
- next.delete(key);
1219
- } else if (beforePresent && afterPresent) {
1220
- const beforeValue = baseline.get(key);
1221
- if (!sameEntry(beforeValue, afterValue)) updated.push({
1222
- key,
1223
- before: beforeValue,
1224
- after: afterValue
1225
- });
1226
- next.set(key, afterValue);
1227
- }
1228
- }
1229
- const ids = record.reset() || orderChanged ? Object.freeze([...collectionRead.ids()]) : baselineIds;
1230
- if (record.reset()) {
1231
- next.clear();
1232
- for (const key of ids) {
1233
- const value = collectionRead.get(key);
1234
- next.set(key, value);
1235
- }
1236
- }
1237
- let order;
1238
- if (orderChanged) {
1239
- const beforeKeys = new Set(baselineIds);
1240
- const afterKeys = new Set(ids);
1241
- const beforeCommon = baselineIds.filter((key) => afterKeys.has(key));
1242
- const afterCommon = ids.filter((key) => beforeKeys.has(key));
1243
- if (beforeCommon.some((key, index) => afterCommon[index] !== key)) order = {
1244
- before: Object.freeze([...baselineIds]),
1245
- after: ids
1246
- };
1247
- }
1248
- const change = record.reset() ? Object.freeze({ kind: "reset" }) : added.length || updated.length || removed.length || order ? Object.freeze({
1249
- kind: "incremental",
1250
- added: Object.freeze(added),
1251
- updated: Object.freeze(updated),
1252
- removed: Object.freeze(removed),
1253
- ...order ? { order: Object.freeze(order) } : {}
1254
- }) : void 0;
1255
- nextBaseline = next;
1256
- nextBaselineIds = ids;
1257
- return Object.freeze({
1258
- kind: "collection",
1259
- read,
1260
- change,
1261
- revision: runtime.revision(),
1262
- cause: batch?.cause
1263
- });
1264
- }
1265
- };
1266
- const handle = collection ? { kind: "collection" } : {
1267
- collection: (pick) => {
1268
- scheduler.assertIdle();
1269
- return make({ collection: compilePath(state.schema, "collection", pick) });
1270
- },
1271
- targets: (...selected) => {
1272
- scheduler.assertIdle();
1273
- if (!selected.length) throw new TypeError("Expected at least one target.");
1274
- return make({ targets: selected.map((pick) => compilePath(state.schema, "value", pick)) });
1275
- }
1276
- };
1277
- Object.freeze(handle);
1278
- scheduler.register(handle, record);
1279
- const receive = (commit) => {
1280
- reset ||= commit.impact.kind === "reset";
1281
- if (collection) {
1282
- const change = collectionImpact(commit.impact, collection);
1283
- if (change.kind === "reset") reset = true;
1284
- else {
1285
- change.added.forEach((key) => keys.add(key));
1286
- change.removed.forEach((key) => keys.add(key));
1287
- change.updated.forEach((key) => keys.add(key));
1288
- orderChanged ||= Boolean(change.added.size || change.removed.size || change.orderChanged);
1289
- }
1290
- }
1291
- };
1292
- bindings.push({
1293
- handle,
1294
- targets,
1295
- record,
1296
- receive
1297
- });
1298
- return handle;
1299
- };
1300
- const handle = make({ targets: [] });
1301
- const guard = (locked) => {
1302
- state.projectionLocks = (state.projectionLocks ?? 0) + (locked ? 1 : -1);
1303
- };
1304
- scheduler.guards.add(guard);
1305
- const unsubscribe = attachProjection(runtime, {
1306
- capture: (commit) => {
1307
- for (const binding of bindings) if (!binding.targets.length || binding.targets.some((value) => affectsTarget(commit.impact, value))) {
1308
- binding.receive(commit);
1309
- batch = scheduler.batchContext();
1310
- scheduler.capture(binding.record);
1311
- }
1312
- },
1313
- settle: scheduler.settle,
1314
- flush: scheduler.flush,
1315
- dispose: () => {
1316
- bindings.forEach(({ record }) => {
1317
- record.fault = new ProjectionError("source", "document disposed", [runtime.revision()], /* @__PURE__ */ new Error("Document has been disposed."));
1318
- scheduler.capture(record);
1319
- });
1320
- scheduler.run();
1321
- }
1322
- });
1323
- scheduler.cleanups.add(() => {
1324
- unsubscribe();
1325
- scheduler.guards.delete(guard);
1326
- });
1327
- documents.set(state, handle);
1328
- return handle;
1329
- };
1330
- const valueSource = (initial, equal) => {
1331
- let value = initial;
1332
- let revision = 0;
1333
- let cause;
1334
- let reset = false;
1335
- const handle = Object.freeze({});
1336
- const record = {
1337
- consumers: /* @__PURE__ */ new Set(),
1338
- disposed: false,
1339
- fault: void 0,
1340
- revision: () => revision,
1341
- reset: () => reset,
1342
- clear: () => {
1343
- cause = void 0;
1344
- reset = false;
1018
+ //#region core/src/projection/value-output.ts
1019
+ const unset = Symbol("projection-value-unset");
1020
+ /** Owns staged and published state for one scalar Projection output. */
1021
+ const createValueOutput = () => {
1022
+ let value;
1023
+ let staged = unset;
1024
+ let next;
1025
+ let initialized = false;
1026
+ let revision = 0;
1027
+ let changed = false;
1028
+ let reset = false;
1029
+ const listeners = /* @__PURE__ */ new Set();
1030
+ const begin = (active, initialize) => {
1031
+ staged = unset;
1032
+ next = value;
1033
+ changed = false;
1034
+ reset = initialize;
1035
+ return {
1036
+ previous: initialized ? value : void 0,
1037
+ next: () => {
1038
+ assertScope(active);
1039
+ if (staged !== unset) return staged;
1040
+ return initialize ? void 0 : initialized ? value : void 0;
1345
1041
  },
1346
- context: (active) => {
1042
+ output: Object.freeze({ set: (candidate) => {
1347
1043
  assertScope(active);
1348
- return Object.freeze({
1349
- kind: "value",
1350
- value,
1351
- revision,
1352
- reset,
1353
- cause
1354
- });
1355
- }
1356
- };
1357
- scheduler.register(handle, record);
1358
- const accept = (next, metadata) => {
1359
- scheduler.assertIdle();
1360
- const recovering = record.fault !== void 0;
1361
- const eventful = metadata !== void 0 && (metadata.reset === true || metadata.revision !== void 0 && metadata.revision !== revision || metadata.cause !== void 0);
1362
- if (equal(value, next) && !recovering && !eventful) return;
1363
- record.fault = void 0;
1364
- value = next;
1365
- revision = metadata?.revision ?? revision + 1;
1366
- cause = metadata?.cause ?? scheduler.batchContext()?.cause ?? cause;
1367
- reset ||= metadata?.reset ?? false;
1368
- scheduler.capture(record);
1369
- };
1370
- return {
1371
- source: handle,
1372
- accept,
1373
- set(next) {
1374
- accept(next);
1375
- scheduler.run();
1376
- }
1044
+ staged = candidate;
1045
+ } })
1377
1046
  };
1378
1047
  };
1379
- const collectionInput = (initial) => {
1380
- const values = new Map(initial);
1381
- const touched = /* @__PURE__ */ new Map();
1382
- let beforeIds;
1383
- let revision = 0;
1384
- let cause;
1385
- const handle = Object.freeze({ kind: "collection" });
1386
- const change = () => {
1387
- const added = [];
1388
- const updated = [];
1389
- const removed = [];
1390
- for (const [key, before] of touched) {
1391
- const afterPresent = values.has(key);
1392
- if (!before.present && afterPresent) added.push({
1393
- key,
1394
- after: values.get(key)
1395
- });
1396
- else if (before.present && !afterPresent) removed.push({
1397
- key,
1398
- before: before.value
1399
- });
1400
- else if (before.present && afterPresent && !Object.is(before.value, values.get(key))) updated.push({
1401
- key,
1402
- before: before.value,
1403
- after: values.get(key)
1404
- });
1405
- }
1406
- let order;
1407
- if (beforeIds) {
1408
- const afterIds = [...values.keys()];
1409
- const beforeKeys = new Set(beforeIds);
1410
- const afterKeys = new Set(afterIds);
1411
- const beforeCommon = beforeIds.filter((key) => afterKeys.has(key));
1412
- const afterCommon = afterIds.filter((key) => beforeKeys.has(key));
1413
- if (beforeCommon.some((key, index) => key !== afterCommon[index])) order = Object.freeze({
1414
- before: beforeIds,
1415
- after: Object.freeze(afterIds)
1416
- });
1417
- }
1418
- if (!added.length && !updated.length && !removed.length && !order) return void 0;
1048
+ const seal = (nextReset, isEqual) => {
1049
+ if (reset && staged === unset) throw new TypeError("Value output must be initialized on reset.");
1050
+ if (staged === unset) {
1051
+ reset = nextReset;
1052
+ changed = false;
1053
+ next = value;
1054
+ return false;
1055
+ }
1056
+ const candidate = staged;
1057
+ changed = !initialized || !isEqual(value, candidate);
1058
+ next = changed ? candidate : value;
1059
+ reset = nextReset;
1060
+ return changed;
1061
+ };
1062
+ return {
1063
+ begin,
1064
+ seal,
1065
+ context: (active, cause) => {
1066
+ assertScope(active);
1419
1067
  return Object.freeze({
1420
- kind: "incremental",
1421
- added: Object.freeze(added),
1422
- updated: Object.freeze(updated),
1423
- removed: Object.freeze(removed),
1424
- ...order ? { order } : {}
1068
+ kind: "value",
1069
+ value: next,
1070
+ revision,
1071
+ reset,
1072
+ cause
1425
1073
  });
1426
- };
1427
- const record = {
1428
- consumers: /* @__PURE__ */ new Set(),
1429
- disposed: false,
1430
- fault: void 0,
1431
- revision: () => revision,
1432
- reset: () => false,
1433
- shouldSettle: () => change() !== void 0,
1434
- clear: () => {
1435
- touched.clear();
1436
- beforeIds = void 0;
1437
- cause = void 0;
1438
- },
1439
- context: (active) => {
1440
- assertScope(active);
1441
- const read = Object.freeze({
1442
- get: (key) => {
1443
- assertScope(active);
1444
- return values.get(key);
1445
- },
1446
- has: (key) => {
1447
- assertScope(active);
1448
- return values.has(key);
1449
- },
1450
- ids: () => {
1451
- assertScope(active);
1452
- return Object.freeze([...values.keys()]);
1453
- }
1454
- });
1455
- return Object.freeze({
1456
- kind: "collection",
1457
- read,
1458
- change: change(),
1459
- revision,
1460
- cause
1461
- });
1462
- }
1463
- };
1464
- scheduler.register(handle, record);
1465
- return {
1466
- source: handle,
1467
- update(run) {
1468
- scheduler.assertIdle();
1469
- const staged = /* @__PURE__ */ new Map();
1470
- let active = true;
1471
- const draft = Object.freeze({
1472
- get: (key) => {
1473
- assertScope(() => active);
1474
- const entry = staged.get(key);
1475
- return entry ? entry.present ? entry.value : void 0 : values.get(key);
1476
- },
1477
- has: (key) => {
1478
- assertScope(() => active);
1479
- const entry = staged.get(key);
1480
- return entry ? entry.present : values.has(key);
1481
- },
1482
- set: (key, value) => {
1483
- assertScope(() => active);
1484
- if (typeof key !== "string") throw new TypeError("Projection keys must be strings.");
1485
- staged.set(key, {
1486
- present: true,
1487
- value
1488
- });
1489
- },
1490
- remove: (key) => {
1491
- assertScope(() => active);
1492
- if (typeof key !== "string") throw new TypeError("Projection keys must be strings.");
1493
- staged.set(key, { present: false });
1494
- }
1495
- });
1496
- try {
1497
- assertSynchronous(run(draft));
1498
- } finally {
1499
- active = false;
1500
- }
1501
- let changed = false;
1502
- for (const [key, entry] of staged) {
1503
- const present = values.has(key);
1504
- if (entry.present ? present && Object.is(values.get(key), entry.value) : !present) continue;
1505
- if (!touched.has(key)) touched.set(key, {
1506
- present,
1507
- value: values.get(key)
1508
- });
1509
- if (!beforeIds && present !== entry.present) beforeIds = Object.freeze([...values.keys()]);
1510
- if (entry.present) values.set(key, entry.value);
1511
- else values.delete(key);
1512
- changed = true;
1513
- }
1514
- if (!changed) return;
1515
- revision++;
1516
- cause = scheduler.batchContext()?.cause ?? cause;
1517
- scheduler.capture(record);
1518
- scheduler.run();
1519
- }
1520
- };
1074
+ },
1075
+ current: (check) => {
1076
+ check();
1077
+ return value;
1078
+ },
1079
+ revision: () => revision,
1080
+ reset: () => reset,
1081
+ publish: () => {
1082
+ if (changed && initialized) revision++;
1083
+ if (changed) value = next;
1084
+ initialized = true;
1085
+ },
1086
+ emit: (call) => {
1087
+ profile.materialized.notification();
1088
+ Array.from(listeners).forEach((listener) => call(listener));
1089
+ },
1090
+ clear: () => {
1091
+ staged = unset;
1092
+ next = value;
1093
+ changed = false;
1094
+ reset = false;
1095
+ },
1096
+ release: () => {
1097
+ listeners.clear();
1098
+ staged = unset;
1099
+ value = next = void 0;
1100
+ initialized = false;
1101
+ revision = 0;
1102
+ changed = false;
1103
+ reset = false;
1104
+ },
1105
+ subscribe: (listener) => listeners.add(listener),
1106
+ unsubscribe: (listener) => listeners.delete(listener)
1521
1107
  };
1522
- const externalValueSource = (port, equal) => {
1523
- scheduler.assertIdle();
1524
- const old = externalSources.get(port);
1525
- if (old) return old;
1526
- const input = valueSource(port.current(), equal);
1527
- const fail = (cause) => {
1528
- const record = scheduler.source(input.source);
1529
- record.fault = new ProjectionError("source", "external source", [record.revision()], cause);
1530
- scheduler.capture(record);
1531
- };
1532
- let unsubscribe;
1533
- try {
1534
- unsubscribe = port.subscribe((event) => {
1535
- try {
1536
- input.accept(event.value, {
1537
- revision: event.revision,
1538
- reset: event.reset ?? false,
1539
- cause: event.cause
1540
- });
1541
- } catch (cause) {
1542
- fail(cause);
1543
- }
1544
- scheduler.run();
1545
- });
1546
- } catch (cause) {
1547
- unsubscribe?.();
1548
- scheduler.unregisterSource(input.source);
1549
- throw cause;
1550
- }
1551
- scheduler.cleanups.add(() => unsubscribe?.());
1552
- externalSources.set(port, input.source);
1553
- return input.source;
1108
+ };
1109
+ //#endregion
1110
+ //#region core/src/projection/processor.ts
1111
+ /** Materializes every derive/incremental/group definition through one processor lifecycle. */
1112
+ const createProcessor = (scheduler, definition, dependencies) => {
1113
+ scheduler.assertIdle();
1114
+ let processor;
1115
+ let instance;
1116
+ let cause;
1117
+ const check = () => {
1118
+ if (!scheduler.active || processor.disposed) throw new ProjectionDisposedError();
1119
+ if (processor.fault) throw processor.fault;
1554
1120
  };
1555
- const externalCollectionSource = (port) => {
1556
- scheduler.assertIdle();
1557
- const old = externalSources.get(port);
1558
- if (old) return old;
1559
- let read = port.current();
1560
- let previous = read;
1561
- let revision = port.revision();
1562
- let change;
1563
- let reset = false;
1564
- let orderChanged = false;
1565
- let cause;
1566
- let transitionKeys = /* @__PURE__ */ new Set();
1567
- let pending = false;
1568
- const handle = Object.freeze({ kind: "collection" });
1569
- const record = {
1570
- consumers: /* @__PURE__ */ new Set(),
1571
- disposed: false,
1572
- fault: void 0,
1573
- revision: () => revision,
1574
- reset: () => reset,
1575
- clear: () => {
1576
- previous = read;
1577
- pending = false;
1578
- change = void 0;
1579
- reset = false;
1580
- orderChanged = false;
1581
- cause = void 0;
1582
- transitionKeys.clear();
1583
- },
1584
- context: (active) => {
1585
- assertScope(active);
1586
- const currentRead = Object.freeze({
1587
- get: (key) => {
1588
- assertScope(active);
1589
- return read.get(key);
1121
+ const bound = definition.outputs.map((outputDefinition) => {
1122
+ if (outputDefinition.kind === "value") {
1123
+ const state = createValueOutput();
1124
+ return {
1125
+ kind: "value",
1126
+ definition: outputDefinition,
1127
+ state,
1128
+ output: {
1129
+ kind: "value",
1130
+ get owner() {
1131
+ return processor;
1590
1132
  },
1591
- has: (key) => {
1592
- assertScope(active);
1593
- return read.has(key);
1133
+ consumers: /* @__PURE__ */ new Set(),
1134
+ context: (active) => state.context(active, cause),
1135
+ current: () => state.current(check),
1136
+ revision: state.revision,
1137
+ reset: state.reset,
1138
+ subscribe: (listener) => {
1139
+ check();
1140
+ const wrapped = () => listener();
1141
+ state.subscribe(wrapped);
1142
+ return () => state.unsubscribe(wrapped);
1594
1143
  },
1595
- ids: () => {
1596
- assertScope(active);
1597
- return read.ids();
1598
- }
1599
- });
1600
- return Object.freeze({
1601
- kind: "collection",
1602
- read: currentRead,
1603
- change,
1604
- revision,
1605
- cause
1606
- });
1144
+ emit: state.emit,
1145
+ clear: state.clear,
1146
+ release: state.release
1147
+ }
1148
+ };
1149
+ }
1150
+ const state = createCollectionOutput();
1151
+ return {
1152
+ kind: "collection",
1153
+ definition: outputDefinition,
1154
+ state,
1155
+ output: {
1156
+ kind: "collection",
1157
+ get owner() {
1158
+ return processor;
1159
+ },
1160
+ consumers: /* @__PURE__ */ new Set(),
1161
+ context: (active) => state.context(active, cause),
1162
+ current: () => state.current(check),
1163
+ revision: state.revision,
1164
+ reset: state.reset,
1165
+ subscribe: (listener) => {
1166
+ check();
1167
+ const wrapped = (change) => listener(change);
1168
+ state.subscribe(wrapped);
1169
+ return () => state.unsubscribe(wrapped);
1170
+ },
1171
+ emit: state.emit,
1172
+ clear: state.clear,
1173
+ release: state.release
1607
1174
  }
1608
1175
  };
1609
- scheduler.register(handle, record);
1610
- const fail = (error) => {
1611
- record.fault = new ProjectionError("source", "external collection source", [revision], error);
1612
- scheduler.capture(record);
1613
- };
1614
- const deriveChange = () => {
1615
- if (reset) return Object.freeze({ kind: "reset" });
1616
- const added = [];
1617
- const updated = [];
1618
- const removed = [];
1619
- for (const key of transitionKeys) {
1620
- const beforePresent = previous.has(key);
1621
- const afterPresent = read.has(key);
1622
- const before = beforePresent ? previous.get(key) : void 0;
1623
- const after = afterPresent ? read.get(key) : void 0;
1624
- if (beforePresent === afterPresent && Object.is(before, after)) continue;
1625
- if (!beforePresent && afterPresent) added.push({
1626
- key,
1627
- after
1628
- });
1629
- else if (beforePresent && !afterPresent) removed.push({
1630
- key,
1631
- before
1632
- });
1633
- else if (beforePresent && afterPresent) updated.push({
1634
- key,
1635
- before,
1636
- after
1637
- });
1638
- }
1639
- let order;
1640
- if (orderChanged) {
1641
- const beforeIds = previous.ids();
1642
- const afterIds = read.ids();
1643
- const beforeKeys = new Set(beforeIds);
1644
- const afterKeys = new Set(afterIds);
1645
- const beforeCommon = beforeIds.filter((key) => afterKeys.has(key));
1646
- const afterCommon = afterIds.filter((key) => beforeKeys.has(key));
1647
- if (beforeCommon.some((key, index) => afterCommon[index] !== key)) order = Object.freeze({
1648
- before: Object.freeze([...beforeIds]),
1649
- after: Object.freeze([...afterIds])
1650
- });
1651
- }
1652
- if (!added.length && !removed.length && !updated.length && !order) return void 0;
1176
+ });
1177
+ const begin = (active, initialize) => Object.freeze(bound.map((entry) => {
1178
+ if (entry.kind === "value") {
1179
+ const evaluation = entry.state.begin(active, initialize);
1653
1180
  return Object.freeze({
1654
- kind: "incremental",
1655
- added: Object.freeze(added),
1656
- updated: Object.freeze(updated),
1657
- removed: Object.freeze(removed),
1658
- ...order ? { order } : {}
1659
- });
1660
- };
1661
- let unsubscribe;
1662
- try {
1663
- unsubscribe = port.subscribe((event) => {
1664
- try {
1665
- scheduler.assertIdle();
1666
- if (!pending) previous = event.previous;
1667
- read = port.current();
1668
- revision = event.revision;
1669
- if (event.impact?.kind === "incremental") {
1670
- event.impact.added.forEach((key) => transitionKeys.add(key));
1671
- event.impact.removed.forEach((key) => transitionKeys.add(key));
1672
- event.impact.updated.forEach((key) => transitionKeys.add(key));
1673
- orderChanged ||= Boolean(event.impact.added.size || event.impact.removed.size || event.impact.orderChanged);
1674
- } else if (!event.impact) {
1675
- previous.ids().forEach((key) => transitionKeys.add(key));
1676
- read.ids().forEach((key) => transitionKeys.add(key));
1677
- orderChanged = true;
1678
- }
1679
- reset ||= event.impact?.kind === "reset";
1680
- cause = event.cause ?? scheduler.batchContext()?.cause ?? cause;
1681
- change = deriveChange();
1682
- record.fault = void 0;
1683
- scheduler.capture(record);
1684
- pending = true;
1685
- } catch (error) {
1686
- fail(error);
1687
- }
1688
- scheduler.run();
1181
+ kind: "value",
1182
+ ...evaluation
1689
1183
  });
1690
- } catch (error) {
1691
- unsubscribe?.();
1692
- scheduler.unregisterSource(handle);
1693
- throw error;
1694
1184
  }
1695
- scheduler.cleanups.add(() => unsubscribe?.());
1696
- externalSources.set(port, handle);
1697
- return handle;
1185
+ const evaluation = entry.state.begin(active, initialize);
1186
+ return Object.freeze({
1187
+ kind: "collection",
1188
+ ...evaluation
1189
+ });
1190
+ }));
1191
+ const seal = (reset) => {
1192
+ const changed = [];
1193
+ for (const entry of bound) if (entry.state.seal(reset, entry.definition.equality)) changed.push(entry.output);
1194
+ return Object.freeze(changed);
1698
1195
  };
1699
- return {
1700
- dispose: () => {
1701
- documents.clear();
1702
- readables.clear();
1703
- },
1704
- document,
1705
- fromSource: (source, options) => externalValueSource(source, options?.isEqual ?? Object.is),
1706
- fromCollectionSource: (source) => externalCollectionSource(source),
1707
- input: (initial, options) => {
1708
- const { source, set } = valueSource(initial, options?.isEqual ?? Object.is);
1709
- return Object.freeze({
1710
- source,
1711
- set
1196
+ const run = (reset, runCause) => {
1197
+ cause = runCause;
1198
+ let active = true;
1199
+ try {
1200
+ const scopeActive = () => active;
1201
+ const sources = Object.freeze(dependencies.map((output) => output.context(scopeActive)));
1202
+ let outputs = begin(scopeActive, reset);
1203
+ if (!instance) instance = definition.create();
1204
+ profile.materialized[reset ? "rebuilt" : "updated"]();
1205
+ let result = instance.evaluate({
1206
+ reset,
1207
+ cause: runCause,
1208
+ sources,
1209
+ outputs
1712
1210
  });
1713
- },
1714
- collectionInput,
1715
- fromReadable: (readable, options) => {
1716
- scheduler.assertIdle();
1717
- if (projectionHandles.has(readable)) throw new Error("Projection nodes must be declared directly as sources.");
1718
- const equal = options?.isEqual ?? Object.is;
1719
- let bindings = readables.get(readable);
1720
- const old = bindings?.get(equal);
1721
- if (old) return old.source;
1722
- const input = valueSource(readable.current(), equal);
1723
- if (bindings) {
1724
- bindings.set(equal, {
1725
- source: input.source,
1726
- accept: (value) => input.accept(value)
1211
+ assertSynchronous(result);
1212
+ if (result?.kind === "rebuild") {
1213
+ if (reset) throw new TypeError("Initial or reset projection processor cannot rebuild.");
1214
+ instance.release?.();
1215
+ instance = definition.create();
1216
+ outputs = begin(scopeActive, true);
1217
+ result = instance.evaluate({
1218
+ reset: true,
1219
+ cause: runCause,
1220
+ sources,
1221
+ outputs
1727
1222
  });
1728
- return input.source;
1223
+ assertSynchronous(result);
1224
+ if (result?.kind === "rebuild") throw new TypeError("Reset projection processor cannot rebuild again.");
1225
+ return seal(true);
1729
1226
  }
1730
- bindings = new Map([[equal, {
1731
- source: input.source,
1732
- accept: (value) => input.accept(value)
1733
- }]]);
1734
- const members = bindings;
1735
- const receive = (settle = true) => {
1736
- if (!scheduler.active) return;
1737
- let value;
1738
- try {
1739
- value = readable.current();
1740
- } catch (cause) {
1741
- for (const member of members.values()) {
1742
- const record = scheduler.source(member.source);
1743
- record.fault = new ProjectionError("source", "external readable", [record.revision()], cause);
1744
- scheduler.capture(record);
1745
- }
1746
- if (settle) scheduler.run();
1747
- return;
1748
- }
1749
- for (const member of members.values()) try {
1750
- member.accept(value);
1751
- } catch (cause) {
1752
- const record = scheduler.source(member.source);
1753
- record.fault = new ProjectionError("source", "external readable", [record.revision()], cause);
1754
- scheduler.capture(record);
1755
- }
1756
- if (settle) scheduler.run();
1757
- };
1758
- let unsubscribe;
1759
- let detach;
1760
- try {
1761
- const owner = documentReadableOwner(readable);
1762
- if (owner) {
1763
- document(owner);
1764
- detach = attachProjection(owner, {
1765
- capture: () => receive(false),
1766
- settle: scheduler.settle,
1767
- flush: scheduler.flush,
1768
- dispose: () => {
1769
- for (const member of members.values()) {
1770
- const record = scheduler.source(member.source);
1771
- record.fault = new ProjectionError("source", "document readable disposed", [record.revision()], /* @__PURE__ */ new Error("Document has been disposed."));
1772
- scheduler.capture(record);
1773
- }
1774
- scheduler.run();
1775
- }
1776
- });
1777
- }
1778
- unsubscribe = readable.subscribe(() => receive());
1779
- input.accept(readable.current());
1780
- } catch (error) {
1781
- try {
1782
- unsubscribe?.();
1783
- detach?.();
1784
- } finally {
1785
- scheduler.unregisterSource(input.source);
1786
- }
1787
- throw error;
1788
- }
1789
- scheduler.cleanups.add(unsubscribe);
1790
- if (detach) scheduler.cleanups.add(detach);
1791
- readables.set(readable, bindings);
1792
- scheduler.run();
1793
- return input.source;
1794
- },
1795
- assertCollection: (handle) => {
1796
- scheduler.source(handle);
1797
- if (!isCollectionSource(handle)) throw new Error("map requires a collection source.");
1227
+ return seal(reset);
1228
+ } finally {
1229
+ active = false;
1798
1230
  }
1799
1231
  };
1800
- };
1801
- //#endregion
1802
- //#region core/src/projection/node.ts
1803
- const createNode = (scheduler, spec, behavior) => {
1804
- scheduler.assertIdle();
1805
- const entries = Object.entries(spec.sources).map(([key, handle]) => [key, scheduler.source(handle)]);
1806
1232
  const order = scheduler.order();
1807
- const node = {
1808
- owner: void 0,
1809
- ...behavior,
1810
- publish: () => {
1811
- if (node.fault) behavior.clear();
1812
- else behavior.publish();
1813
- },
1233
+ processor = {
1234
+ kind: "processor",
1814
1235
  order,
1815
- name: spec.name ? `${spec.name} (${order})` : `projection-${order}`,
1816
- sources: entries.map(([, source]) => source),
1817
- consumers: /* @__PURE__ */ new Set(),
1818
- disposed: false,
1236
+ name: definition.name ? `${definition.name} (${order})` : `projection-${order}`,
1237
+ dependencies: Object.freeze([...dependencies]),
1238
+ outputs: Object.freeze(bound.map((entry) => entry.output)),
1819
1239
  fault: void 0,
1820
- evaluate: (build) => {
1821
- let active = true;
1822
- try {
1823
- const scopeActive = () => active;
1824
- const inputs = Object.fromEntries(entries.map(([key, source]) => [key, source.context(scopeActive)]));
1825
- return behavior.evaluate(inputs, build, scopeActive);
1826
- } finally {
1827
- active = false;
1828
- }
1829
- }
1830
- };
1831
- node.owner = node;
1832
- scheduler.initialize(() => {
1833
- const invalid = node.sources.find((source) => source.fault || source.disposed);
1834
- if (invalid) throw invalid.fault ?? new ProjectionDisposedError();
1835
- node.evaluate(true);
1836
- node.publish();
1837
- node.clear();
1838
- });
1839
- const check = () => {
1840
- if (node.disposed || !scheduler.active) throw new ProjectionDisposedError();
1841
- if (node.fault) throw node.fault;
1842
- };
1843
- return {
1844
- node,
1845
- check,
1846
- install(handle) {
1847
- scheduler.register(handle, node);
1848
- scheduler.addNode(node);
1240
+ disposed: false,
1241
+ evaluate: run,
1242
+ publish: () => bound.forEach((entry) => entry.state.publish()),
1243
+ clear: () => {
1244
+ bound.forEach((entry) => entry.state.clear());
1245
+ cause = void 0;
1246
+ },
1247
+ release: () => {
1248
+ instance?.release?.();
1249
+ instance = void 0;
1250
+ bound.forEach((entry) => {
1251
+ entry.output.consumers.clear();
1252
+ entry.state.release();
1253
+ });
1254
+ cause = void 0;
1849
1255
  }
1850
1256
  };
1257
+ try {
1258
+ scheduler.initialize(() => {
1259
+ const invalid = dependencies.find((output) => output.owner.fault || output.owner.disposed);
1260
+ if (invalid) throw invalid.owner.fault ?? new ProjectionDisposedError();
1261
+ if (processor.evaluate(true, void 0).length) processor.publish();
1262
+ processor.clear();
1263
+ });
1264
+ } catch (error) {
1265
+ processor.release();
1266
+ throw error;
1267
+ }
1268
+ scheduler.addProcessor(processor);
1269
+ return processor;
1851
1270
  };
1852
1271
  //#endregion
1853
- //#region core/src/projection/value.ts
1854
- const createValue = (scheduler, spec, options) => {
1855
- let instance;
1856
- let value;
1857
- let next;
1858
- let initialized = false;
1859
- let revision = 0;
1860
- let changed = false;
1861
- let reset = false;
1862
- let cause;
1863
- const listeners = /* @__PURE__ */ new Set();
1864
- const owner = createNode(scheduler, spec, {
1865
- evaluate: (sources, build) => {
1866
- cause = Object.values(sources).find((value) => value && typeof value === "object" && "cause" in value && value.cause !== void 0)?.cause ?? scheduler.batchContext()?.cause;
1867
- let candidate;
1868
- if (build || !instance) {
1869
- profile.materialized.rebuilt();
1870
- instance = void 0;
1871
- const built = spec.build(sources);
1872
- assertSynchronous(built);
1873
- instance = built;
1874
- candidate = built.value;
1875
- } else {
1876
- profile.materialized.updated();
1877
- const result = instance.update(sources);
1878
- assertSynchronous(result);
1879
- if (result.kind === "rebuild") {
1880
- instance = void 0;
1881
- const built = spec.build(sources);
1882
- assertSynchronous(built);
1883
- instance = built;
1884
- candidate = built.value;
1885
- build = true;
1886
- } else candidate = result.kind === "changed" ? result.value : value;
1887
- }
1888
- changed = !initialized || !(options?.isEqual ?? Object.is)(value, candidate);
1889
- next = changed ? candidate : value;
1890
- reset = build;
1891
- return changed;
1272
+ //#region core/src/projection/projection-readable.ts
1273
+ let activeTracker;
1274
+ const trackKey = (key) => {
1275
+ activeTracker?.keys.add(key);
1276
+ };
1277
+ const trackStructure = () => {
1278
+ if (activeTracker) activeTracker.structure = true;
1279
+ };
1280
+ const trackAll = () => {
1281
+ if (activeTracker) activeTracker.all = true;
1282
+ };
1283
+ const isMapLike = (value) => value instanceof Map || value !== null && typeof value === "object" && typeof value.get === "function" && typeof value.keys === "function" && typeof value.entries === "function";
1284
+ const trackedMap = (source) => {
1285
+ const view = {
1286
+ get: (key) => {
1287
+ trackKey(key);
1288
+ return source.get(key);
1892
1289
  },
1893
- context: (active) => {
1894
- assertScope(active);
1895
- return Object.freeze({
1896
- kind: "value",
1897
- value: next,
1898
- revision: revision + (changed ? 1 : 0),
1899
- reset,
1900
- cause
1901
- });
1290
+ has: (key) => {
1291
+ trackKey(key);
1292
+ return source.has(key);
1902
1293
  },
1903
- revision: () => revision,
1904
- reset: () => reset,
1905
- publish: () => {
1906
- if (changed && initialized) revision++;
1907
- value = next;
1908
- initialized = true;
1294
+ get size() {
1295
+ trackStructure();
1296
+ return source.size;
1909
1297
  },
1910
- emit: (call) => {
1911
- profile.materialized.notification();
1912
- Array.from(listeners).forEach((listener) => call(listener));
1298
+ keys: () => {
1299
+ trackStructure();
1300
+ return source.keys();
1913
1301
  },
1914
- clear: () => {
1915
- next = value;
1916
- changed = false;
1917
- reset = false;
1918
- cause = void 0;
1302
+ values: () => {
1303
+ trackAll();
1304
+ return source.values();
1919
1305
  },
1920
- release: () => {
1921
- listeners.clear();
1922
- instance = void 0;
1923
- value = next = void 0;
1924
- }
1925
- });
1926
- const handle = Object.freeze({
1927
- kind: "value",
1928
- current: () => {
1929
- owner.check();
1930
- return value;
1306
+ entries: () => {
1307
+ trackAll();
1308
+ return source.entries();
1931
1309
  },
1932
- revision: () => {
1933
- owner.check();
1934
- return revision;
1310
+ forEach: (callback, thisArg) => {
1311
+ trackAll();
1312
+ source.forEach((value, key) => callback.call(thisArg, value, key, view));
1935
1313
  },
1936
- subscribe: (listener) => {
1937
- owner.check();
1938
- listeners.add(listener);
1939
- return () => {
1940
- listeners.delete(listener);
1941
- };
1314
+ [Symbol.iterator]: () => {
1315
+ trackAll();
1316
+ return source[Symbol.iterator]();
1942
1317
  }
1943
- });
1944
- owner.install(handle);
1945
- return handle;
1946
- };
1947
- //#endregion
1948
- //#region core/src/projection/collection-state.ts
1949
- const height = (node) => node?.height ?? 0;
1950
- const makeTreeNode = (key, value, left, right) => Object.freeze({
1951
- key,
1952
- value,
1953
- left,
1954
- right,
1955
- height: Math.max(height(left), height(right)) + 1
1956
- });
1957
- const rotateLeft = (node) => {
1958
- const right = node.right;
1959
- return makeTreeNode(right.key, right.value, makeTreeNode(node.key, node.value, node.left, right.left), right.right);
1960
- };
1961
- const rotateRight = (node) => {
1962
- const left = node.left;
1963
- return makeTreeNode(left.key, left.value, left.left, makeTreeNode(node.key, node.value, left.right, node.right));
1964
- };
1965
- const rebalance = (node) => {
1966
- const balance = height(node.left) - height(node.right);
1967
- if (balance > 1) {
1968
- if (height(node.left.left) < height(node.left.right)) return rotateRight(makeTreeNode(node.key, node.value, rotateLeft(node.left), node.right));
1969
- return rotateRight(node);
1970
- }
1971
- if (balance < -1) {
1972
- if (height(node.right.right) < height(node.right.left)) return rotateLeft(makeTreeNode(node.key, node.value, node.left, rotateRight(node.right)));
1973
- return rotateLeft(node);
1974
- }
1975
- return node;
1976
- };
1977
- const treeSet = (node, key, value) => {
1978
- if (!node) return makeTreeNode(key, value);
1979
- if (key === node.key) return makeTreeNode(key, value, node.left, node.right);
1980
- if (key < node.key) return rebalance(makeTreeNode(node.key, node.value, treeSet(node.left, key, value), node.right));
1981
- return rebalance(makeTreeNode(node.key, node.value, node.left, treeSet(node.right, key, value)));
1982
- };
1983
- const treeMin = (node) => node.left ? treeMin(node.left) : node;
1984
- const treeRemove = (node, key) => {
1985
- if (!node) return void 0;
1986
- if (key < node.key) return rebalance(makeTreeNode(node.key, node.value, treeRemove(node.left, key), node.right));
1987
- if (key > node.key) return rebalance(makeTreeNode(node.key, node.value, node.left, treeRemove(node.right, key)));
1988
- if (!node.left) return node.right;
1989
- if (!node.right) return node.left;
1990
- const next = treeMin(node.right);
1991
- return rebalance(makeTreeNode(next.key, next.value, node.left, treeRemove(node.right, next.key)));
1992
- };
1993
- const treeGet = (node, key) => {
1994
- let current = node;
1995
- while (current) {
1996
- if (key === current.key) return current.value;
1997
- current = key < current.key ? current.left : current.right;
1998
- }
1318
+ };
1319
+ return Object.freeze(view);
1999
1320
  };
2000
- const treeHas = (node, key) => {
2001
- let current = node;
2002
- while (current) {
2003
- if (key === current.key) return true;
2004
- current = key < current.key ? current.left : current.right;
2005
- }
1321
+ const selectionAffects = (selection, change) => {
1322
+ if (!change || change.kind === "reset") return true;
1323
+ if (selection.all && (change.added.length || change.removed.length || change.updated.length || change.order)) return true;
1324
+ if (selection.structure && (change.added.length || change.removed.length || change.order)) return true;
1325
+ for (const key of selection.keys) if (change.added.some((entry) => entry.key === key) || change.removed.some((entry) => entry.key === key) || change.updated.some((entry) => entry.key === key)) return true;
2006
1326
  return false;
2007
1327
  };
2008
- const treeFromSorted = (entries, start = 0, end = entries.length) => {
2009
- if (start >= end) return void 0;
2010
- const middle = start + end >> 1;
2011
- const [key, value] = entries[middle];
2012
- return makeTreeNode(key, value, treeFromSorted(entries, start, middle), treeFromSorted(entries, middle + 1, end));
2013
- };
2014
- /**
2015
- * The one keyed publication kernel used by ordinary collection nodes and
2016
- * processor-group output leaves. It owns staged intent, exact transitions and
2017
- * the persistent published snapshot; callers only provide the processor
2018
- * callback and the equality policy.
2019
- */
2020
- const createCollectionState = () => {
2021
- const values = /* @__PURE__ */ new Map();
2022
- let ids = Object.freeze([]);
2023
- let staged = /* @__PURE__ */ new Map();
2024
- let nextIds = ids;
2025
- let change;
1328
+ const createDirectReadable = (source) => Object.freeze({
1329
+ current: source.current,
1330
+ revision: source.revision,
1331
+ subscribe: (listener) => source.subscribe(() => listener())
1332
+ });
1333
+ /** Consumer-side selector tracking; it never mutates processor dependencies. */
1334
+ const createSelectorReadable = (source, selector, equality = Object.is) => {
2026
1335
  let initialized = false;
2027
- let reset = false;
2028
- let revision = 0;
2029
- let publishedRoot;
2030
- let changedKeys = /* @__PURE__ */ new Set();
2031
- let orderChanged = false;
2032
- let explicitOrder;
2033
- let cleared = false;
1336
+ let value;
1337
+ let selectedRevision = 0;
1338
+ let sourceRevision = -1;
1339
+ let selection = Object.freeze({
1340
+ keys: /* @__PURE__ */ new Set(),
1341
+ all: true,
1342
+ structure: false
1343
+ });
1344
+ let previousCollection;
1345
+ let unsubscribeSource;
2034
1346
  const listeners = /* @__PURE__ */ new Set();
2035
- const hasNext = (key) => staged.has(key) ? staged.get(key).present : !cleared && values.has(key);
2036
- const getNext = (key) => {
2037
- const entry = staged.get(key);
2038
- return entry ? entry.present ? entry.value : void 0 : cleared ? void 0 : values.get(key);
2039
- };
2040
- const deriveIds = () => {
2041
- const result = cleared ? [] : ids.filter(hasNext);
2042
- for (const [key, entry] of staged) if (entry.present && (cleared || !values.has(key))) result.push(key);
2043
- return result;
1347
+ const updateCollectionSnapshot = (current) => {
1348
+ const candidate = current ?? source.current();
1349
+ previousCollection = isMapLike(candidate) ? candidate : void 0;
2044
1350
  };
2045
- const begin = (active, nextReset) => {
2046
- staged = /* @__PURE__ */ new Map();
2047
- nextIds = ids;
2048
- change = void 0;
2049
- changedKeys = /* @__PURE__ */ new Set();
2050
- explicitOrder = void 0;
2051
- orderChanged = false;
2052
- cleared = nextReset;
2053
- const read = (next) => ({
2054
- get: (key) => {
2055
- assertScope(active);
2056
- return next ? getNext(key) : values.get(key);
2057
- },
2058
- has: (key) => {
2059
- assertScope(active);
2060
- return next ? hasNext(key) : values.has(key);
2061
- },
2062
- ids: () => {
2063
- assertScope(active);
2064
- return next ? Object.freeze(explicitOrder ? [...explicitOrder] : [...deriveIds()]) : ids;
2065
- }
2066
- });
2067
- return {
2068
- previous: read(false),
2069
- next: read(true),
2070
- output: {
2071
- set: (key, value) => {
2072
- assertScope(active);
2073
- if (typeof key !== "string") throw new TypeError("Projection keys must be strings.");
2074
- staged.set(key, {
2075
- present: true,
2076
- value
2077
- });
2078
- },
2079
- remove: (key) => {
2080
- assertScope(active);
2081
- staged.set(key, { present: false });
2082
- },
2083
- order: (order) => {
2084
- assertScope(active);
2085
- explicitOrder = Object.freeze([...order]);
2086
- }
2087
- }
1351
+ const evaluate = () => {
1352
+ const tracker = {
1353
+ keys: /* @__PURE__ */ new Set(),
1354
+ all: false,
1355
+ structure: false
2088
1356
  };
2089
- };
2090
- const seal = (nextReset, isEqual) => {
2091
- if (cleared) {
2092
- for (const key of values.keys()) if (!staged.has(key)) staged.set(key, { present: false });
1357
+ const previousTracker = activeTracker;
1358
+ activeTracker = tracker;
1359
+ let current;
1360
+ let next;
1361
+ try {
1362
+ current = source.current();
1363
+ next = selector(isMapLike(current) ? trackedMap(current) : current);
1364
+ } finally {
1365
+ activeTracker = previousTracker;
2093
1366
  }
2094
- profile.projection("touchedKeys", staged.size);
2095
- const added = /* @__PURE__ */ new Set();
2096
- const removed = /* @__PURE__ */ new Set();
2097
- const updated = /* @__PURE__ */ new Set();
2098
- for (const [key, entry] of staged) {
2099
- const existed = values.has(key);
2100
- if (entry.present) {
2101
- if (!existed) added.add(key);
2102
- else if (!isEqual(values.get(key), entry.value)) updated.add(key);
2103
- else if (!nextReset) staged.delete(key);
2104
- } else if (existed) removed.add(key);
2105
- else staged.delete(key);
1367
+ const nextSelection = Object.freeze({
1368
+ keys: new Set(tracker.keys),
1369
+ all: tracker.all || isMapLike(current) && tracker.keys.size === 0 && !tracker.structure,
1370
+ structure: tracker.structure
1371
+ });
1372
+ const changed = !initialized || !equality(value, next);
1373
+ if (changed) {
1374
+ if (initialized) selectedRevision++;
1375
+ value = next;
2106
1376
  }
2107
- if (explicitOrder || added.size || removed.size) {
2108
- const order = explicitOrder ?? [...ids.filter((key) => !removed.has(key)), ...added];
2109
- if (new Set(order).size !== order.length || order.length !== values.size + added.size - removed.size || order.some((key) => !hasNext(key))) throw new TypeError("Projection order must contain every key exactly once.");
2110
- nextIds = ids.length === order.length && ids.every((key, index) => key === order[index]) ? ids : Object.freeze([...order]);
2111
- } else nextIds = ids;
2112
- orderChanged = nextIds !== ids;
2113
- for (const key of added) changedKeys.add(key);
2114
- for (const key of removed) changedKeys.add(key);
2115
- for (const key of updated) changedKeys.add(key);
2116
- profile.projection("changedKeys", changedKeys.size);
2117
- const addedEntries = [];
2118
- const updatedEntries = [];
2119
- const removedEntries = [];
2120
- for (const key of changedKeys) {
2121
- const beforePresent = values.has(key);
2122
- const afterPresent = hasNext(key);
2123
- if (beforePresent === afterPresent) {
2124
- if (!beforePresent || isEqual(values.get(key), getNext(key))) continue;
2125
- }
2126
- if (!beforePresent) addedEntries.push({
2127
- key,
2128
- after: getNext(key)
2129
- });
2130
- else if (!afterPresent) removedEntries.push({
2131
- key,
2132
- before: values.get(key)
2133
- });
2134
- else updatedEntries.push({
2135
- key,
2136
- before: values.get(key),
2137
- after: getNext(key)
2138
- });
1377
+ selection = nextSelection;
1378
+ initialized = true;
1379
+ sourceRevision = source.revision();
1380
+ updateCollectionSnapshot(current);
1381
+ return changed;
1382
+ };
1383
+ const ensureCurrent = () => {
1384
+ const revision = source.revision();
1385
+ if (!initialized || !unsubscribeSource && sourceRevision !== revision) evaluate();
1386
+ return value;
1387
+ };
1388
+ const onProjectionChange = (incoming) => {
1389
+ let change = incoming;
1390
+ if (!change && previousCollection) {
1391
+ const current = source.current();
1392
+ if (isMapLike(current)) {
1393
+ change = collectionChangeBetween(previousCollection, current);
1394
+ previousCollection = current;
1395
+ if (!change) {
1396
+ sourceRevision = source.revision();
1397
+ return;
1398
+ }
1399
+ } else previousCollection = void 0;
2139
1400
  }
2140
- if (nextReset) change = Object.freeze({ kind: "reset" });
2141
- else if (addedEntries.length || updatedEntries.length || removedEntries.length || orderChanged) {
2142
- let commonOrderChanged = false;
2143
- if (orderChanged) {
2144
- const before = ids.filter((key) => !removed.has(key));
2145
- const after = nextIds.filter((key) => !added.has(key));
2146
- commonOrderChanged = before.some((key, index) => after[index] !== key);
2147
- }
2148
- change = Object.freeze({
2149
- kind: "incremental",
2150
- added: Object.freeze(addedEntries),
2151
- updated: Object.freeze(updatedEntries),
2152
- removed: Object.freeze(removedEntries),
2153
- ...commonOrderChanged ? { order: Object.freeze({
2154
- before: Object.freeze([...ids]),
2155
- after: Object.freeze([...nextIds])
2156
- }) } : {}
2157
- });
1401
+ if (!selectionAffects(selection, change)) {
1402
+ sourceRevision = source.revision();
1403
+ updateCollectionSnapshot();
1404
+ return;
2158
1405
  }
2159
- reset = nextReset;
2160
- return change !== void 0;
1406
+ if (evaluate()) Array.from(listeners).forEach((listener) => listener());
2161
1407
  };
2162
- const context = (active, cause) => Object.freeze({
2163
- kind: "collection",
2164
- read: {
2165
- get: (key) => {
2166
- assertScope(active);
2167
- return getNext(key);
2168
- },
2169
- has: (key) => {
2170
- assertScope(active);
2171
- return hasNext(key);
2172
- },
2173
- ids: () => {
2174
- assertScope(active);
2175
- return nextIds;
2176
- }
1408
+ const installSource = () => {
1409
+ if (!unsubscribeSource) unsubscribeSource = source.subscribe(onProjectionChange);
1410
+ };
1411
+ return Object.freeze({
1412
+ current: ensureCurrent,
1413
+ revision: () => {
1414
+ ensureCurrent();
1415
+ return selectedRevision;
2177
1416
  },
2178
- change,
2179
- revision: revision + (change ? 1 : 0),
2180
- cause
1417
+ subscribe: (listener) => {
1418
+ ensureCurrent();
1419
+ listeners.add(listener);
1420
+ installSource();
1421
+ return () => {
1422
+ listeners.delete(listener);
1423
+ if (!listeners.size && unsubscribeSource) {
1424
+ unsubscribeSource();
1425
+ unsubscribeSource = void 0;
1426
+ }
1427
+ };
1428
+ }
2181
1429
  });
2182
- const current = (check) => {
2183
- const root = publishedRoot;
2184
- const publishedIds = ids;
2185
- return Object.freeze({
2186
- get: (key) => {
2187
- check();
2188
- return treeGet(root, key);
2189
- },
2190
- has: (key) => {
2191
- check();
2192
- return treeHas(root, key);
2193
- },
2194
- ids: () => {
2195
- check();
2196
- return publishedIds;
2197
- }
2198
- });
1430
+ };
1431
+ //#endregion
1432
+ //#region core/src/projection/source.ts
1433
+ const sourceError = (source, cause) => new ProjectionError("source", source.name, [source.outputs[0].revision()], cause);
1434
+ const createValueBoundary = (scheduler, name, initial, equality) => {
1435
+ const state = createValueOutput();
1436
+ let boundary;
1437
+ let latest = initial;
1438
+ let pendingReset = false;
1439
+ let pendingCause;
1440
+ let contextCause;
1441
+ let recovering = false;
1442
+ let cleanup;
1443
+ const check = () => {
1444
+ if (!scheduler.active || boundary.disposed) throw new ProjectionDisposedError();
1445
+ if (boundary.fault) throw boundary.fault;
2199
1446
  };
2200
- return {
2201
- begin,
2202
- seal,
2203
- context,
2204
- current,
2205
- revision: () => revision,
2206
- reset: () => reset,
2207
- publish: () => {
2208
- if (change) if (change.kind === "reset") {
2209
- const entries = [];
2210
- for (const [key, entry] of staged) if (entry.present) entries.push([key, entry.value]);
2211
- entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
2212
- publishedRoot = treeFromSorted(entries);
2213
- } else for (const [key, entry] of staged) publishedRoot = entry.present ? treeSet(publishedRoot, key, entry.value) : treeRemove(publishedRoot, key);
2214
- for (const [key, entry] of staged) if (entry.present) values.set(key, entry.value);
2215
- else values.delete(key);
2216
- if (change && initialized) revision++;
2217
- ids = nextIds;
2218
- initialized = true;
1447
+ const output = {
1448
+ kind: "value",
1449
+ get owner() {
1450
+ return boundary;
2219
1451
  },
2220
- emit: (call) => {
2221
- const resetEvent = Object.freeze({ kind: "reset" });
2222
- Array.from(listeners).forEach((listener) => call(() => listener(change ?? resetEvent)));
1452
+ consumers: /* @__PURE__ */ new Set(),
1453
+ context: (active) => state.context(active, contextCause),
1454
+ current: () => state.current(check),
1455
+ revision: state.revision,
1456
+ reset: state.reset,
1457
+ subscribe: (listener) => {
1458
+ check();
1459
+ const wrapped = () => listener();
1460
+ state.subscribe(wrapped);
1461
+ return () => state.unsubscribe(wrapped);
1462
+ },
1463
+ emit: state.emit,
1464
+ clear: state.clear,
1465
+ release: state.release
1466
+ };
1467
+ boundary = {
1468
+ kind: "source",
1469
+ name,
1470
+ outputs: Object.freeze([output]),
1471
+ fault: void 0,
1472
+ disposed: false,
1473
+ pendingCause: () => pendingCause,
1474
+ recovering: () => recovering,
1475
+ prepare: (cause) => {
1476
+ contextCause = cause;
1477
+ let active = true;
1478
+ try {
1479
+ state.begin(() => active, pendingReset).output.set(latest);
1480
+ const changed = state.seal(pendingReset, equality);
1481
+ recovering = false;
1482
+ return {
1483
+ changed,
1484
+ force: pendingReset
1485
+ };
1486
+ } finally {
1487
+ active = false;
1488
+ }
2223
1489
  },
1490
+ publish: state.publish,
2224
1491
  clear: () => {
2225
- staged.clear();
2226
- nextIds = ids;
2227
- change = void 0;
2228
- reset = false;
2229
- changedKeys.clear();
2230
- explicitOrder = void 0;
2231
- orderChanged = false;
2232
- cleared = false;
1492
+ state.clear();
1493
+ pendingReset = false;
1494
+ pendingCause = void 0;
1495
+ contextCause = void 0;
1496
+ recovering = false;
2233
1497
  },
2234
1498
  release: () => {
2235
- values.clear();
2236
- staged.clear();
2237
- publishedRoot = void 0;
2238
- listeners.clear();
2239
- ids = nextIds = Object.freeze([]);
2240
- change = void 0;
2241
- initialized = false;
2242
- revision = 0;
2243
- },
2244
- subscribe: (listener) => {
2245
- listeners.add(listener);
1499
+ cleanup?.();
1500
+ cleanup = void 0;
1501
+ state.release();
1502
+ output.consumers.clear();
1503
+ }
1504
+ };
1505
+ scheduler.initialize(() => {
1506
+ let active = true;
1507
+ try {
1508
+ state.begin(() => active, true).output.set(initial);
1509
+ state.seal(true, equality);
1510
+ state.publish();
1511
+ state.clear();
1512
+ } finally {
1513
+ active = false;
1514
+ }
1515
+ });
1516
+ scheduler.addSource(boundary);
1517
+ const mark = (metadata = {}) => {
1518
+ scheduler.assertIdle();
1519
+ pendingReset ||= metadata.reset ?? false;
1520
+ if (metadata.cause !== void 0) pendingCause ??= metadata.cause;
1521
+ if (boundary.fault) recovering = true;
1522
+ scheduler.capture(boundary);
1523
+ };
1524
+ const fail = (cause) => {
1525
+ boundary.fault = sourceError(boundary, cause);
1526
+ recovering = false;
1527
+ scheduler.capture(boundary);
1528
+ };
1529
+ return {
1530
+ producer: boundary,
1531
+ output,
1532
+ setLatest: (value) => {
1533
+ latest = value;
2246
1534
  },
2247
- unsubscribe: (listener) => {
2248
- listeners.delete(listener);
1535
+ mark,
1536
+ fail,
1537
+ detach: (next) => {
1538
+ cleanup = next;
2249
1539
  }
2250
1540
  };
2251
1541
  };
2252
- //#endregion
2253
- //#region core/src/projection/collection.ts
2254
- const createCollection = (scheduler, spec) => {
2255
- const state = createCollectionState();
2256
- let instance;
2257
- let cause;
2258
- const owner = createNode(scheduler, spec, {
2259
- evaluate: (sources, build, active) => {
2260
- cause = Object.values(sources).find((value) => value && typeof value === "object" && "cause" in value && value.cause !== void 0)?.cause ?? scheduler.batchContext()?.cause;
2261
- const input = state.begin(active, build);
2262
- if (build || !instance) {
2263
- profile.materialized.rebuilt();
2264
- instance = void 0;
2265
- const built = spec.build({
2266
- sources,
2267
- ...input
2268
- });
2269
- assertSynchronous(built);
2270
- instance = built;
2271
- } else {
2272
- profile.materialized.updated();
2273
- const result = instance.update({
2274
- sources,
2275
- ...input
2276
- });
2277
- assertSynchronous(result);
2278
- if (result?.kind === "rebuild") {
2279
- const rebuildInput = state.begin(active, true);
2280
- instance = void 0;
2281
- const built = spec.build({
2282
- sources,
2283
- ...rebuildInput
2284
- });
2285
- assertSynchronous(built);
2286
- instance = built;
2287
- build = true;
2288
- }
2289
- }
2290
- return state.seal(build, spec.isEqual ?? Object.is);
1542
+ const createCollectionBoundary = (scheduler, name, readLatest, equality) => {
1543
+ const state = createCollectionOutput();
1544
+ let boundary;
1545
+ let pendingReset = false;
1546
+ let pendingCause;
1547
+ let contextCause;
1548
+ let recovering = false;
1549
+ let fullScan = false;
1550
+ let orderMayChange = false;
1551
+ const candidates = /* @__PURE__ */ new Set();
1552
+ let cleanup;
1553
+ const check = () => {
1554
+ if (!scheduler.active || boundary.disposed) throw new ProjectionDisposedError();
1555
+ if (boundary.fault) throw boundary.fault;
1556
+ };
1557
+ const output = {
1558
+ kind: "collection",
1559
+ get owner() {
1560
+ return boundary;
2291
1561
  },
2292
- context: (active) => state.context(active, cause),
1562
+ consumers: /* @__PURE__ */ new Set(),
1563
+ context: (active) => state.context(active, contextCause),
1564
+ current: () => state.current(check),
2293
1565
  revision: state.revision,
2294
1566
  reset: state.reset,
2295
- publish: state.publish,
1567
+ subscribe: (listener) => {
1568
+ check();
1569
+ const wrapped = (change) => listener(change);
1570
+ state.subscribe(wrapped);
1571
+ return () => state.unsubscribe(wrapped);
1572
+ },
2296
1573
  emit: state.emit,
2297
1574
  clear: state.clear,
1575
+ release: state.release
1576
+ };
1577
+ boundary = {
1578
+ kind: "source",
1579
+ name,
1580
+ outputs: Object.freeze([output]),
1581
+ fault: void 0,
1582
+ disposed: false,
1583
+ pendingCause: () => pendingCause,
1584
+ recovering: () => recovering,
1585
+ prepare: (cause) => {
1586
+ contextCause = cause;
1587
+ let active = true;
1588
+ try {
1589
+ const changed = stageCollectionRead(state, readLatest(() => active), () => active, {
1590
+ reset: pendingReset,
1591
+ ...fullScan ? {} : { candidates },
1592
+ orderMayChange,
1593
+ isEqual: equality
1594
+ });
1595
+ recovering = false;
1596
+ return {
1597
+ changed,
1598
+ force: pendingReset
1599
+ };
1600
+ } finally {
1601
+ active = false;
1602
+ }
1603
+ },
1604
+ publish: state.publish,
1605
+ clear: () => {
1606
+ state.clear();
1607
+ pendingReset = false;
1608
+ pendingCause = void 0;
1609
+ contextCause = void 0;
1610
+ recovering = false;
1611
+ fullScan = false;
1612
+ orderMayChange = false;
1613
+ candidates.clear();
1614
+ },
2298
1615
  release: () => {
1616
+ cleanup?.();
1617
+ cleanup = void 0;
2299
1618
  state.release();
2300
- instance = void 0;
1619
+ output.consumers.clear();
1620
+ }
1621
+ };
1622
+ scheduler.initialize(() => {
1623
+ let active = true;
1624
+ try {
1625
+ stageCollectionRead(state, readLatest(() => active), () => active, {
1626
+ reset: true,
1627
+ isEqual: equality
1628
+ });
1629
+ state.publish();
1630
+ state.clear();
1631
+ } finally {
1632
+ active = false;
2301
1633
  }
2302
1634
  });
2303
- const handle = {
2304
- kind: "collection",
2305
- current: () => {
2306
- owner.check();
2307
- return state.current(owner.check);
2308
- },
2309
- revision: () => {
2310
- owner.check();
2311
- return state.revision();
2312
- },
2313
- subscribe: (listener) => {
2314
- owner.check();
2315
- state.subscribe(listener);
2316
- return () => state.unsubscribe(listener);
1635
+ scheduler.addSource(boundary);
1636
+ const mark = (metadata = {}) => {
1637
+ scheduler.assertIdle();
1638
+ pendingReset ||= metadata.reset ?? false;
1639
+ if (metadata.cause !== void 0) pendingCause ??= metadata.cause;
1640
+ fullScan ||= metadata.fullScan ?? false;
1641
+ orderMayChange ||= metadata.orderMayChange ?? false;
1642
+ if (metadata.candidates) for (const key of metadata.candidates) candidates.add(key);
1643
+ if (boundary.fault) recovering = true;
1644
+ scheduler.capture(boundary);
1645
+ };
1646
+ const fail = (cause) => {
1647
+ boundary.fault = sourceError(boundary, cause);
1648
+ recovering = false;
1649
+ scheduler.capture(boundary);
1650
+ };
1651
+ return {
1652
+ producer: boundary,
1653
+ output,
1654
+ mark,
1655
+ fail,
1656
+ detach: (next) => {
1657
+ cleanup = next;
2317
1658
  }
2318
1659
  };
2319
- owner.install(handle);
2320
- return Object.freeze(handle);
2321
1660
  };
2322
- //#endregion
2323
- //#region core/src/projection/group.ts
2324
- /**
2325
- * Materializes one processor callback with several keyed output leaves. The
2326
- * scheduler sees one node and the output leaves remain ordinary source records
2327
- * for dependency, revision and listener matching.
2328
- */
2329
- const createCollectionGroup = (scheduler, spec) => {
2330
- scheduler.assertIdle();
2331
- const sources = Object.entries(spec.sources).map(([key, handle]) => [key, scheduler.source(handle)]);
2332
- const states = spec.outputs.map(() => createCollectionState());
2333
- let instance;
2334
- let cause;
2335
- let changedOutputs = Object.freeze([]);
2336
- let node;
2337
- const order = scheduler.order();
2338
- const outputs = states.map((state, index) => {
2339
- const output = {
2340
- owner: void 0,
2341
- consumers: /* @__PURE__ */ new Set(),
2342
- context: (active) => state.context(active, cause),
2343
- revision: state.revision,
2344
- reset: state.reset,
2345
- fault: void 0,
2346
- disposed: false,
2347
- clear: state.clear,
2348
- emit: state.emit
1661
+ const createSourceRegistry = (scheduler) => {
1662
+ const documents = /* @__PURE__ */ new Map();
1663
+ const readables = /* @__PURE__ */ new Map();
1664
+ const externalValues = /* @__PURE__ */ new WeakMap();
1665
+ const externalCollections = /* @__PURE__ */ new WeakMap();
1666
+ const documentConnection = (runtime) => {
1667
+ const state = accessOf(runtime);
1668
+ const cached = documents.get(state);
1669
+ if (cached) return cached;
1670
+ if (state.disposed) throw new Error("Document has been disposed.");
1671
+ const bindings = /* @__PURE__ */ new Set();
1672
+ const guard = (locked) => {
1673
+ state.projectionLocks = (state.projectionLocks ?? 0) + (locked ? 1 : -1);
1674
+ };
1675
+ scheduler.guards.add(guard);
1676
+ let closed = false;
1677
+ let detach = () => void 0;
1678
+ const connection = {
1679
+ add(binding) {
1680
+ if (closed) throw new Error("Document projection connection is closed.");
1681
+ bindings.add(binding);
1682
+ let active = true;
1683
+ return () => {
1684
+ if (!active) return;
1685
+ active = false;
1686
+ bindings.delete(binding);
1687
+ if (!bindings.size) connection.close();
1688
+ };
1689
+ },
1690
+ close() {
1691
+ if (closed) return;
1692
+ closed = true;
1693
+ detach();
1694
+ bindings.clear();
1695
+ scheduler.guards.delete(guard);
1696
+ documents.delete(state);
1697
+ }
2349
1698
  };
2350
- Object.defineProperty(output, "fault", {
2351
- configurable: false,
2352
- enumerable: true,
2353
- get: () => node.fault,
2354
- set: (value) => {
2355
- node.fault = value;
1699
+ detach = attachProjection(runtime, {
1700
+ capture: (commit) => {
1701
+ for (const binding of bindings) if (binding.matches(commit)) binding.capture(commit);
1702
+ },
1703
+ settle: scheduler.settle,
1704
+ flush: scheduler.flush,
1705
+ dispose: () => {
1706
+ for (const binding of bindings) binding.dispose();
1707
+ scheduler.run();
2356
1708
  }
2357
1709
  });
2358
- return output;
2359
- });
2360
- const check = () => {
2361
- if (node.disposed || !scheduler.active) throw new ProjectionDisposedError();
2362
- if (node.fault) throw node.fault;
1710
+ documents.set(state, connection);
1711
+ return connection;
2363
1712
  };
2364
- const evaluate = (inputSources, build, active) => {
2365
- cause = Object.values(inputSources).find((value) => value && typeof value === "object" && "cause" in value && value.cause !== void 0)?.cause ?? scheduler.batchContext()?.cause;
2366
- const makeInput = (reset) => {
2367
- const evaluations = states.map((state) => state.begin(active, reset));
2368
- return {
2369
- sources: inputSources,
2370
- previous: evaluations.map((evaluation) => evaluation.previous),
2371
- next: evaluations.map((evaluation) => evaluation.next),
2372
- outputs: evaluations.map((evaluation) => evaluation.output)
2373
- };
1713
+ const valueInput = (definition) => {
1714
+ if (definition.source.kind !== "value-input") throw new Error("Invalid value input source.");
1715
+ const source = definition.source;
1716
+ const boundary = createValueBoundary(scheduler, "projection input", source.initial, source.equality);
1717
+ return {
1718
+ ...boundary,
1719
+ write: {
1720
+ kind: "value",
1721
+ set(value) {
1722
+ scheduler.assertIdle();
1723
+ boundary.setLatest(value);
1724
+ boundary.mark();
1725
+ scheduler.run();
1726
+ }
1727
+ }
2374
1728
  };
2375
- let input = makeInput(build);
2376
- if (build || !instance) {
2377
- profile.materialized.rebuilt();
2378
- instance = void 0;
2379
- const built = spec.build(input);
2380
- assertSynchronous(built);
2381
- instance = built;
2382
- } else {
2383
- profile.materialized.updated();
2384
- const result = instance.update(input);
2385
- assertSynchronous(result);
2386
- if (result?.kind === "rebuild") {
2387
- input = makeInput(true);
2388
- instance = void 0;
2389
- const built = spec.build(input);
2390
- assertSynchronous(built);
2391
- instance = built;
2392
- build = true;
1729
+ };
1730
+ const collectionInput = (definition) => {
1731
+ if (definition.source.kind !== "collection-input") throw new Error("Invalid collection input source.");
1732
+ const values = new Map(definition.source.initial);
1733
+ const boundary = createCollectionBoundary(scheduler, "projection collection input", () => mapRead(values), definition.output.equality);
1734
+ return {
1735
+ ...boundary,
1736
+ write: {
1737
+ kind: "collection",
1738
+ update(run) {
1739
+ scheduler.assertIdle();
1740
+ const staged = /* @__PURE__ */ new Map();
1741
+ const operations = [];
1742
+ let active = true;
1743
+ const draft = Object.freeze({
1744
+ get: (key) => {
1745
+ assertScope(() => active);
1746
+ const entry = staged.get(key);
1747
+ return entry ? entry.present ? entry.value : void 0 : values.get(key);
1748
+ },
1749
+ has: (key) => {
1750
+ assertScope(() => active);
1751
+ const entry = staged.get(key);
1752
+ return entry ? entry.present : values.has(key);
1753
+ },
1754
+ set: (key, value) => {
1755
+ assertScope(() => active);
1756
+ if (typeof key !== "string") throw new TypeError("Projection keys must be strings.");
1757
+ staged.set(key, {
1758
+ present: true,
1759
+ value
1760
+ });
1761
+ operations.push({
1762
+ kind: "set",
1763
+ key,
1764
+ value
1765
+ });
1766
+ },
1767
+ remove: (key) => {
1768
+ assertScope(() => active);
1769
+ if (typeof key !== "string") throw new TypeError("Projection keys must be strings.");
1770
+ staged.set(key, { present: false });
1771
+ operations.push({
1772
+ kind: "remove",
1773
+ key
1774
+ });
1775
+ }
1776
+ });
1777
+ try {
1778
+ assertSynchronous(run(draft));
1779
+ } finally {
1780
+ active = false;
1781
+ }
1782
+ const candidates = /* @__PURE__ */ new Set();
1783
+ let structural = false;
1784
+ let changed = false;
1785
+ for (const operation of operations) {
1786
+ const present = values.has(operation.key);
1787
+ if (operation.kind === "set") {
1788
+ if (present && Object.is(values.get(operation.key), operation.value)) continue;
1789
+ structural ||= !present;
1790
+ values.set(operation.key, operation.value);
1791
+ } else {
1792
+ if (!present) continue;
1793
+ structural = true;
1794
+ values.delete(operation.key);
1795
+ }
1796
+ candidates.add(operation.key);
1797
+ changed = true;
1798
+ }
1799
+ if (!changed) return;
1800
+ boundary.mark({
1801
+ candidates,
1802
+ orderMayChange: structural
1803
+ });
1804
+ scheduler.run();
1805
+ }
2393
1806
  }
2394
- }
2395
- changedOutputs = Object.freeze(states.flatMap((state, index) => state.seal(build, spec.outputs[index].isEqual ?? Object.is) ? [outputs[index]] : []));
2396
- return changedOutputs.length > 0;
1807
+ };
2397
1808
  };
2398
- node = {
2399
- owner: void 0,
2400
- order,
2401
- name: `processor-group (${order})`,
2402
- sources: sources.map(([, source]) => source),
2403
- consumers: /* @__PURE__ */ new Set(),
2404
- outputs,
2405
- disposed: false,
2406
- fault: void 0,
2407
- context: (active) => {
2408
- assertScope(active);
2409
- return Object.freeze({ kind: "group" });
2410
- },
2411
- revision: () => Math.max(0, ...states.map((state) => state.revision())),
2412
- reset: () => states.some((state) => state.reset()),
2413
- evaluate: (build) => {
2414
- let active = true;
2415
- try {
2416
- const scopeActive = () => active;
2417
- return evaluate(Object.fromEntries(sources.map(([key, source]) => [key, source.context(scopeActive)])), build, scopeActive);
2418
- } finally {
2419
- active = false;
1809
+ const documentValue = (definition) => {
1810
+ if (definition.source.kind !== "document-value") throw new Error("Invalid document value source.");
1811
+ const { document, selector } = definition.source;
1812
+ const state = accessOf(document);
1813
+ const address = selector?.address ?? Object.freeze([]);
1814
+ const schemaNode = () => nodeAt(state.schema, address, state.document);
1815
+ const read = () => {
1816
+ if (state.disposed) throw new Error("Document has been disposed.");
1817
+ const node = schemaNode();
1818
+ if (!node) return void 0;
1819
+ return copyValue(node, selector ? read$1(state.document, address, state.schema) : state.document);
1820
+ };
1821
+ const boundary = createValueBoundary(scheduler, "document source", read(), (left, right) => {
1822
+ const node = schemaNode();
1823
+ return node ? equalValue(node, left, right) : Object.is(left, right);
1824
+ });
1825
+ const remove = documentConnection(document).add({
1826
+ matches: (commit) => !selector || affectsTarget(commit.impact, selector),
1827
+ capture: (commit) => {
1828
+ try {
1829
+ boundary.setLatest(read());
1830
+ boundary.mark({ reset: commit.impact.kind === "reset" });
1831
+ } catch (cause) {
1832
+ boundary.fail(cause);
1833
+ }
1834
+ },
1835
+ dispose: () => boundary.fail(/* @__PURE__ */ new Error("Document has been disposed."))
1836
+ });
1837
+ boundary.detach(remove);
1838
+ return boundary;
1839
+ };
1840
+ const documentCollection = (definition) => {
1841
+ if (definition.source.kind !== "document-collection") throw new Error("Invalid document collection source.");
1842
+ const { document, selector } = definition.source;
1843
+ const state = accessOf(document);
1844
+ const read = (active) => {
1845
+ if (state.disposed) throw new Error("Document has been disposed.");
1846
+ const raw = collectionAccess({
1847
+ state,
1848
+ active
1849
+ }, selector.address);
1850
+ const snapshots = /* @__PURE__ */ new Map();
1851
+ return Object.freeze({
1852
+ get: (key) => {
1853
+ assertScope(active);
1854
+ if (!snapshots.has(key)) snapshots.set(key, snapshot(raw.get(key)));
1855
+ return snapshots.get(key);
1856
+ },
1857
+ has: (key) => {
1858
+ assertScope(active);
1859
+ return raw.has(key);
1860
+ },
1861
+ ids: () => {
1862
+ assertScope(active);
1863
+ return raw.ids();
1864
+ }
1865
+ });
1866
+ };
1867
+ const collectionNode = () => nodeAt(state.schema, selector.address, state.document);
1868
+ const boundary = createCollectionBoundary(scheduler, "document collection source", read, (left, right) => {
1869
+ const node = collectionNode();
1870
+ const entry = node && collectionEntryNode(node);
1871
+ return entry ? equalValue(entry, left, right) : Object.is(left, right);
1872
+ });
1873
+ const remove = documentConnection(document).add({
1874
+ matches: (commit) => affectsTarget(commit.impact, selector),
1875
+ capture: (commit) => {
1876
+ const impact = collectionImpact(commit.impact, selector);
1877
+ if (impact.kind === "reset") {
1878
+ boundary.mark({ reset: true });
1879
+ return;
1880
+ }
1881
+ const candidates = new Set([
1882
+ ...impact.added,
1883
+ ...impact.updated,
1884
+ ...impact.removed
1885
+ ]);
1886
+ boundary.mark({
1887
+ candidates,
1888
+ orderMayChange: Boolean(impact.added.size || impact.removed.size || impact.orderChanged)
1889
+ });
1890
+ },
1891
+ dispose: () => boundary.fail(/* @__PURE__ */ new Error("Document has been disposed."))
1892
+ });
1893
+ boundary.detach(remove);
1894
+ return boundary;
1895
+ };
1896
+ const readable = (definition) => {
1897
+ if (definition.source.kind !== "readable") throw new Error("Invalid Readable source.");
1898
+ const { readable, equality } = definition.source;
1899
+ let initial = readable.current();
1900
+ const boundary = createValueBoundary(scheduler, "external readable", initial, equality);
1901
+ const member = {
1902
+ boundary,
1903
+ receive(value, metadata) {
1904
+ initial = value;
1905
+ boundary.setLatest(value);
1906
+ boundary.mark(metadata);
2420
1907
  }
2421
- },
2422
- changedOutputs: () => changedOutputs,
2423
- publish: () => {
2424
- if (node.fault) states.forEach((state) => state.clear());
2425
- else states.forEach((state) => state.publish());
2426
- },
2427
- emit: () => void 0,
2428
- clear: () => {
2429
- states.forEach((state) => state.clear());
2430
- changedOutputs = Object.freeze([]);
2431
- cause = void 0;
2432
- },
2433
- release: () => {
2434
- states.forEach((state) => state.release());
2435
- outputs.forEach((output) => {
2436
- output.disposed = true;
2437
- output.consumers.clear();
1908
+ };
1909
+ let connection = readables.get(readable);
1910
+ if (!connection) {
1911
+ const members = /* @__PURE__ */ new Set();
1912
+ let closed = false;
1913
+ let unsubscribe;
1914
+ let detachDocument;
1915
+ const receive = (settle) => {
1916
+ if (closed || !scheduler.active) return;
1917
+ try {
1918
+ const value = readable.current();
1919
+ members.forEach((next) => next.receive(value));
1920
+ } catch (cause) {
1921
+ members.forEach((next) => next.boundary.fail(cause));
1922
+ }
1923
+ if (settle) scheduler.run();
1924
+ };
1925
+ const created = {
1926
+ members,
1927
+ close() {
1928
+ if (closed) return;
1929
+ closed = true;
1930
+ unsubscribe?.();
1931
+ detachDocument?.();
1932
+ readables.delete(readable);
1933
+ }
1934
+ };
1935
+ const owner = documentReadableOwner(readable);
1936
+ if (owner) detachDocument = documentConnection(owner).add({
1937
+ matches: () => true,
1938
+ capture: () => receive(false),
1939
+ dispose: () => {
1940
+ members.forEach((next) => next.boundary.fail(/* @__PURE__ */ new Error("Document has been disposed.")));
1941
+ }
2438
1942
  });
2439
- instance = void 0;
1943
+ unsubscribe = readable.subscribe(() => receive(true));
1944
+ connection = created;
1945
+ readables.set(readable, connection);
2440
1946
  }
1947
+ connection.members.add(member);
1948
+ boundary.detach(() => {
1949
+ connection.members.delete(member);
1950
+ if (!connection.members.size) connection.close();
1951
+ });
1952
+ return boundary;
2441
1953
  };
2442
- node.owner = node;
2443
- outputs.forEach((output) => {
2444
- output.owner = node;
2445
- });
2446
- scheduler.initialize(() => {
2447
- const invalid = node.sources.find((source) => source.fault || source.disposed);
2448
- if (invalid) throw invalid.fault ?? new ProjectionDisposedError();
2449
- node.evaluate(true);
2450
- node.publish();
2451
- node.clear();
2452
- });
2453
- const handles = outputs.map((output, index) => {
2454
- const handle = {
2455
- kind: "collection",
2456
- current: () => {
2457
- check();
2458
- return states[index].current(check);
2459
- },
2460
- revision: () => {
2461
- check();
2462
- return states[index].revision();
2463
- },
2464
- subscribe: (listener) => {
2465
- check();
2466
- states[index].subscribe(listener);
2467
- return () => states[index].unsubscribe(listener);
1954
+ const externalValue = (definition) => {
1955
+ if (definition.source.kind !== "external-value") throw new Error("Invalid external value source.");
1956
+ const { source, equality } = definition.source;
1957
+ const boundary = createValueBoundary(scheduler, "external source", source.current(), equality);
1958
+ const member = {
1959
+ boundary,
1960
+ receive(value, metadata) {
1961
+ boundary.setLatest(value);
1962
+ boundary.mark(metadata);
2468
1963
  }
2469
1964
  };
2470
- scheduler.register(handle, output);
2471
- return Object.freeze(handle);
2472
- });
2473
- scheduler.addNode(node);
2474
- return Object.freeze(handles);
2475
- };
2476
- //#endregion
2477
- //#region core/src/projection/graph.ts
2478
- const createProjectionGraph = (options) => {
2479
- const scheduler = createScheduler(options.onError);
2480
- const sources = createSources(scheduler);
2481
- const value = (spec, options) => createValue(scheduler, spec, options);
2482
- const map = (source, mapper, options) => {
2483
- sources.assertCollection(source);
2484
- const calculate = (id, entry) => {
2485
- const value = mapper(id, entry);
2486
- assertSynchronous(value);
2487
- return value;
2488
- };
2489
- return createCollection(scheduler, {
2490
- sources: { source },
2491
- isEqual: options?.isEqual,
2492
- build: ({ sources, output }) => {
2493
- const read = sources.source.read;
2494
- const ids = read.ids();
2495
- profile.collectionView.idsScanned(ids.length);
2496
- for (const id of ids) {
2497
- profile.collectionView.mapped();
2498
- output.set(id, calculate(id, read.get(id)));
1965
+ let connection = externalValues.get(source);
1966
+ if (!connection) {
1967
+ const members = /* @__PURE__ */ new Set();
1968
+ let closed = false;
1969
+ let unsubscribe;
1970
+ const created = {
1971
+ members,
1972
+ close() {
1973
+ if (closed) return;
1974
+ closed = true;
1975
+ unsubscribe?.();
1976
+ externalValues.delete(source);
2499
1977
  }
2500
- output.order(ids);
2501
- return { update: ({ sources, output }) => {
2502
- const input = sources.source;
2503
- const read = input.read;
2504
- if (input.change?.kind === "reset") return { kind: "rebuild" };
2505
- const change = input.change;
2506
- if (!change) return;
2507
- for (const entry of change.added) {
2508
- profile.collectionView.mapped();
2509
- output.set(entry.key, calculate(entry.key, entry.after));
2510
- }
2511
- for (const entry of change.updated) {
2512
- profile.collectionView.mapped();
2513
- output.set(entry.key, calculate(entry.key, entry.after));
2514
- }
2515
- for (const entry of change.removed) output.remove(entry.key);
2516
- if (change.added.length || change.removed.length || change.order) {
2517
- const ids = read.ids();
2518
- profile.collectionView.idsScanned(ids.length);
2519
- output.order(ids);
1978
+ };
1979
+ try {
1980
+ unsubscribe = source.subscribe((event) => {
1981
+ try {
1982
+ members.forEach((next) => next.receive(event.value, {
1983
+ reset: event.reset,
1984
+ cause: event.cause
1985
+ }));
1986
+ } catch (cause) {
1987
+ members.forEach((next) => next.boundary.fail(cause));
2520
1988
  }
2521
- } };
1989
+ scheduler.run();
1990
+ });
1991
+ } catch (cause) {
1992
+ created.close();
1993
+ throw cause;
2522
1994
  }
1995
+ connection = created;
1996
+ externalValues.set(source, connection);
1997
+ }
1998
+ connection.members.add(member);
1999
+ boundary.detach(() => {
2000
+ connection.members.delete(member);
2001
+ if (!connection.members.size) connection.close();
2523
2002
  });
2003
+ return boundary;
2524
2004
  };
2525
- const graph = {
2526
- document: sources.document,
2527
- fromSource: (source, options) => sources.fromSource(source, options),
2528
- fromCollectionSource: (source) => sources.fromCollectionSource(source),
2529
- input: sources.input,
2530
- collectionInput: sources.collectionInput,
2531
- fromReadable: sources.fromReadable,
2532
- value,
2533
- collection: (spec) => createCollection(scheduler, spec),
2534
- group: (spec) => createCollectionGroup(scheduler, spec),
2535
- map,
2536
- batch: scheduler.batch,
2537
- assertIdle: scheduler.assertIdle,
2538
- releaseNode: scheduler.releaseNode,
2539
- releaseInput: scheduler.unregisterSource,
2540
- dispose: () => {
2005
+ const externalCollection = (definition) => {
2006
+ if (definition.source.kind !== "external-collection") throw new Error("Invalid external collection source.");
2007
+ const source = definition.source.source;
2008
+ const boundary = createCollectionBoundary(scheduler, "external collection source", () => {
2009
+ const current = source.current();
2010
+ return Object.freeze({
2011
+ get: (key) => current.get(key),
2012
+ has: (key) => current.has(key),
2013
+ ids: () => current.ids()
2014
+ });
2015
+ }, definition.output.equality);
2016
+ let connection = externalCollections.get(source);
2017
+ if (!connection) {
2018
+ const members = /* @__PURE__ */ new Set();
2019
+ let closed = false;
2020
+ let unsubscribe;
2021
+ const created = {
2022
+ members,
2023
+ close() {
2024
+ if (closed) return;
2025
+ closed = true;
2026
+ unsubscribe?.();
2027
+ externalCollections.delete(source);
2028
+ }
2029
+ };
2541
2030
  try {
2542
- scheduler.dispose();
2543
- } finally {
2544
- if (!scheduler.active) sources.dispose();
2031
+ unsubscribe = source.subscribe((event) => {
2032
+ try {
2033
+ const impact = event.impact;
2034
+ if (impact?.kind === "reset") members.forEach((next) => next.mark({
2035
+ reset: true,
2036
+ cause: event.cause
2037
+ }));
2038
+ else if (impact?.kind === "incremental") {
2039
+ const candidates = new Set([
2040
+ ...impact.added,
2041
+ ...impact.updated,
2042
+ ...impact.removed
2043
+ ]);
2044
+ members.forEach((next) => next.mark({
2045
+ candidates,
2046
+ orderMayChange: Boolean(impact.added.size || impact.removed.size || impact.orderChanged),
2047
+ cause: event.cause
2048
+ }));
2049
+ } else members.forEach((next) => next.mark({
2050
+ fullScan: true,
2051
+ orderMayChange: true,
2052
+ cause: event.cause
2053
+ }));
2054
+ } catch (cause) {
2055
+ members.forEach((next) => next.fail(cause));
2056
+ }
2057
+ scheduler.run();
2058
+ });
2059
+ } catch (cause) {
2060
+ created.close();
2061
+ throw cause;
2545
2062
  }
2063
+ connection = created;
2064
+ externalCollections.set(source, connection);
2546
2065
  }
2066
+ connection.members.add(boundary);
2067
+ boundary.detach(() => {
2068
+ connection.members.delete(boundary);
2069
+ if (!connection.members.size) connection.close();
2070
+ });
2071
+ return boundary;
2547
2072
  };
2548
- return Object.freeze(graph);
2549
- };
2550
- //#endregion
2551
- //#region core/src/projection/runtime.ts
2552
- let activeTracker;
2553
- const trackKey = (key) => {
2554
- activeTracker?.keys.add(key);
2555
- };
2556
- const trackStructure = () => {
2557
- if (!activeTracker) return;
2558
- activeTracker.structure = true;
2559
- };
2560
- const trackAll = () => {
2561
- if (!activeTracker) return;
2562
- activeTracker.all = true;
2563
- };
2564
- const mapView = (read) => {
2565
- const keys = read.ids();
2566
- const source = /* @__PURE__ */ new Map();
2567
- for (const key of keys) source.set(key, read.get(key));
2568
- const view = {
2569
- get: (key) => {
2570
- trackKey(key);
2571
- return source.get(key);
2572
- },
2573
- has: (key) => {
2574
- trackKey(key);
2575
- return source.has(key);
2576
- },
2577
- get size() {
2578
- trackStructure();
2579
- return source.size;
2580
- },
2581
- keys: () => {
2582
- trackStructure();
2583
- return source.keys();
2584
- },
2585
- values: () => {
2586
- trackAll();
2587
- return source.values();
2588
- },
2589
- entries: () => {
2590
- trackAll();
2591
- return source.entries();
2592
- },
2593
- forEach: (callback, thisArg) => {
2594
- trackAll();
2595
- source.forEach((value, key) => callback.call(thisArg, value, key, view));
2596
- },
2597
- [Symbol.iterator]: () => {
2598
- trackAll();
2599
- return source[Symbol.iterator]();
2600
- }
2601
- };
2602
- return Object.freeze(view);
2603
- };
2604
- const lazyMapView = (read) => {
2605
- const ids = read.ids();
2606
- const entries = function* () {
2607
- for (const key of ids) yield [key, read.get(key)];
2608
- };
2609
- const values = function* () {
2610
- for (const key of ids) yield read.get(key);
2611
- };
2612
- const view = {
2613
- get: (key) => {
2614
- trackKey(key);
2615
- return read.get(key);
2616
- },
2617
- has: (key) => {
2618
- trackKey(key);
2619
- return read.has(key);
2620
- },
2621
- get size() {
2622
- trackStructure();
2623
- return ids.length;
2624
- },
2625
- keys: () => {
2626
- trackStructure();
2627
- return ids[Symbol.iterator]();
2628
- },
2629
- values: () => {
2630
- trackAll();
2631
- return values();
2632
- },
2633
- entries: () => {
2634
- trackAll();
2635
- return entries();
2636
- },
2637
- forEach: (callback, thisArg) => {
2638
- trackAll();
2639
- for (const key of ids) callback.call(thisArg, read.get(key), key, view);
2073
+ return {
2074
+ materialize(definition) {
2075
+ switch (definition.source.kind) {
2076
+ case "value-input": return valueInput(definition);
2077
+ case "collection-input": return collectionInput(definition);
2078
+ case "document-value": return documentValue(definition);
2079
+ case "document-collection": return documentCollection(definition);
2080
+ case "readable": return readable(definition);
2081
+ case "external-value": return externalValue(definition);
2082
+ case "external-collection": return externalCollection(definition);
2083
+ }
2640
2084
  },
2641
- [Symbol.iterator]: () => {
2642
- trackAll();
2643
- return entries();
2085
+ dispose() {
2086
+ documents.forEach((connection) => connection.close());
2087
+ readables.forEach((connection) => connection.close());
2088
+ documents.clear();
2089
+ readables.clear();
2644
2090
  }
2645
2091
  };
2646
- return Object.freeze(view);
2647
- };
2648
- const isMapLike = (value) => value instanceof Map || value !== null && typeof value === "object" && typeof value.get === "function" && typeof value.keys === "function" && typeof value.entries === "function";
2649
- const mapRead = (value) => ({
2650
- get: (key) => value.get(key),
2651
- has: (key) => value.has(key),
2652
- ids: () => Object.freeze([...value.keys()])
2653
- });
2654
- const mapChange = (previous, current) => {
2655
- const added = [];
2656
- const removed = [];
2657
- const updated = [];
2658
- for (const key of previous.keys()) if (!current.has(key)) removed.push({
2659
- key,
2660
- before: previous.get(key)
2661
- });
2662
- else if (!Object.is(previous.get(key), current.get(key))) updated.push({
2663
- key,
2664
- before: previous.get(key),
2665
- after: current.get(key)
2666
- });
2667
- for (const key of current.keys()) if (!previous.has(key)) added.push({
2668
- key,
2669
- after: current.get(key)
2670
- });
2671
- const before = [...previous.keys()].filter((key) => current.has(key));
2672
- const after = [...current.keys()].filter((key) => previous.has(key));
2673
- const orderChanged = before.length !== after.length || before.some((key, index) => key !== after[index]);
2674
- if (!added.length && !removed.length && !updated.length && !orderChanged) return void 0;
2675
- return {
2676
- kind: "incremental",
2677
- added: Object.freeze(added),
2678
- removed: Object.freeze(removed),
2679
- updated: Object.freeze(updated),
2680
- ...orderChanged ? { order: Object.freeze({
2681
- before: Object.freeze([...previous.keys()]),
2682
- after: Object.freeze([...current.keys()])
2683
- }) } : {}
2684
- };
2685
- };
2686
- const sameSelection = (left, right) => {
2687
- if (left.all !== right.all || left.structure !== right.structure || left.keys.size !== right.keys.size) return false;
2688
- for (const key of left.keys) if (!right.keys.has(key)) return false;
2689
- return true;
2690
- };
2691
- const selectionAffects = (selection, change) => {
2692
- if (!change || change.kind === "reset") return true;
2693
- if (selection.all && (change.added.length || change.removed.length || change.updated.length || change.order)) return true;
2694
- if (selection.structure && (change.added.length || change.removed.length || change.order)) return true;
2695
- for (const key of selection.keys) if (change.added.some((entry) => entry.key === key) || change.removed.some((entry) => entry.key === key) || change.updated.some((entry) => entry.key === key)) return true;
2696
- return false;
2697
2092
  };
2698
- const currentValue = (event) => {
2699
- if (!event || typeof event !== "object") return event;
2700
- if ("kind" in event && event.kind === "value") return event.value;
2701
- if ("kind" in event && event.kind === "document") return snapshot(event.read);
2702
- if ("kind" in event && event.kind === "collection") return mapView(event.read);
2703
- return event;
2704
- };
2705
- const sourceOf = (instance) => instance;
2093
+ //#endregion
2094
+ //#region core/src/projection/runtime.ts
2706
2095
  const createProjectionRuntime = (options) => {
2707
- const runtime = createProjectionGraph({ onError: options?.onError ?? (() => void 0) });
2096
+ const scheduler = createScheduler(options?.onError ?? (() => void 0));
2097
+ const sources = createSourceRegistry(scheduler);
2708
2098
  const materialized = /* @__PURE__ */ new WeakMap();
2709
- const groups = /* @__PURE__ */ new WeakMap();
2710
- let disposed = false;
2099
+ const publicViews = /* @__PURE__ */ new WeakMap();
2711
2100
  const scopes = /* @__PURE__ */ new Set();
2712
- let materialize;
2713
- const materializeGroup = (definition, requester) => {
2714
- const existing = groups.get(definition);
2715
- if (existing) return existing;
2716
- if (definition.projections.map((projection) => ownerOf(projection)).filter((owner) => owner !== void 0).some((owner) => owner !== requester)) throw new TypeError("Projection group belongs to another scope.");
2717
- const sources = Object.fromEntries(Object.entries(definition.dependencies).map(([key, dependency]) => [key, sourceOf(materialize(dependency, requester))]));
2718
- const nodes = runtime.group({
2719
- sources,
2720
- outputs: definition.outputs,
2721
- build: definition.build
2722
- });
2723
- const group = Object.freeze({
2724
- definition,
2725
- nodes
2726
- });
2727
- groups.set(definition, group);
2728
- for (let index = 0; index < definition.projections.length; index++) materialized.set(definition.projections[index], {
2729
- kind: "projection",
2730
- node: nodes[index]
2731
- });
2732
- if (requester) requester.materialized.push(definition);
2733
- return group;
2101
+ const assertAccess = (definition, requester) => {
2102
+ scheduler.assertActive();
2103
+ const owner = definition.owner;
2104
+ if (owner === void 0) return;
2105
+ if (owner !== requester) throw new TypeError("Projection belongs to another scope.");
2106
+ if (!requester?.active) throw new ProjectionDisposedError();
2734
2107
  };
2735
- materialize = (projection, requester) => {
2736
- const owner = ownerOf(projection);
2737
- if (owner) {
2738
- if (owner !== requester) throw new TypeError("Projection belongs to another scope.");
2739
- if (!requester.active) throw new ProjectionDisposedError();
2740
- }
2741
- const old = materialized.get(projection);
2742
- if (old) return old.node;
2743
- const definition = definitionOf(projection);
2744
- if (definition.kind === "incremental-group-output") return materializeGroup(definition.group, requester).nodes[definition.output];
2745
- let instance;
2746
- let write;
2747
- switch (definition.kind) {
2748
- case "input": {
2749
- const input = runtime.input(definition.initial, { isEqual: definition.isEqual });
2750
- write = {
2751
- kind: "value-input",
2752
- source: input.source,
2753
- set: input.set
2754
- };
2755
- instance = runtime.value({
2756
- sources: { input: input.source },
2757
- build: (values) => ({
2758
- value: values.input.value,
2759
- update: (next) => ({
2760
- kind: "changed",
2761
- value: next.input.value
2762
- })
2763
- })
2764
- });
2765
- break;
2766
- }
2767
- case "collection-input": {
2768
- const source = runtime.collectionInput(definition.initial);
2769
- write = {
2770
- kind: "collection-input",
2771
- source: source.source,
2772
- update: source.update
2773
- };
2774
- instance = runtime.map(source.source, (_key, value) => value);
2775
- break;
2776
- }
2777
- case "readable": {
2778
- const source = runtime.fromReadable(definition.readable, { isEqual: definition.isEqual });
2779
- instance = runtime.value({
2780
- sources: { source },
2781
- build: (values) => ({
2782
- value: values.source.value,
2783
- update: (next) => ({
2784
- kind: "changed",
2785
- value: next.source.value
2786
- })
2787
- })
2788
- });
2789
- break;
2790
- }
2791
- case "source-value":
2792
- {
2793
- const source = runtime.fromSource(definition.source);
2794
- instance = runtime.value({
2795
- sources: { source },
2796
- build: (values) => ({
2797
- value: values.source.value,
2798
- update: (next) => ({
2799
- kind: "changed",
2800
- value: next.source.value
2801
- })
2802
- })
2803
- });
2804
- }
2805
- break;
2806
- case "source-collection": {
2807
- const source = runtime.fromCollectionSource(definition.source);
2808
- instance = runtime.map(source, (_key, value) => value);
2809
- break;
2810
- }
2811
- case "document": {
2812
- const document = runtime.document(definition.document);
2813
- if (!definition.selector) {
2814
- instance = runtime.value({
2815
- sources: { document },
2816
- build: (values) => ({
2817
- value: snapshot(values.document.read),
2818
- update: (next) => ({
2819
- kind: "changed",
2820
- value: snapshot(next.document.read)
2821
- })
2822
- })
2823
- });
2824
- break;
2825
- }
2826
- const state = accessOf(definition.document);
2827
- try {
2828
- const collection = document.collection(definition.selector);
2829
- instance = runtime.map(collection, (_id, entry) => entry);
2830
- break;
2831
- } catch {
2832
- const selected = document.targets(definition.selector);
2833
- const address = compilePath(state.schema, "value", definition.selector).address;
2834
- instance = runtime.value({
2835
- sources: { selected },
2836
- build: () => ({
2837
- value: read$1(snapshot(state.document), address, state.schema),
2838
- update: () => ({
2839
- kind: "changed",
2840
- value: read$1(snapshot(state.document), address, state.schema)
2841
- })
2842
- })
2843
- });
2844
- }
2845
- break;
2846
- }
2847
- case "derive": {
2848
- const sources = Object.fromEntries(definition.dependencies.map((dependency, index) => [`d${index}`, sourceOf(materialize(dependency, owner))]));
2849
- instance = runtime.value({
2850
- sources,
2851
- build: (values) => ({
2852
- value: definition.compute(...Object.values(values).map(currentValue)),
2853
- update: (next) => ({
2854
- kind: "changed",
2855
- value: definition.compute(...Object.values(next).map(currentValue))
2856
- })
2857
- })
2858
- }, { isEqual: definition.isEqual });
2859
- break;
2860
- }
2861
- case "incremental-value": {
2862
- const sources = Object.fromEntries(Object.entries(definition.dependencies).map(([key, dependency]) => [key, sourceOf(materialize(dependency, owner))]));
2863
- instance = runtime.value({
2864
- sources,
2865
- build: definition.build
2866
- }, { isEqual: definition.isEqual });
2867
- break;
2868
- }
2869
- case "incremental-collection": {
2870
- const sources = Object.fromEntries(Object.entries(definition.dependencies).map(([key, dependency]) => [key, sourceOf(materialize(dependency, owner))]));
2871
- instance = runtime.collection({
2872
- sources,
2873
- build: definition.build,
2874
- isEqual: definition.isEqual,
2875
- name: definition.name
2876
- });
2877
- break;
2878
- }
2108
+ const rememberScoped = (definition, requester) => {
2109
+ if (requester && definition.owner === requester) requester.materialized.push(definition);
2110
+ };
2111
+ const materializeProducer = (definition, requester) => {
2112
+ assertAccess(definition, requester);
2113
+ const existing = materialized.get(definition);
2114
+ if (existing) return existing;
2115
+ let result;
2116
+ if (definition.kind === "source") {
2117
+ const source = sources.materialize(definition);
2118
+ result = Object.freeze({
2119
+ record: source.producer,
2120
+ outputs: Object.freeze([source.output]),
2121
+ ...source.write ? { write: source.write } : {}
2122
+ });
2123
+ } else {
2124
+ if (definition.owner === void 0 && definition.dependencies.some((dependency) => producerOf(dependency).owner !== void 0)) throw new TypeError("A root projection cannot depend on a scoped projection.");
2125
+ const dependencies = definition.dependencies.map((dependency) => resolveOutput(dependency, requester));
2126
+ const processor = createProcessor(scheduler, definition, Object.freeze(dependencies));
2127
+ result = Object.freeze({
2128
+ record: processor,
2129
+ outputs: processor.outputs
2130
+ });
2879
2131
  }
2880
- const record = write ? {
2881
- node: instance,
2882
- ...write
2883
- } : {
2884
- kind: "projection",
2885
- node: instance
2886
- };
2887
- materialized.set(projection, record);
2888
- if (owner) owner.materialized.push(projection);
2889
- return instance;
2132
+ materialized.set(definition, result);
2133
+ rememberScoped(definition, requester);
2134
+ return result;
2135
+ };
2136
+ const resolveOutput = (projection, requester) => {
2137
+ const ref = projectionRef(projection);
2138
+ const output = materializeProducer(ref.producer, requester).outputs[ref.output];
2139
+ if (!output) throw new TypeError("Projection output does not exist.");
2140
+ return output;
2141
+ };
2142
+ const assertOutput = (output) => {
2143
+ scheduler.assertActive();
2144
+ if (output.owner.disposed) throw new ProjectionDisposedError();
2145
+ if (output.owner.fault) throw output.owner.fault;
2890
2146
  };
2891
- const get = (projection, requester) => {
2892
- const instance = materialize(projection, requester);
2893
- const current = instance.current();
2894
- if (instance.kind === "collection") {
2895
- const record = materialized.get(projection) ?? materialized.get(instance);
2896
- const existing = record?.collection;
2897
- const revision = instance.revision();
2898
- if (existing?.revision === revision) return existing.value;
2899
- const value = lazyMapView(current);
2900
- if (record) record.collection = {
2147
+ const publicCurrent = (output) => {
2148
+ assertOutput(output);
2149
+ const current = output.current();
2150
+ const revision = output.revision();
2151
+ const cached = publicViews.get(output);
2152
+ if (cached?.revision === revision) return cached.value;
2153
+ if (output.kind === "collection") {
2154
+ const value = collectionView(current);
2155
+ publicViews.set(output, {
2901
2156
  revision,
2902
2157
  value
2903
- };
2158
+ });
2904
2159
  return value;
2905
2160
  }
2906
2161
  if (isMapLike(current)) {
2907
- const record = materialized.get(projection) ?? materialized.get(instance);
2908
- const existing = record?.collection;
2909
- const revision = instance.revision();
2910
- if (existing?.revision === revision) return existing.value;
2911
- const value = mapView(mapRead(current));
2912
- if (record) record.collection = {
2162
+ const value = snapshotCollectionView(mapRead(current));
2163
+ publicViews.set(output, {
2913
2164
  revision,
2914
2165
  value
2915
- };
2166
+ });
2916
2167
  return value;
2917
2168
  }
2169
+ publicViews.delete(output);
2918
2170
  return current;
2919
2171
  };
2920
- const makeReadable = (projection, selector, equality = Object.is, requester) => {
2921
- if (!selector) return Object.freeze({
2922
- current: () => get(projection, requester),
2923
- revision: () => materialize(projection, requester).revision(),
2924
- subscribe: (listener) => materialize(projection, requester).subscribe(listener)
2925
- });
2926
- let initialized = false;
2927
- let value;
2928
- let selectedRevision = 0;
2929
- let sourceRevision = -1;
2930
- let selection = Object.freeze({
2931
- keys: /* @__PURE__ */ new Set(),
2932
- all: true,
2933
- structure: false
2934
- });
2935
- let previousCollection;
2936
- let unsubscribeSource;
2937
- const listeners = /* @__PURE__ */ new Set();
2938
- const updateCollectionSnapshot = () => {
2939
- const current = get(projection, requester);
2940
- previousCollection = isMapLike(current) ? current : void 0;
2941
- };
2942
- const evaluate = () => {
2943
- const tracker = {
2944
- keys: /* @__PURE__ */ new Set(),
2945
- all: false,
2946
- structure: false
2947
- };
2948
- const previousTracker = activeTracker;
2949
- activeTracker = tracker;
2950
- let source;
2951
- let next;
2952
- try {
2953
- source = get(projection, requester);
2954
- next = selector(source);
2955
- } finally {
2956
- activeTracker = previousTracker;
2957
- }
2958
- const nextSelection = Object.freeze({
2959
- keys: new Set(tracker.keys),
2960
- all: tracker.all || isMapLike(source) && tracker.keys.size === 0,
2961
- structure: tracker.structure
2962
- });
2963
- const changed = !initialized || !equality(value, next);
2964
- if (changed) {
2965
- if (initialized) selectedRevision++;
2966
- value = next;
2967
- }
2968
- selection = nextSelection;
2969
- initialized = true;
2970
- sourceRevision = materialize(projection, requester).revision();
2971
- updateCollectionSnapshot();
2972
- return changed;
2973
- };
2974
- const ensureCurrent = () => {
2975
- const revision = materialize(projection, requester).revision();
2976
- if (!initialized || !unsubscribeSource && sourceRevision !== revision) evaluate();
2977
- return value;
2978
- };
2979
- const onProjectionChange = (incoming) => {
2980
- let change = incoming;
2981
- if (!change && previousCollection) {
2982
- const current = get(projection, requester);
2983
- if (isMapLike(current)) {
2984
- change = mapChange(previousCollection, current);
2985
- previousCollection = current;
2986
- if (!change) {
2987
- sourceRevision = materialize(projection, requester).revision();
2988
- return;
2989
- }
2990
- } else previousCollection = void 0;
2991
- }
2992
- if (!selectionAffects(selection, change)) {
2993
- sourceRevision = materialize(projection, requester).revision();
2994
- updateCollectionSnapshot();
2995
- return;
2996
- }
2997
- const previousSelection = selection;
2998
- const changed = evaluate();
2999
- if (unsubscribeSource && !sameSelection(previousSelection, selection)) {
3000
- unsubscribeSource();
3001
- unsubscribeSource = materialize(projection, requester).subscribe(onProjectionChange);
3002
- }
3003
- if (changed) Array.from(listeners).forEach((listener) => listener());
3004
- };
3005
- const installSource = () => {
3006
- if (!unsubscribeSource) unsubscribeSource = materialize(projection, requester).subscribe(onProjectionChange);
3007
- };
2172
+ const get = (projection, requester) => publicCurrent(resolveOutput(projection, requester));
2173
+ const readableSource = (projection, requester) => {
2174
+ const output = resolveOutput(projection, requester);
3008
2175
  return Object.freeze({
3009
- current: ensureCurrent,
2176
+ kind: output.kind,
2177
+ current: () => get(projection, requester),
3010
2178
  revision: () => {
3011
- ensureCurrent();
3012
- return selectedRevision;
2179
+ assertOutput(output);
2180
+ return output.revision();
3013
2181
  },
3014
- subscribe: (listener) => {
3015
- ensureCurrent();
3016
- listeners.add(listener);
3017
- installSource();
3018
- return () => {
3019
- listeners.delete(listener);
3020
- if (!listeners.size && unsubscribeSource) {
3021
- unsubscribeSource();
3022
- unsubscribeSource = void 0;
3023
- }
3024
- };
3025
- }
2182
+ subscribe: output.subscribe
3026
2183
  });
3027
2184
  };
2185
+ const makeReadable = (projection, selector, equality, requester) => {
2186
+ const source = readableSource(projection, requester);
2187
+ return selector ? createSelectorReadable(source, selector, equality) : createDirectReadable(source);
2188
+ };
3028
2189
  function readable(projection, selector, equality) {
3029
2190
  return makeReadable(projection, selector, equality);
3030
2191
  }
2192
+ const inputWrite = (target, requester) => {
2193
+ const ref = projectionRef(target);
2194
+ if (ref.output !== 0 || ref.producer.kind !== "source") throw new TypeError("Projection is not an input.");
2195
+ if (ref.producer.source.kind !== "value-input" && ref.producer.source.kind !== "collection-input") throw new TypeError("Projection is not an input.");
2196
+ const write = materializeProducer(ref.producer, requester).write;
2197
+ if (!write) throw new TypeError("Projection input is not writable.");
2198
+ return write;
2199
+ };
3031
2200
  const setInput = (target, value, requester) => {
3032
- materialize(target, requester);
3033
- const record = materialized.get(target);
3034
- if (record?.kind !== "value-input") throw new TypeError("Projection is not a value input.");
3035
- record.set(value);
2201
+ const write = inputWrite(target, requester);
2202
+ if (write.kind !== "value") throw new TypeError("Projection is not a value input.");
2203
+ write.set(value);
3036
2204
  };
3037
2205
  const updateInput = (target, run, requester) => {
3038
- materialize(target, requester);
3039
- const record = materialized.get(target);
3040
- if (record?.kind !== "collection-input") throw new TypeError("Projection is not a collection input.");
3041
- record.update(run);
2206
+ const write = inputWrite(target, requester);
2207
+ if (write.kind !== "collection") throw new TypeError("Projection is not a collection input.");
2208
+ write.update(run);
2209
+ };
2210
+ function batch(optionsOrCallback, maybeCallback) {
2211
+ return typeof optionsOrCallback === "function" ? scheduler.batch(optionsOrCallback) : scheduler.batch(optionsOrCallback, maybeCallback);
2212
+ }
2213
+ const disposeScope = (state) => {
2214
+ if (!state.active) return;
2215
+ scheduler.assertIdle();
2216
+ state.active = false;
2217
+ state.subscriptions.forEach((unsubscribe) => unsubscribe());
2218
+ state.subscriptions.clear();
2219
+ for (let index = state.materialized.length - 1; index >= 0; index--) {
2220
+ const definition = state.materialized[index];
2221
+ const current = materialized.get(definition);
2222
+ if (!current) continue;
2223
+ scheduler.releaseProducer(current.record);
2224
+ materialized.delete(definition);
2225
+ }
2226
+ state.materialized.length = 0;
2227
+ scopes.delete(state);
3042
2228
  };
3043
2229
  const createScope = () => {
3044
- runtime.assertIdle();
2230
+ scheduler.assertIdle();
3045
2231
  const state = {
3046
2232
  active: true,
3047
2233
  materialized: [],
@@ -3050,33 +2236,26 @@ const createProjectionRuntime = (options) => {
3050
2236
  scopes.add(state);
3051
2237
  const assertActive = () => {
3052
2238
  if (!state.active) throw new ProjectionDisposedError();
2239
+ scheduler.assertActive();
3053
2240
  };
3054
2241
  const own = (projection) => {
3055
2242
  assertActive();
3056
- const definition = definitionOf(projection);
3057
- const dependencies = definition.kind === "derive" ? definition.dependencies : definition.kind === "incremental-value" || definition.kind === "incremental-collection" ? Object.values(definition.dependencies) : definition.kind === "incremental-group-output" ? Object.values(definition.group.dependencies) : [];
3058
- for (const dependency of dependencies) {
3059
- const dependencyOwner = ownerOf(dependency);
3060
- if (dependencyOwner && dependencyOwner !== state) throw new TypeError("A scope cannot depend on another scope.");
3061
- }
3062
- ownDefinition(projection, state);
3063
- return projection;
2243
+ return ownProjection(projection, state);
3064
2244
  };
3065
- const scopedInput = Object.assign((initial, equality) => own(input(initial, equality)), { collection: (initial) => own(input.collection(initial)) });
3066
- const scopedDerive = (dependencies, compute, equality) => own(derive(dependencies, compute, equality));
3067
2245
  const ownOutputTree = (tree) => {
3068
2246
  if (isProjection(tree)) return own(tree);
3069
2247
  if (!tree || typeof tree !== "object") throw new TypeError("Incremental group output tree must be an object.");
3070
2248
  for (const value of Object.values(tree)) ownOutputTree(value);
3071
2249
  return tree;
3072
2250
  };
2251
+ const scopedInput = Object.assign((initial, equality) => own(input(initial, equality)), { collection: (initial) => own(input.collection(initial)) });
2252
+ const scopedDerive = (dependencies, compute, equality) => own(derive(dependencies, compute, equality));
3073
2253
  const scopedIncremental = Object.assign((dependencies, processor) => own(incremental(dependencies, processor)), {
3074
2254
  collection: (dependencies, processor) => own(incremental.collection(dependencies, processor)),
3075
2255
  group: (dependencies, defineOutputs, processor) => ownOutputTree(incremental.group(dependencies, defineOutputs, processor))
3076
2256
  });
3077
2257
  function scopeReadable(projection, selector, equality) {
3078
2258
  assertActive();
3079
- materialize(projection, state);
3080
2259
  const source = makeReadable(projection, selector, equality, state);
3081
2260
  return Object.freeze({
3082
2261
  current: () => {
@@ -3104,7 +2283,7 @@ const createProjectionRuntime = (options) => {
3104
2283
  }
3105
2284
  function scopeBatch(optionsOrCallback, maybeCallback) {
3106
2285
  assertActive();
3107
- return typeof optionsOrCallback === "function" ? runtime.batch(optionsOrCallback) : runtime.batch(optionsOrCallback, maybeCallback);
2286
+ return typeof optionsOrCallback === "function" ? batch(optionsOrCallback) : batch(optionsOrCallback, maybeCallback);
3108
2287
  }
3109
2288
  return Object.freeze({
3110
2289
  input: scopedInput,
@@ -3124,41 +2303,19 @@ const createProjectionRuntime = (options) => {
3124
2303
  updateInput(target, run, state);
3125
2304
  },
3126
2305
  batch: scopeBatch,
3127
- dispose: () => {
3128
- if (!state.active) return;
3129
- runtime.assertIdle();
3130
- state.active = false;
3131
- state.subscriptions.forEach((unsubscribe) => unsubscribe());
3132
- for (const definition of state.materialized.reverse()) {
3133
- const group = groups.get(definition);
3134
- if (group) {
3135
- runtime.releaseNode(group.nodes[0]);
3136
- groups.delete(definition);
3137
- group.definition.projections.forEach((projection) => materialized.delete(projection));
3138
- continue;
3139
- }
3140
- const record = materialized.get(definition);
3141
- if (!record) continue;
3142
- runtime.releaseNode(record.node);
3143
- if (record.kind !== "projection") runtime.releaseInput(record.source);
3144
- materialized.delete(definition);
3145
- }
3146
- state.materialized.length = 0;
3147
- scopes.delete(state);
3148
- }
2306
+ dispose: () => disposeScope(state)
3149
2307
  });
3150
2308
  };
3151
- const store = {
2309
+ return Object.freeze({
3152
2310
  get,
3153
2311
  readable,
3154
2312
  set: setInput,
3155
2313
  update: updateInput,
3156
- batch: runtime.batch,
2314
+ batch,
3157
2315
  scope: createScope,
3158
2316
  dispose: () => {
3159
- if (disposed) return;
3160
- runtime.assertIdle();
3161
- disposed = true;
2317
+ if (!scheduler.active) return;
2318
+ scheduler.assertIdle();
3162
2319
  for (const scope of scopes) {
3163
2320
  scope.active = false;
3164
2321
  scope.subscriptions.forEach((unsubscribe) => unsubscribe());
@@ -3166,10 +2323,10 @@ const createProjectionRuntime = (options) => {
3166
2323
  scope.materialized.length = 0;
3167
2324
  }
3168
2325
  scopes.clear();
3169
- runtime.dispose();
2326
+ sources.dispose();
2327
+ scheduler.dispose();
3170
2328
  }
3171
- };
3172
- return Object.freeze(store);
2329
+ });
3173
2330
  };
3174
2331
  //#endregion
3175
2332
  export { DocumentDisposedError, DocumentReentrancyError, ParseError, ProjectionDisposedError, ProjectionError, TransactionRejected, asReadable, createDocument, createProjectionRuntime, derive, field, input, list, map, object, observe, optional, parse, read, replace, snapshot, table, tree, variant };