@spine-event-engine/server 2.0.0-snapshot.25 → 2.0.0-snapshot.26

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 (150) hide show
  1. package/README.md +49 -9
  2. package/REFERENCE.md +339 -22
  3. package/dist/agent/agent-history.d.ts +19 -3
  4. package/dist/agent/agent-history.d.ts.map +1 -1
  5. package/dist/agent/agent-history.js +41 -10
  6. package/dist/agent/agent-history.js.map +1 -1
  7. package/dist/agent/agent-live-read-runtime.d.ts +12 -1
  8. package/dist/agent/agent-live-read-runtime.d.ts.map +1 -1
  9. package/dist/agent/agent-live-read-runtime.js +5 -4
  10. package/dist/agent/agent-live-read-runtime.js.map +1 -1
  11. package/dist/browser/browser-server.d.ts +163 -0
  12. package/dist/browser/browser-server.d.ts.map +1 -1
  13. package/dist/browser/browser-server.js +579 -188
  14. package/dist/browser/browser-server.js.map +1 -1
  15. package/dist/browser/durable-subscription-bindings.d.ts +5 -0
  16. package/dist/browser/durable-subscription-bindings.d.ts.map +1 -1
  17. package/dist/browser/durable-subscription-bindings.js +226 -38
  18. package/dist/browser/durable-subscription-bindings.js.map +1 -1
  19. package/dist/browser/options.d.ts +5 -0
  20. package/dist/browser/options.d.ts.map +1 -1
  21. package/dist/bus/command-bus.d.ts +3 -0
  22. package/dist/bus/command-bus.d.ts.map +1 -1
  23. package/dist/bus/command-bus.js +69 -9
  24. package/dist/bus/command-bus.js.map +1 -1
  25. package/dist/bus/command-errors.d.ts +20 -4
  26. package/dist/bus/command-errors.d.ts.map +1 -1
  27. package/dist/bus/command-errors.js +47 -7
  28. package/dist/bus/command-errors.js.map +1 -1
  29. package/dist/bus/event-bus.d.ts +3 -0
  30. package/dist/bus/event-bus.d.ts.map +1 -1
  31. package/dist/bus/event-bus.js +130 -8
  32. package/dist/bus/event-bus.js.map +1 -1
  33. package/dist/context/bounded-context.d.ts +29 -11
  34. package/dist/context/bounded-context.d.ts.map +1 -1
  35. package/dist/context/bounded-context.js +147 -186
  36. package/dist/context/bounded-context.js.map +1 -1
  37. package/dist/entity/entity.d.ts +59 -0
  38. package/dist/entity/entity.d.ts.map +1 -1
  39. package/dist/entity/entity.js +60 -0
  40. package/dist/entity/entity.js.map +1 -1
  41. package/dist/handler/app-query-handler.d.ts +60 -0
  42. package/dist/handler/app-query-handler.d.ts.map +1 -0
  43. package/dist/handler/app-query-handler.js +23 -0
  44. package/dist/handler/app-query-handler.js.map +1 -0
  45. package/dist/handler/generated-registry-discovery.d.ts +6 -0
  46. package/dist/handler/generated-registry-discovery.d.ts.map +1 -1
  47. package/dist/handler/generated-registry-discovery.js +71 -2
  48. package/dist/handler/generated-registry-discovery.js.map +1 -1
  49. package/dist/handler/handler-decorators.d.ts +8 -29
  50. package/dist/handler/handler-decorators.d.ts.map +1 -1
  51. package/dist/handler/handler-decorators.js +33 -210
  52. package/dist/handler/handler-decorators.js.map +1 -1
  53. package/dist/handler/spine-metadata-inventory.d.ts +99 -0
  54. package/dist/handler/spine-metadata-inventory.d.ts.map +1 -0
  55. package/dist/handler/spine-metadata-inventory.js +162 -0
  56. package/dist/handler/spine-metadata-inventory.js.map +1 -0
  57. package/dist/index.d.ts +5 -3
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +4 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/repository/repository.d.ts +44 -1
  62. package/dist/repository/repository.d.ts.map +1 -1
  63. package/dist/repository/repository.js +368 -88
  64. package/dist/repository/repository.js.map +1 -1
  65. package/dist/server/managed-server-application.d.ts +12 -11
  66. package/dist/server/managed-server-application.d.ts.map +1 -1
  67. package/dist/server/managed-server-application.js +41 -25
  68. package/dist/server/managed-server-application.js.map +1 -1
  69. package/dist/server/node-coordinator.d.ts +18 -5
  70. package/dist/server/node-coordinator.d.ts.map +1 -1
  71. package/dist/server/node-coordinator.js +554 -84
  72. package/dist/server/node-coordinator.js.map +1 -1
  73. package/dist/server/reserved-spine-rpc-paths.json +1 -0
  74. package/dist/server/server-environment.d.ts +17 -0
  75. package/dist/server/server-environment.d.ts.map +1 -1
  76. package/dist/server/server-environment.js +77 -16
  77. package/dist/server/server-environment.js.map +1 -1
  78. package/dist/server/server.d.ts +45 -0
  79. package/dist/server/server.d.ts.map +1 -1
  80. package/dist/server/server.js +111 -3
  81. package/dist/server/server.js.map +1 -1
  82. package/dist/services/app-query-execution.d.ts +53 -0
  83. package/dist/services/app-query-execution.d.ts.map +1 -0
  84. package/dist/services/app-query-execution.js +408 -0
  85. package/dist/services/app-query-execution.js.map +1 -0
  86. package/dist/services/query-reader.d.ts +32 -7
  87. package/dist/services/query-reader.d.ts.map +1 -1
  88. package/dist/services/query-reader.js +22 -2
  89. package/dist/services/query-reader.js.map +1 -1
  90. package/dist/services/read-request-status.d.ts +65 -0
  91. package/dist/services/read-request-status.d.ts.map +1 -0
  92. package/dist/services/read-request-status.js +151 -0
  93. package/dist/services/read-request-status.js.map +1 -0
  94. package/dist/services/spine-services.d.ts +36 -7
  95. package/dist/services/spine-services.d.ts.map +1 -1
  96. package/dist/services/spine-services.js +900 -168
  97. package/dist/services/spine-services.js.map +1 -1
  98. package/dist/services/trusted-reader-ingress.d.ts +92 -0
  99. package/dist/services/trusted-reader-ingress.d.ts.map +1 -0
  100. package/dist/services/trusted-reader-ingress.js +89 -0
  101. package/dist/services/trusted-reader-ingress.js.map +1 -0
  102. package/dist/spi/handler-registry.d.ts +2 -0
  103. package/dist/spi/handler-registry.d.ts.map +1 -1
  104. package/dist/spi/handler-registry.js +1 -1
  105. package/dist/spi/handler-registry.js.map +1 -1
  106. package/dist/stand/read-side-filter-engine.d.ts +46 -0
  107. package/dist/stand/read-side-filter-engine.d.ts.map +1 -0
  108. package/dist/stand/read-side-filter-engine.js +356 -0
  109. package/dist/stand/read-side-filter-engine.js.map +1 -0
  110. package/dist/stand/read-side-filter.d.ts +247 -0
  111. package/dist/stand/read-side-filter.d.ts.map +1 -0
  112. package/dist/stand/read-side-filter.js +39 -0
  113. package/dist/stand/read-side-filter.js.map +1 -0
  114. package/dist/stand/read-side-message.d.ts +66 -0
  115. package/dist/stand/read-side-message.d.ts.map +1 -0
  116. package/dist/stand/read-side-message.js +167 -0
  117. package/dist/stand/read-side-message.js.map +1 -0
  118. package/dist/stand/stand-direct-delivery.d.ts +161 -0
  119. package/dist/stand/stand-direct-delivery.d.ts.map +1 -0
  120. package/dist/stand/stand-direct-delivery.js +210 -0
  121. package/dist/stand/stand-direct-delivery.js.map +1 -0
  122. package/dist/stand/stand.d.ts +80 -42
  123. package/dist/stand/stand.d.ts.map +1 -1
  124. package/dist/stand/stand.js +410 -54
  125. package/dist/stand/stand.js.map +1 -1
  126. package/dist/stand/subscription-observer.d.ts.map +1 -1
  127. package/dist/stand/subscription-observer.js +103 -0
  128. package/dist/stand/subscription-observer.js.map +1 -1
  129. package/dist/stand/subscription-read-filters.d.ts +40 -0
  130. package/dist/stand/subscription-read-filters.d.ts.map +1 -0
  131. package/dist/stand/subscription-read-filters.js +91 -0
  132. package/dist/stand/subscription-read-filters.js.map +1 -0
  133. package/dist/stand/subscription-reader-binding.d.ts +20 -0
  134. package/dist/stand/subscription-reader-binding.d.ts.map +1 -0
  135. package/dist/stand/subscription-reader-binding.js +56 -0
  136. package/dist/stand/subscription-reader-binding.js.map +1 -0
  137. package/dist/stand/subscription-records.d.ts +30 -3
  138. package/dist/stand/subscription-records.d.ts.map +1 -1
  139. package/dist/stand/subscription-records.js +61 -16
  140. package/dist/stand/subscription-records.js.map +1 -1
  141. package/dist/stand/subscription-registry.d.ts +49 -14
  142. package/dist/stand/subscription-registry.d.ts.map +1 -1
  143. package/dist/stand/subscription-registry.js +150 -31
  144. package/dist/stand/subscription-registry.js.map +1 -1
  145. package/dist/stand/subscription-runtime.d.ts +7 -2
  146. package/dist/stand/subscription-runtime.d.ts.map +1 -1
  147. package/dist/stand/subscription-runtime.js +131 -25
  148. package/dist/stand/subscription-runtime.js.map +1 -1
  149. package/dist/tsconfig.tsbuildinfo +1 -1
  150. package/package.json +14 -14
@@ -12,14 +12,23 @@
12
12
  * the License.
13
13
  */
14
14
  var _a;
15
- import { clone } from "@bufbuild/protobuf";
16
- import { TypeUrls } from "@spine-event-engine/core";
17
- import { TenantIdSchema, VersionSchema, } from "@spine-event-engine/proto";
15
+ import { clone, create, toBinary } from "@bufbuild/protobuf";
16
+ import { AnySchema } from "@bufbuild/protobuf/wkt";
17
+ import { TypeRegistry, TypeUrls } from "@spine-event-engine/core";
18
+ import { registeredProtoModules } from "@spine-event-engine/proto/codegen";
19
+ import { ActorContextSchema, EventSchema, TenantIdSchema, VersionSchema, } from "@spine-event-engine/proto";
18
20
  import { ColumnTypes, RecordColumn, RecordQuery, } from "@spine-event-engine/storage";
19
21
  import { TenantBoundary } from "@spine-event-engine/storage/provider";
20
- import { EntityRecords, standEntityStorageDescriptor, } from "../entity/entity-storage-descriptor.js";
22
+ import { EntityIds, EntityRecords, standEntityStorageDescriptor, } from "../entity/entity-storage-descriptor.js";
21
23
  import { SubscriptionObservers } from "./subscription-observer.js";
24
+ import { ReadSideFilterEngine } from "./read-side-filter-engine.js";
25
+ import { SubscriptionReadFilters } from "./subscription-read-filters.js";
26
+ import { ReadSideMessages } from "./read-side-message.js";
27
+ import { StandDirectDelivery, } from "./stand-direct-delivery.js";
28
+ import { ServerEnvironment } from "../server/server-environment.js";
29
+ import { snapshotTrustedReader, } from "@spine-event-engine/core/spi/trusted-reader";
22
30
  const HANDLE_CLOSE_ERROR_LIMIT = 16;
31
+ export { StandSubscriptionCapacityError } from "./stand-direct-delivery.js";
23
32
  /**
24
33
  * Error thrown when direct Stand access targets an unregistered state type.
25
34
  */
@@ -54,12 +63,14 @@ export class StandStateTypeError extends Error {
54
63
  */
55
64
  export class Stand {
56
65
  #context;
66
+ #subscriptionQueueCapacity;
57
67
  #storageFactory;
58
68
  #registrations = new Map();
59
69
  #entityHandles = new Map();
60
70
  #handleCloseErrors = [];
61
71
  #omittedHandleCloseErrors = 0;
62
72
  #inFlight = new Set();
73
+ #pendingUpdates = new Map();
63
74
  #closing = false;
64
75
  #closed = false;
65
76
  #closedPromise;
@@ -69,10 +80,12 @@ export class Stand {
69
80
  * @param options The storage context and factory used for state records.
70
81
  */
71
82
  constructor(options) {
83
+ this.#subscriptionQueueCapacity = ServerEnvironment.instance().subscriptionQueueCapacity;
72
84
  this.#context = _a.#cloneContext(options.context);
73
85
  this.#storageFactory = options.storageFactory;
74
86
  deferredUpdates.set(this, (schema, state, updateOptions, metadata) => this.#deferUpdate(schema, state, updateOptions, metadata));
75
87
  currentReads.set(this, (schema, id, readOptions) => this.#readCurrent(schema, id, readOptions));
88
+ routedQueryPlans.set(this, (schema, plan, readOptions, readSide) => this.#queryPlanVersioned(schema, plan, readOptions, readSide));
76
89
  }
77
90
  /**
78
91
  * Registers one entity state schema. Re-registering the same schema is idempotent.
@@ -146,24 +159,44 @@ export class Stand {
146
159
  async readVersioned(schema, id, options = {}) {
147
160
  const finish = this.#beginPublicOperation();
148
161
  try {
162
+ const captured = _a.#snapshotRead(options);
163
+ const copiedId = ReadSideMessages.copy(id);
149
164
  const registration = this.#registration(schema, "read");
150
- const tenantId = await this.#checkedTenantId(options.tenantId);
151
- const storage = this.#leaseCurrent(registration, tenantId);
152
- try {
153
- const stored = await storage.current.read(id);
154
- if (stored === undefined || EntityRecords.unpack(registration.schema, stored).deleted) {
155
- return undefined;
156
- }
157
- return this.#currentResult(registration, stored);
158
- }
159
- finally {
160
- storage.release();
161
- }
165
+ const tenantId = await this.#checkedTenantId(captured.tenantId);
166
+ const result = await this.#readStored(registration, copiedId, tenantId);
167
+ return await this.#filteredResult(registration.schema, result, {
168
+ ...captured,
169
+ ...(tenantId === undefined ? {} : { tenantId }),
170
+ }, {
171
+ kind: "direct",
172
+ read: { typeUrl: registration.typeUrl, id: copiedId },
173
+ });
162
174
  }
163
175
  finally {
164
176
  finish();
165
177
  }
166
178
  }
179
+ /**
180
+ * Captures a current record and releases its provider lease before policy awaits.
181
+ *
182
+ * @typeParam Schema Registered state schema.
183
+ * @param registration State metadata and storage descriptor.
184
+ * @param id Detached Entity identifier.
185
+ * @param tenantId Admitted tenant, if multitenant.
186
+ * @returns Detached live state and version, or undefined.
187
+ */
188
+ async #readStored(registration, id, tenantId) {
189
+ const storage = this.#leaseCurrent(registration, tenantId);
190
+ try {
191
+ const stored = await storage.current.read(id);
192
+ if (stored === undefined || EntityRecords.unpack(registration.schema, stored).deleted)
193
+ return undefined;
194
+ return this.#currentResult(registration, stored);
195
+ }
196
+ finally {
197
+ storage.release();
198
+ }
199
+ }
167
200
  /**
168
201
  * Reads all latest states and version metadata in storage query order.
169
202
  *
@@ -187,17 +220,25 @@ export class Stand {
187
220
  async queryVersioned(schema, query = {}, options = {}) {
188
221
  const finish = this.#beginPublicOperation();
189
222
  try {
223
+ const captured = _a.#snapshotRead(options);
224
+ const copiedQuery = ReadSideMessages.copy(query);
190
225
  const registration = this.#registration(schema, "read");
191
- const tenantId = await this.#checkedTenantId(options.tenantId);
226
+ const tenantId = await this.#checkedTenantId(captured.tenantId);
192
227
  const storage = this.#leaseCurrent(registration, tenantId);
228
+ let results;
193
229
  try {
194
- const stored = await storage.current.query(_a.#legacyPlan(query));
195
- const results = stored.map((entry) => this.#entryResult(registration, entry.record));
196
- return results.filter((result) => result !== undefined);
230
+ const stored = await storage.current.query(_a.#legacyPlan(copiedQuery));
231
+ results = stored
232
+ .map((entry) => this.#entryResult(registration, entry.record))
233
+ .filter((result) => result !== undefined);
197
234
  }
198
235
  finally {
199
236
  storage.release();
200
237
  }
238
+ return await this.#filteredResults(registration.schema, results, {
239
+ ...captured,
240
+ ...(tenantId === undefined ? {} : { tenantId }),
241
+ }, { kind: "direct", query: copiedQuery });
201
242
  }
202
243
  finally {
203
244
  finish();
@@ -213,24 +254,117 @@ export class Stand {
213
254
  * @returns The matching current states and versions.
214
255
  */
215
256
  async queryPlanVersioned(schema, plan, options = {}) {
257
+ return this.#queryPlanVersioned(schema, plan, options);
258
+ }
259
+ /**
260
+ * Reads stored rows and applies the context's outward policy in one operation lifetime.
261
+ * @typeParam Schema Registered state schema.
262
+ * @param schema Registered Entity state schema.
263
+ * @param plan Normalized storage query plan.
264
+ * @param options Trusted read facts and tenant.
265
+ * @param readSide Actual routed Query context, if supplied internally.
266
+ * @returns Surviving versioned state records.
267
+ */
268
+ async #queryPlanVersioned(schema, plan, options, readSide) {
216
269
  const finish = this.#beginPublicOperation();
217
270
  try {
271
+ const captured = _a.#snapshotRead(options, readSide);
272
+ const copiedPlan = ReadSideMessages.copy(plan);
218
273
  const registration = this.#registration(schema, "read");
219
- const tenantId = await this.#checkedTenantId(options.tenantId);
274
+ const tenantId = await this.#checkedTenantId(captured.tenantId);
220
275
  const storage = this.#leaseCurrent(registration, tenantId);
276
+ let results;
221
277
  try {
222
- const stored = await storage.current.query(plan);
223
- const results = stored.map((entry) => this.#entryResult(registration, entry.record));
224
- return results.filter((result) => result !== undefined);
278
+ const stored = await storage.current.query(copiedPlan);
279
+ results = stored
280
+ .map((entry) => this.#entryResult(registration, entry.record))
281
+ .filter((result) => result !== undefined);
225
282
  }
226
283
  finally {
227
284
  storage.release();
228
285
  }
286
+ return await this.#filteredResults(registration.schema, results, {
287
+ ...captured,
288
+ ...(tenantId === undefined ? {} : { tenantId }),
289
+ }, { kind: "direct", plan: copiedPlan });
229
290
  }
230
291
  finally {
231
292
  finish();
232
293
  }
233
294
  }
295
+ /**
296
+ * Detaches mutable caller facts before storage admission can await.
297
+ *
298
+ * @param options Direct or service read options.
299
+ * @returns Stable tenant and invocation facts, retaining the live cancellation signal.
300
+ */
301
+ static #snapshotRead(options, side) {
302
+ const { signal, producingContext, effectiveTenant, parent, field, ...facts } = side ?? {};
303
+ void producingContext;
304
+ void effectiveTenant;
305
+ void parent;
306
+ void field;
307
+ return {
308
+ ...(options.tenantId === undefined
309
+ ? {}
310
+ : { tenantId: clone(TenantIdSchema, options.tenantId) }),
311
+ readSide: {
312
+ ...(side === undefined ? {} : ReadSideMessages.copy(facts)),
313
+ ...(options.actorContext === undefined
314
+ ? {}
315
+ : { actorContext: clone(ActorContextSchema, options.actorContext) }),
316
+ ...(options.reader === undefined ? {} : snapshotTrustedReader(options.reader)),
317
+ ...((signal ?? options.signal) === undefined ? {} : { signal: signal ?? options.signal }),
318
+ },
319
+ };
320
+ }
321
+ /**
322
+ * Applies this context's outward read policy after storage has been released.
323
+ *
324
+ * @typeParam Schema Registered state schema.
325
+ * @param schema State schema selected by the read.
326
+ * @param result Captured state and durable version.
327
+ * @param options Tenant and service read facts.
328
+ * @param operation Actual direct operation when no service request is present.
329
+ * @returns Detached visible state with its unchanged version, or no state.
330
+ */
331
+ async #filteredResult(schema, result, options, operation) {
332
+ if (result === undefined)
333
+ return undefined;
334
+ const filters = standReadFilters.get(this);
335
+ if (filters === undefined)
336
+ return result;
337
+ const producer = standProducingContexts.get(this);
338
+ const context = {
339
+ ...(options.readSide ?? {}),
340
+ operation: options.readSide?.operation ?? operation,
341
+ ...(producer === undefined ? {} : { producingContext: producer }),
342
+ ...(options.tenantId === undefined ? {} : { effectiveTenant: options.tenantId }),
343
+ };
344
+ const state = await filters.apply(schema, result.state, context);
345
+ return state === null ? undefined : { ...result, state };
346
+ }
347
+ /**
348
+ * Applies outward policy in stored query order without replenishing removed records.
349
+ *
350
+ * @typeParam Schema Registered state schema.
351
+ * @param schema State schema selected by the read.
352
+ * @param results Captured stored states and durable versions.
353
+ * @param options Tenant and service read facts.
354
+ * @param operation Actual direct query when no service request is present.
355
+ * @returns Surviving visible records in their original order.
356
+ */
357
+ async #filteredResults(schema, results, options, operation) {
358
+ if (!standReadFilters.has(this))
359
+ return results;
360
+ const visible = [];
361
+ for (const result of results) {
362
+ const next = await this.#filteredResult(schema, result, options, operation);
363
+ if (next !== undefined)
364
+ visible.push(next);
365
+ }
366
+ return visible;
367
+ }
234
368
  /**
235
369
  * Clears all stored states and durable version metadata for one known state schema.
236
370
  *
@@ -317,26 +451,77 @@ export class Stand {
317
451
  * @returns Prepared write with cleanup actions.
318
452
  */
319
453
  async #prepareUpdate(schema, state, options, metadata) {
320
- const finish = this.#beginOperation();
454
+ const onFinish = this.#beginOperation();
321
455
  let storage;
456
+ let releaseUpdate;
322
457
  try {
323
458
  const registration = this.#registration(schema, "update");
324
- if (metadata !== undefined && metadata.schema !== registration.schema)
325
- throw new Error("Repository metadata does not match the Entity state schema.");
459
+ _a.#requireUpdateMetadata(registration, metadata);
326
460
  const snapshot = _a.#snapshotUpdateOptions(options);
327
461
  const stateCopy = clone(schema, state);
328
462
  const tenantId = await this.#checkedTenantId(options.tenantId);
329
463
  const id = _a.#readStateId(stateCopy, registration);
464
+ releaseUpdate = await this.#reserveUpdate(registration, tenantId, id, metadata);
330
465
  storage = this.#leaseCurrent(registration, tenantId);
331
466
  const previousState = await this.#previousUpdateState(registration, tenantId, id, storage);
332
- return this.#preparedUpdate(registration, { id, previousState, state: stateCopy, tenantId, version: snapshot.version }, snapshot, metadata, storage, finish);
467
+ return this.#preparedUpdate(registration, { id, previousState, state: stateCopy, tenantId, version: snapshot.version }, snapshot, metadata, storage, () => {
468
+ _a.#finishUpdate(releaseUpdate, onFinish);
469
+ });
333
470
  }
334
471
  catch (error) {
335
472
  storage?.release();
336
- finish();
473
+ _a.#finishUpdate(releaseUpdate, onFinish);
337
474
  throw error;
338
475
  }
339
476
  }
477
+ /**
478
+ * Checks repository metadata before preparing the state update.
479
+ *
480
+ * @param registration Registered state metadata.
481
+ * @param metadata Repository metadata, when supplied.
482
+ */
483
+ static #requireUpdateMetadata(registration, metadata) {
484
+ if (metadata !== undefined && metadata.schema !== registration.schema)
485
+ throw new Error("Repository metadata does not match the Entity state schema.");
486
+ }
487
+ /**
488
+ * Releases the update reservation and completes its tracked Stand operation.
489
+ *
490
+ * @param onReleaseUpdate Per-Entity reservation release, when acquired.
491
+ * @param onFinish Completes the tracked Stand operation.
492
+ */
493
+ static #finishUpdate(onReleaseUpdate, onFinish) {
494
+ onReleaseUpdate?.();
495
+ onFinish();
496
+ }
497
+ /**
498
+ * Keeps one Entity's previous-state read and commit in notification order.
499
+ *
500
+ * @param registration State metadata.
501
+ * @param tenantId Tenant for the state.
502
+ * @param id Entity identifier.
503
+ * @param metadata Repository-validated identifier metadata, when available.
504
+ * @returns A release action for notification or cancellation.
505
+ */
506
+ async #reserveUpdate(registration, tenantId, id, metadata) {
507
+ const packed = metadata === undefined
508
+ ? EntityIds.pack(registration.schema, id)
509
+ : EntityIds.packField(metadata.idField.descriptor, id);
510
+ const key = JSON.stringify([
511
+ registration.typeUrl,
512
+ this.#tenantKey(tenantId),
513
+ Buffer.from(toBinary(AnySchema, packed)).toString("base64"),
514
+ ]);
515
+ const previous = this.#pendingUpdates.get(key);
516
+ const settled = Promise.withResolvers();
517
+ this.#pendingUpdates.set(key, settled.promise);
518
+ await previous;
519
+ return () => {
520
+ if (this.#pendingUpdates.get(key) === settled.promise)
521
+ this.#pendingUpdates.delete(key);
522
+ settled.resolve(undefined);
523
+ };
524
+ }
340
525
  /**
341
526
  * Copies mutable Version and lifecycle input before update preparation awaits.
342
527
  * @param options Update metadata supplied by the caller.
@@ -453,29 +638,74 @@ export class Stand {
453
638
  *
454
639
  * @typeParam Schema The registered state schema.
455
640
  * @param schema The registered entity state schema.
456
- * @param callback The function receiving matching state updates.
457
- * @param options The tenant slice to subscribe to.
458
- * @returns A handle that stops future callback delivery.
641
+ * @param observer Receives serial visible updates and one terminal local fault.
642
+ * @param options Tenant, trusted facts, and cancellation.
643
+ * @returns A handle that invalidates pending and active delivery.
459
644
  */
460
- subscribe(schema, callback, options = {}) {
645
+ subscribe(schema, observer, options = {}) {
461
646
  this.#requireOpen();
647
+ _a.#requireObserver(observer);
462
648
  const registration = this.#registration(schema, "subscribe");
463
649
  const tenantKey = this.#tenantKey(options.tenantId);
464
- const subscriber = Object.freeze({ tenantKey, callback });
465
- let closed = false;
466
- registration.subscribers.add(subscriber);
650
+ const delivery = this.#subscribeDelivery(schema, observer, options, registration, tenantKey);
467
651
  return Object.freeze({
652
+ /**
653
+ * Reports whether this subscription handle has stopped delivering.
654
+ */
468
655
  get closed() {
469
- return closed;
656
+ return delivery.closed;
470
657
  },
658
+ /**
659
+ * Detaches the subscriber and stops future local deliveries.
660
+ */
471
661
  unsubscribe() {
472
- if (!closed) {
473
- closed = true;
474
- registration.subscribers.delete(subscriber);
475
- }
662
+ delivery.close();
476
663
  },
477
664
  });
478
665
  }
666
+ /**
667
+ * Rejects a raw callback or malformed JavaScript observer before registration.
668
+ *
669
+ * @param observer The runtime value supplied for direct delivery.
670
+ */
671
+ static #requireObserver(observer) {
672
+ if (typeof observer !== "object" ||
673
+ observer === null ||
674
+ !("next" in observer) ||
675
+ typeof observer.next !== "function" ||
676
+ !("error" in observer) ||
677
+ typeof observer.error !== "function")
678
+ throw new TypeError("Stand.subscribe requires an observer with next and error callbacks.");
679
+ }
680
+ /**
681
+ * Captures the subscriber's read facts and attaches its independent delivery queue.
682
+ *
683
+ * @typeParam Schema The registered state schema.
684
+ * @param schema The state schema selected by the caller.
685
+ * @param observer Receives visible updates or one terminal local fault.
686
+ * @param options Trusted reader, tenant, and cancellation at subscribe time.
687
+ * @param registration The Stand registration for this state schema.
688
+ * @param tenantKey The normalized tenant slice for delivery.
689
+ * @returns The subscriber's serial delivery queue.
690
+ */
691
+ #subscribeDelivery(schema, observer, options, registration, tenantKey) {
692
+ const captured = _a.#snapshotRead(options);
693
+ const producingContext = standProducingContexts.get(this);
694
+ const attachment = {};
695
+ const delivery = new StandDirectDelivery(schema, observer, standReadFilters.get(this), {
696
+ ...captured.readSide,
697
+ operation: { kind: "direct", subscription: { typeUrl: registration.typeUrl } },
698
+ ...(captured.tenantId === undefined ? {} : { effectiveTenant: captured.tenantId }),
699
+ ...(producingContext === undefined ? {} : { producingContext }),
700
+ }, this.#subscriptionQueueCapacity, () => {
701
+ if (attachment.subscriber !== undefined)
702
+ registration.subscribers.delete(attachment.subscriber);
703
+ }, options.signal);
704
+ attachment.subscriber = Object.freeze({ tenantKey, delivery });
705
+ if (!delivery.closed)
706
+ registration.subscribers.add(attachment.subscriber);
707
+ return delivery;
708
+ }
479
709
  /**
480
710
  * Closes direct subscriptions and rejects later Stand operations.
481
711
  *
@@ -490,10 +720,15 @@ export class Stand {
490
720
  this.#closedPromise ??= this.#closeOnce();
491
721
  return this.#closedPromise;
492
722
  }
723
+ /**
724
+ * Drains accepted Stand work, closes subscriptions, and releases Entity handles.
725
+ */
493
726
  async #closeOnce() {
494
727
  this.#closing = true;
495
728
  await Promise.all([...this.#inFlight]);
496
729
  for (const registration of this.#registrations.values()) {
730
+ for (const subscriber of registration.subscribers)
731
+ subscriber.delivery.close();
497
732
  registration.subscribers.clear();
498
733
  }
499
734
  const errors = this.#handleCloseErrors.splice(0);
@@ -535,6 +770,9 @@ export class Stand {
535
770
  }
536
771
  return registration;
537
772
  }
773
+ /**
774
+ * Reuses single-tenant storage or opens a temporary multitenant Entity handle.
775
+ */
538
776
  #leaseCurrent(registration, tenantId) {
539
777
  if (this.#context.multitenant) {
540
778
  return this.#lease(_a.#openStorage(this.#storageFactory, standEntityStorageDescriptor(this.#storageContext(tenantId), registration.schema, registration.columns)), true);
@@ -547,6 +785,9 @@ export class Stand {
547
785
  this.#entityHandles.set(key, handle);
548
786
  return this.#lease(handle, false);
549
787
  }
788
+ /**
789
+ * Returns a release callback for a current-state storage handle.
790
+ */
550
791
  #lease(handle, releaseAfterOperation) {
551
792
  let released = false;
552
793
  return {
@@ -611,28 +852,22 @@ export class Stand {
611
852
  * @param captured Subscribers captured before persistence, if available.
612
853
  */
613
854
  #notify(registration, input, captured) {
614
- const errors = [];
615
855
  const tenantKey = this.#tenantKey(input.tenantId);
616
856
  const subscribers = captured ?? this.#tenantSubscribers(registration, tenantKey);
617
857
  for (const subscriber of subscribers) {
618
- try {
619
- subscriber.callback(_a.#createUpdate(registration, input));
620
- }
621
- catch (error) {
622
- errors.push(error);
623
- }
624
- }
625
- if (errors.length === 1) {
626
- throw errors[0];
627
- }
628
- if (errors.length > 1) {
629
- throw new AggregateError(errors, "Stand subscriber delivery failed.");
858
+ subscriber.delivery.admit(_a.#createUpdate(registration, input));
630
859
  }
631
860
  }
861
+ /**
862
+ * Creates the subscription partition key for the selected tenant.
863
+ */
632
864
  #tenantKey(tenantId) {
633
865
  const selected = this.#tenantId(tenantId);
634
866
  return selected === undefined ? "__single__" : String(TenantBoundary.from(selected).key);
635
867
  }
868
+ /**
869
+ * Checks whether a registered Entity state has subscribers in this tenant slice.
870
+ */
636
871
  #hasTenantSubscribers(registration, tenantId) {
637
872
  const tenantKey = this.#tenantKey(tenantId);
638
873
  for (const subscriber of registration.subscribers) {
@@ -653,6 +888,9 @@ export class Stand {
653
888
  #tenantSubscribers(registration, tenantKey) {
654
889
  return [...registration.subscribers].filter((subscriber) => subscriber.tenantKey === tenantKey);
655
890
  }
891
+ /**
892
+ * Validates and detaches the tenant ID required by this Stand's tenancy mode.
893
+ */
656
894
  #tenantId(tenantId) {
657
895
  if (!this.#context.multitenant) {
658
896
  if (tenantId !== undefined) {
@@ -676,6 +914,9 @@ export class Stand {
676
914
  await standAdmissions.get(this)?.(selected);
677
915
  return selected;
678
916
  }
917
+ /**
918
+ * Builds the provider storage scope for a validated tenant selection.
919
+ */
679
920
  #storageContext(tenantId) {
680
921
  if (!this.#context.multitenant) {
681
922
  this.#tenantId(tenantId);
@@ -690,11 +931,17 @@ export class Stand {
690
931
  tenantId: TenantBoundary.from(tenantId).tenantId,
691
932
  });
692
933
  }
934
+ /**
935
+ * Rejects a new operation once Stand closing has begun.
936
+ */
693
937
  #requireOpen() {
694
938
  if (this.#closing || this.#closed) {
695
939
  throw new Error("Stand is closed.");
696
940
  }
697
941
  }
942
+ /**
943
+ * Tracks an accepted operation and returns its completion callback.
944
+ */
698
945
  #beginOperation() {
699
946
  this.#requireOpen();
700
947
  let finish;
@@ -737,6 +984,9 @@ export class Stand {
737
984
  ...(input.tenantId === undefined ? {} : { tenantId: clone(TenantIdSchema, input.tenantId) }),
738
985
  });
739
986
  }
987
+ /**
988
+ * Extracts the registered Entity ID field from a state update.
989
+ */
740
990
  static #readStateId(state, registration) {
741
991
  const id = state[registration.idField];
742
992
  if (id === undefined || id === null) {
@@ -744,9 +994,15 @@ export class Stand {
744
994
  }
745
995
  return id;
746
996
  }
997
+ /**
998
+ * Detaches a structured storage value before returning it to a caller.
999
+ */
747
1000
  static #cloneValue(value) {
748
1001
  return typeof value === "object" && value !== null ? globalThis.structuredClone(value) : value;
749
1002
  }
1003
+ /**
1004
+ * Translates a supported RecordQuery into the normalized storage query plan.
1005
+ */
750
1006
  static #legacyPlan(query) {
751
1007
  RecordQuery.validate(query);
752
1008
  if (query.after !== undefined) {
@@ -794,6 +1050,9 @@ export class Stand {
794
1050
  }
795
1051
  return candidate.createEntityStorage(input);
796
1052
  }
1053
+ /**
1054
+ * Freezes a detached name and tenancy mode for storage configuration.
1055
+ */
797
1056
  static #cloneContext(context) {
798
1057
  return Object.freeze({ name: context.name, multitenant: context.multitenant });
799
1058
  }
@@ -805,6 +1064,100 @@ _a = Stand;
805
1064
  * @internal
806
1065
  */
807
1066
  export const standAccess = Object.freeze({
1067
+ /**
1068
+ * Forwards trusted routed Query context to the private Stand read path.
1069
+ * @typeParam Schema Registered state schema.
1070
+ * @param stand Producing context's Stand.
1071
+ * @param schema Registered state schema.
1072
+ * @param plan Normalized routed query plan.
1073
+ * @param options Admitted tenant and trusted facts.
1074
+ * @param readSide Actual service or handler Query context.
1075
+ * @returns Surviving versioned states.
1076
+ */
1077
+ queryPlanVersioned(stand, schema, plan, options, readSide) {
1078
+ const read = routedQueryPlans.get(stand);
1079
+ if (read === undefined)
1080
+ throw new Error("Stand routed read is unavailable.");
1081
+ return read(schema, plan, options, readSide);
1082
+ },
1083
+ /**
1084
+ * Associates actual context identity with direct and historical outward reads.
1085
+ *
1086
+ * @param stand Context Stand.
1087
+ * @param context Built Bounded Context.
1088
+ */
1089
+ bindProducingContext(stand, context) {
1090
+ standProducingContexts.set(stand, context);
1091
+ },
1092
+ /**
1093
+ * Applies a history state policy after the physical history read.
1094
+ *
1095
+ * @typeParam Schema Registered domain schema.
1096
+ * @param stand Context read facade.
1097
+ * @param schema Historical state schema.
1098
+ * @param state Captured state.
1099
+ * @param context Actual history call.
1100
+ * @returns Filtered state or null.
1101
+ */
1102
+ filterState(stand, schema, state, context) {
1103
+ const filters = standReadFilters.get(stand);
1104
+ const producer = standProducingContexts.get(stand);
1105
+ return filters === undefined
1106
+ ? Promise.resolve(state)
1107
+ : filters.apply(schema, state, {
1108
+ ...context,
1109
+ ...(producer === undefined ? {} : { producingContext: producer }),
1110
+ });
1111
+ },
1112
+ /**
1113
+ * Filters a retained Event payload while retaining envelope identity and metadata.
1114
+ *
1115
+ * @param stand Context read facade.
1116
+ * @param event Retained Event envelope.
1117
+ * @param context Actual history call.
1118
+ * @returns Event with filtered payload, or null when removed.
1119
+ */
1120
+ async filterEvent(stand, event, context) {
1121
+ const filters = standReadFilters.get(stand);
1122
+ if (filters === undefined)
1123
+ return event;
1124
+ if (event.message === undefined)
1125
+ throw new TypeError("Retained Event payload is missing.");
1126
+ const producer = standProducingContexts.get(stand);
1127
+ const message = await filters.applyPacked(event.message, {
1128
+ ...context,
1129
+ ...(producer === undefined ? {} : { producingContext: producer }),
1130
+ });
1131
+ return message === null ? null : create(EventSchema, { ...clone(EventSchema, event), message });
1132
+ },
1133
+ /**
1134
+ * Applies Bounded Context policy to one subscription update before delivery.
1135
+ *
1136
+ * @param stand Context read facade.
1137
+ * @param update Subscription update awaiting delivery.
1138
+ * @param context Actual subscription read facts.
1139
+ * @returns Visible update, or null when policy removes it.
1140
+ */
1141
+ filterSubscription(stand, update, context) {
1142
+ const producer = standProducingContexts.get(stand);
1143
+ return SubscriptionReadFilters.apply(update, standReadFilters.get(stand), {
1144
+ ...context,
1145
+ ...(producer === undefined ? {} : { producingContext: producer }),
1146
+ });
1147
+ },
1148
+ /**
1149
+ * Attaches registered policies to this Stand without changing persistence reads.
1150
+ *
1151
+ * @param stand Context read facade.
1152
+ * @param filters Policies in registration order.
1153
+ */
1154
+ installReadFilters(stand, filters) {
1155
+ if (!(stand instanceof Stand))
1156
+ throw new TypeError("Read filters require a Stand instance.");
1157
+ if (standReadFilters.has(stand))
1158
+ throw new TypeError("Stand read filters are already installed.");
1159
+ standReadFilters.set(stand, new ReadSideFilterEngine(filters, TypeRegistry.from(...registeredProtoModules())));
1160
+ },
808
1161
  /**
809
1162
  * Sets the immediate external read admission check.
810
1163
  *
@@ -893,5 +1246,8 @@ export const standAccess = Object.freeze({
893
1246
  const deferredUpdates = new WeakMap();
894
1247
  const standAdmissions = new WeakMap();
895
1248
  const standPublicAdmissions = new WeakMap();
1249
+ const standReadFilters = new WeakMap();
1250
+ const standProducingContexts = new WeakMap();
896
1251
  const currentReads = new WeakMap();
1252
+ const routedQueryPlans = new WeakMap();
897
1253
  //# sourceMappingURL=stand.js.map