@hraness/direct 0.7.5

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +436 -0
  3. package/dist/core/index.js +162 -0
  4. package/dist/index-1csg00w4.js +1167 -0
  5. package/dist/index-6mdfd2ey.js +464 -0
  6. package/dist/index-7n1h75n6.js +616 -0
  7. package/dist/index.js +232 -0
  8. package/dist/react.js +32 -0
  9. package/dist/testing/index.js +1069 -0
  10. package/dist/tooling/bombadil.js +2117 -0
  11. package/dist/tooling/browser-verification-entry.js +1499 -0
  12. package/dist/tooling/bundle-boundary.js +119 -0
  13. package/dist/web.js +605 -0
  14. package/package.json +179 -0
  15. package/skills/direct/AGENTS.md +13 -0
  16. package/skills/direct/SKILL.md +49 -0
  17. package/skills/direct/agents/openai.yaml +4 -0
  18. package/skills/direct/references/adoption.md +131 -0
  19. package/skills/direct/references/install.md +91 -0
  20. package/skills/direct/references/verification.md +247 -0
  21. package/src/core/coverage.ts +336 -0
  22. package/src/core/definition.ts +378 -0
  23. package/src/core/effects.ts +88 -0
  24. package/src/core/fixture.ts +185 -0
  25. package/src/core/ids.ts +77 -0
  26. package/src/core/index.ts +13 -0
  27. package/src/core/json-value.ts +7 -0
  28. package/src/core/json.ts +593 -0
  29. package/src/core/query.ts +230 -0
  30. package/src/core/reason.ts +16 -0
  31. package/src/core/resource.ts +10 -0
  32. package/src/core/result.ts +19 -0
  33. package/src/core/runtime.ts +229 -0
  34. package/src/core/scenario.ts +149 -0
  35. package/src/core/store.ts +784 -0
  36. package/src/index.ts +51 -0
  37. package/src/react.ts +54 -0
  38. package/src/testing/activity.ts +228 -0
  39. package/src/testing/coverage-binding.ts +99 -0
  40. package/src/testing/evidence.ts +59 -0
  41. package/src/testing/index.ts +22 -0
  42. package/src/testing/manifest.ts +559 -0
  43. package/src/testing/probe.ts +446 -0
  44. package/src/testing/scripted-transport.ts +775 -0
  45. package/src/testing/session.ts +525 -0
  46. package/src/tooling/bombadil-campaign.ts +288 -0
  47. package/src/tooling/bombadil-internal.d.ts +46 -0
  48. package/src/tooling/bombadil-runner.ts +1424 -0
  49. package/src/tooling/bombadil.ts +27 -0
  50. package/src/tooling/browser-verification-entry.ts +32 -0
  51. package/src/tooling/browser-verification.ts +916 -0
  52. package/src/tooling/bundle-boundary.ts +159 -0
  53. package/src/web/browser-bridge.ts +296 -0
  54. package/src/web/browser.ts +277 -0
  55. package/src/web/fetch-firewall.ts +251 -0
  56. package/src/web.ts +27 -0
@@ -0,0 +1,784 @@
1
+ import { parseOperationId, type OperationId } from "./ids.js";
2
+ import {
3
+ cloneJson,
4
+ DEFAULT_JSON_LIMITS,
5
+ parseAndCloneWorld,
6
+ utf8ByteLength,
7
+ type WorldParser,
8
+ } from "./json.js";
9
+ import type { JsonArray, JsonPrimitive, JsonValue } from "./json-value.js";
10
+ import { renderUnknownReason } from "./reason.js";
11
+ import { err, ok, type Result } from "./result.js";
12
+
13
+ declare const generationBrand: unique symbol;
14
+ export type StoreGeneration = number & { readonly [generationBrand]: "StoreGeneration" };
15
+
16
+ export interface ActivitySnapshot {
17
+ readonly active: number;
18
+ readonly started: number;
19
+ readonly settled: number;
20
+ }
21
+
22
+ export interface DirectStoreSnapshot<World extends JsonValue> {
23
+ readonly generation: StoreGeneration;
24
+ readonly revision: number;
25
+ readonly world: World;
26
+ readonly activity: ActivitySnapshot;
27
+ }
28
+
29
+ export type StoreErrorCode =
30
+ | "activity-not-found"
31
+ | "duplicate-activity"
32
+ | "generation-overflow"
33
+ | "invalid-operation"
34
+ | "invalid-world"
35
+ | "stale-generation"
36
+ | "transaction-conflict"
37
+ | "transaction-failed";
38
+
39
+ export interface StoreError {
40
+ readonly code: StoreErrorCode;
41
+ readonly message: string;
42
+ readonly operation: OperationId | null;
43
+ }
44
+
45
+ export interface TypedActivityLease<World extends JsonValue> {
46
+ readonly generation: StoreGeneration;
47
+ readonly operation: OperationId;
48
+ readonly settle: () => Result<DirectStoreSnapshot<World>, StoreError>;
49
+ }
50
+
51
+ export interface DirectStore<World extends JsonValue> {
52
+ readonly getSnapshot: () => DirectStoreSnapshot<World>;
53
+ readonly subscribe: (listener: () => void | PromiseLike<void>) => () => void;
54
+ readonly transact: (
55
+ generation: StoreGeneration,
56
+ operation: OperationId,
57
+ update: (draft: World) => World | void,
58
+ ) => Result<DirectStoreSnapshot<World>, StoreError>;
59
+ readonly transactReplacements: (
60
+ generation: StoreGeneration,
61
+ operation: OperationId,
62
+ replacements: readonly DirectStorePrimitiveReplacement[],
63
+ ) => Result<DirectStoreSnapshot<World>, StoreError>;
64
+ readonly reset: (world: World) => Result<DirectStoreSnapshot<World>, StoreError>;
65
+ readonly beginActivity: (
66
+ generation: StoreGeneration,
67
+ operation: OperationId,
68
+ ) => Result<TypedActivityLease<World>, StoreError>;
69
+ readonly settleActivity: (
70
+ generation: StoreGeneration,
71
+ operation: OperationId,
72
+ ) => Result<DirectStoreSnapshot<World>, StoreError>;
73
+ readonly isQuiescent: (generation: StoreGeneration) => Result<boolean, StoreError>;
74
+ readonly whenQuiescent: (
75
+ generation: StoreGeneration,
76
+ ) => Promise<Result<DirectStoreSnapshot<World>, StoreError>>;
77
+ }
78
+
79
+ export const DIRECT_STORE_MAX_REPLACEMENTS = 32;
80
+ export const DIRECT_STORE_MAX_REPLACEMENT_PATH_DEPTH = 32;
81
+
82
+ export type DirectStoreReplacementPathSegment = number | string;
83
+
84
+ export interface DirectStorePrimitiveReplacement {
85
+ readonly expected: JsonPrimitive;
86
+ readonly path: readonly DirectStoreReplacementPathSegment[];
87
+ readonly value: JsonPrimitive;
88
+ }
89
+
90
+ export interface DirectStoreReplacementValidationContext<
91
+ World extends JsonValue,
92
+ > {
93
+ readonly baseWorld: World;
94
+ readonly candidateWorld: World;
95
+ readonly generation: StoreGeneration;
96
+ readonly operation: OperationId;
97
+ readonly replacements: readonly DirectStorePrimitiveReplacement[];
98
+ }
99
+
100
+ export type DirectStoreReplacementValidator<World extends JsonValue> = (
101
+ context: DirectStoreReplacementValidationContext<World>,
102
+ ) => undefined;
103
+
104
+ export interface DirectStoreOptions<World extends JsonValue = JsonValue> {
105
+ /** Listener failures are isolated from committed state and reported here. */
106
+ readonly onListenerError?: (reason: unknown) => void;
107
+ /**
108
+ * Optional app-owned semantic gate for bounded primitive-leaf replacements.
109
+ * It is captured once at store construction and must return `undefined`.
110
+ */
111
+ readonly validateReplacements?: DirectStoreReplacementValidator<World>;
112
+ }
113
+
114
+ function storeError(code: StoreErrorCode, message: string, operation: OperationId | null = null): StoreError {
115
+ return { code, message, operation };
116
+ }
117
+
118
+ type ReplacementTrieNode = {
119
+ readonly children: Map<DirectStoreReplacementPathSegment, ReplacementTrieNode>;
120
+ replacement: DirectStorePrimitiveReplacement | null;
121
+ };
122
+
123
+ type ReplacementInputBudget = {
124
+ expectedStringBytes: number;
125
+ pathStringBytes: number;
126
+ valueStringBytes: number;
127
+ };
128
+
129
+ function replacementFailure(message: string): never {
130
+ throw new TypeError(message);
131
+ }
132
+
133
+ function standardRecord(input: object): boolean {
134
+ const prototype = Object.getPrototypeOf(input) as unknown;
135
+ return prototype === Object.prototype || prototype === null;
136
+ }
137
+
138
+ function isJsonArray(input: JsonValue): input is JsonArray {
139
+ return Array.isArray(input);
140
+ }
141
+
142
+ function ownEnumerableDataValue(input: object, key: string): unknown {
143
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
144
+ if (
145
+ descriptor === undefined
146
+ || descriptor.enumerable !== true
147
+ || !("value" in descriptor)
148
+ ) {
149
+ replacementFailure(`Replacement property ${JSON.stringify(key)} must be an own enumerable data property.`);
150
+ }
151
+ return descriptor.value;
152
+ }
153
+
154
+ function exactEnumerableKeys(
155
+ input: object,
156
+ expected: readonly string[],
157
+ label: string,
158
+ ): void {
159
+ if (Object.getOwnPropertySymbols(input).length > 0) {
160
+ replacementFailure(`${label} cannot contain symbol properties.`);
161
+ }
162
+ const keys = Object.keys(input);
163
+ const ownKeys = Reflect.ownKeys(input);
164
+ if (
165
+ keys.length !== expected.length
166
+ || ownKeys.length !== expected.length
167
+ || expected.some(key => !Object.hasOwn(input, key))
168
+ ) {
169
+ replacementFailure(`${label} must contain exactly ${expected.join(", ")}.`);
170
+ }
171
+ for (const key of expected) ownEnumerableDataValue(input, key);
172
+ }
173
+
174
+ function denseStandardArrayValues(
175
+ input: unknown,
176
+ label: string,
177
+ maximumLength: number,
178
+ ): unknown[] {
179
+ if (
180
+ !Array.isArray(input)
181
+ || Object.getPrototypeOf(input) !== Array.prototype
182
+ ) {
183
+ replacementFailure(`${label} must be a standard array.`);
184
+ }
185
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(input, "length");
186
+ if (
187
+ lengthDescriptor === undefined
188
+ || !("value" in lengthDescriptor)
189
+ ) {
190
+ replacementFailure(
191
+ `${label} must have a data length from 0 through ${String(maximumLength)}.`,
192
+ );
193
+ }
194
+ const lengthValue = lengthDescriptor.value as unknown;
195
+ if (
196
+ typeof lengthValue !== "number"
197
+ || !Number.isSafeInteger(lengthValue)
198
+ || lengthValue < 0
199
+ || lengthValue > maximumLength
200
+ ) {
201
+ replacementFailure(
202
+ `${label} must have a data length from 0 through ${String(maximumLength)}.`,
203
+ );
204
+ }
205
+ const length = lengthValue;
206
+ const ownKeys = Reflect.ownKeys(input);
207
+ if (ownKeys.length !== length + 1 || ownKeys.at(-1) !== "length") {
208
+ replacementFailure(`${label} must be dense and cannot contain extra properties.`);
209
+ }
210
+ return Array.from({ length }, (_, index) => {
211
+ const key = String(index);
212
+ if (ownKeys[index] !== key) {
213
+ replacementFailure(`${label} must contain every index exactly once.`);
214
+ }
215
+ return ownEnumerableDataValue(input, key);
216
+ });
217
+ }
218
+
219
+ function consumeReplacementString(
220
+ value: string,
221
+ label: string,
222
+ budget: ReplacementInputBudget,
223
+ budgetKey: keyof ReplacementInputBudget,
224
+ ): string {
225
+ budget[budgetKey] += utf8ByteLength(value);
226
+ if (budget[budgetKey] > DEFAULT_JSON_LIMITS.maxStringBytes) {
227
+ replacementFailure(`${label} exceeds the replacement string byte limit.`);
228
+ }
229
+ return value;
230
+ }
231
+
232
+ function replacementPrimitive(
233
+ input: unknown,
234
+ label: string,
235
+ budget: ReplacementInputBudget,
236
+ budgetKey: "expectedStringBytes" | "valueStringBytes",
237
+ ): JsonPrimitive {
238
+ if (input === null || typeof input === "boolean") return input;
239
+ if (typeof input === "number") {
240
+ if (!Number.isFinite(input) || Object.is(input, -0)) {
241
+ replacementFailure(`${label} must be a finite normalized JSON number.`);
242
+ }
243
+ return input;
244
+ }
245
+ if (typeof input === "string") {
246
+ return consumeReplacementString(input, label, budget, budgetKey);
247
+ }
248
+ return replacementFailure(`${label} must be a JSON primitive.`);
249
+ }
250
+
251
+ function replacementPath(
252
+ input: unknown,
253
+ index: number,
254
+ budget: ReplacementInputBudget,
255
+ ): readonly DirectStoreReplacementPathSegment[] {
256
+ const values = denseStandardArrayValues(
257
+ input,
258
+ `Replacement ${String(index)} path`,
259
+ DIRECT_STORE_MAX_REPLACEMENT_PATH_DEPTH,
260
+ );
261
+ if (values.length === 0) {
262
+ replacementFailure(
263
+ `Replacement ${String(index)} path must contain 1 through ${String(DIRECT_STORE_MAX_REPLACEMENT_PATH_DEPTH)} segments.`,
264
+ );
265
+ }
266
+ return Object.freeze(values.map((segment, segmentIndex) => {
267
+ if (typeof segment === "string") {
268
+ return consumeReplacementString(
269
+ segment,
270
+ `Replacement ${String(index)} path segment ${String(segmentIndex)}`,
271
+ budget,
272
+ "pathStringBytes",
273
+ );
274
+ }
275
+ if (
276
+ typeof segment === "number"
277
+ && Number.isSafeInteger(segment)
278
+ && segment >= 0
279
+ && !Object.is(segment, -0)
280
+ ) {
281
+ return segment;
282
+ }
283
+ return replacementFailure(
284
+ `Replacement ${String(index)} path segment ${String(segmentIndex)} must be a string or non-negative safe integer.`,
285
+ );
286
+ }));
287
+ }
288
+
289
+ function parsePrimitiveReplacements(
290
+ input: unknown,
291
+ ): readonly DirectStorePrimitiveReplacement[] {
292
+ const values = denseStandardArrayValues(
293
+ input,
294
+ "Replacements",
295
+ DIRECT_STORE_MAX_REPLACEMENTS,
296
+ );
297
+ if (values.length === 0) {
298
+ replacementFailure(
299
+ `A replacement transaction must contain 1 through ${String(DIRECT_STORE_MAX_REPLACEMENTS)} entries.`,
300
+ );
301
+ }
302
+ const budget: ReplacementInputBudget = {
303
+ expectedStringBytes: 0,
304
+ pathStringBytes: 0,
305
+ valueStringBytes: 0,
306
+ };
307
+ return Object.freeze(values.map((value, index) => {
308
+ if (typeof value !== "object" || value === null || Array.isArray(value) || !standardRecord(value)) {
309
+ replacementFailure(`Replacement ${String(index)} must be a standard or null-prototype object.`);
310
+ }
311
+ exactEnumerableKeys(value, ["expected", "path", "value"], `Replacement ${String(index)}`);
312
+ return Object.freeze({
313
+ expected: replacementPrimitive(
314
+ ownEnumerableDataValue(value, "expected"),
315
+ `Replacement ${String(index)} expected value`,
316
+ budget,
317
+ "expectedStringBytes",
318
+ ),
319
+ path: replacementPath(
320
+ ownEnumerableDataValue(value, "path"),
321
+ index,
322
+ budget,
323
+ ),
324
+ value: replacementPrimitive(
325
+ ownEnumerableDataValue(value, "value"),
326
+ `Replacement ${String(index)} value`,
327
+ budget,
328
+ "valueStringBytes",
329
+ ),
330
+ });
331
+ }));
332
+ }
333
+
334
+ function replacementTrie(
335
+ replacements: readonly DirectStorePrimitiveReplacement[],
336
+ ): ReplacementTrieNode {
337
+ const root: ReplacementTrieNode = { children: new Map(), replacement: null };
338
+ for (const replacement of replacements) {
339
+ let node = root;
340
+ for (const segment of replacement.path) {
341
+ if (node.replacement !== null) {
342
+ replacementFailure("Replacement paths cannot overlap.");
343
+ }
344
+ let child = node.children.get(segment);
345
+ if (child === undefined) {
346
+ child = { children: new Map(), replacement: null };
347
+ node.children.set(segment, child);
348
+ }
349
+ node = child;
350
+ }
351
+ if (node.replacement !== null) {
352
+ replacementFailure("Replacement paths cannot be duplicated.");
353
+ }
354
+ if (node.children.size > 0) {
355
+ replacementFailure("Replacement paths cannot overlap.");
356
+ }
357
+ node.replacement = replacement;
358
+ }
359
+ return root;
360
+ }
361
+
362
+ function samePrimitive(left: JsonPrimitive, right: JsonPrimitive): boolean {
363
+ return left === right;
364
+ }
365
+
366
+ function primitiveShapeMatches(
367
+ current: JsonPrimitive,
368
+ replacement: JsonPrimitive,
369
+ ): boolean {
370
+ return current === null
371
+ ? replacement === null
372
+ : replacement !== null && typeof current === typeof replacement;
373
+ }
374
+
375
+ function replacedJsonValue(
376
+ current: JsonValue,
377
+ node: ReplacementTrieNode,
378
+ path: string,
379
+ ): JsonValue {
380
+ if (node.replacement !== null) {
381
+ if (current !== null && typeof current === "object") {
382
+ return replacementFailure(`${path} is a container, not a primitive leaf.`);
383
+ }
384
+ if (!samePrimitive(current, node.replacement.expected)) {
385
+ return replacementFailure(`${path} no longer matches its expected value.`);
386
+ }
387
+ if (!primitiveShapeMatches(current, node.replacement.value)) {
388
+ return replacementFailure(`${path} replacement would change the JSON shape.`);
389
+ }
390
+ return node.replacement.value;
391
+ }
392
+
393
+ if (current === null || typeof current !== "object") {
394
+ return replacementFailure(`${path} is a primitive and cannot contain a replacement path.`);
395
+ }
396
+ if (isJsonArray(current)) {
397
+ const next: JsonValue[] = [...current];
398
+ for (const [segment, child] of node.children) {
399
+ if (
400
+ typeof segment !== "number"
401
+ || !Number.isSafeInteger(segment)
402
+ || segment < 0
403
+ || segment >= current.length
404
+ ) {
405
+ return replacementFailure(`${path} requires an existing numeric array index.`);
406
+ }
407
+ const currentValue = current[segment];
408
+ if (currentValue === undefined) {
409
+ return replacementFailure(`${path} requires an existing numeric array index.`);
410
+ }
411
+ next[segment] = replacedJsonValue(
412
+ currentValue,
413
+ child,
414
+ `${path}[${String(segment)}]`,
415
+ );
416
+ }
417
+ return Object.freeze(next);
418
+ }
419
+
420
+ const prototype = Object.getPrototypeOf(current) as unknown;
421
+ if (prototype !== Object.prototype && prototype !== null) {
422
+ return replacementFailure(`${path} is not a standard JSON object.`);
423
+ }
424
+ const record = current as Readonly<Record<string, JsonValue>>;
425
+ const next = (
426
+ prototype === null ? Object.create(null) : {}
427
+ ) as Record<string, JsonValue>;
428
+ for (const key of Object.keys(record)) {
429
+ const child = node.children.get(key);
430
+ const currentValue = record[key];
431
+ if (currentValue === undefined) {
432
+ return replacementFailure(`${path} requires an existing string-keyed property.`);
433
+ }
434
+ Object.defineProperty(next, key, {
435
+ configurable: true,
436
+ enumerable: true,
437
+ value: child === undefined
438
+ ? currentValue
439
+ : replacedJsonValue(currentValue, child, `${path}.${key}`),
440
+ writable: true,
441
+ });
442
+ }
443
+ for (const segment of node.children.keys()) {
444
+ if (typeof segment !== "string" || !Object.hasOwn(record, segment)) {
445
+ return replacementFailure(`${path} requires an existing string-keyed property.`);
446
+ }
447
+ }
448
+ return Object.freeze(next);
449
+ }
450
+
451
+ function applyPrimitiveReplacements<World extends JsonValue>(
452
+ world: World,
453
+ replacements: readonly DirectStorePrimitiveReplacement[],
454
+ ): World {
455
+ const trie = replacementTrie(replacements);
456
+ const stringByteDelta = replacements.reduce((delta, replacement) => (
457
+ delta
458
+ + (typeof replacement.value === "string"
459
+ ? utf8ByteLength(replacement.value)
460
+ : 0)
461
+ - (typeof replacement.expected === "string"
462
+ ? utf8ByteLength(replacement.expected)
463
+ : 0)
464
+ ), 0);
465
+ if (stringByteDelta > 0) {
466
+ replacementFailure(
467
+ "Primitive replacements cannot increase aggregate raw UTF-8 string bytes.",
468
+ );
469
+ }
470
+ return replacedJsonValue(world, trie, "$") as World;
471
+ }
472
+
473
+ function generation(value: number): StoreGeneration {
474
+ return value as StoreGeneration;
475
+ }
476
+
477
+ function activity(active: number, started: number, settled: number): ActivitySnapshot {
478
+ return Object.freeze({ active, started, settled });
479
+ }
480
+
481
+ function storeSnapshot<World extends JsonValue>(
482
+ currentGeneration: StoreGeneration,
483
+ revision: number,
484
+ world: World,
485
+ currentActivity: ActivitySnapshot,
486
+ ): DirectStoreSnapshot<World> {
487
+ return Object.freeze({ generation: currentGeneration, revision, world, activity: currentActivity });
488
+ }
489
+
490
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
491
+ return (
492
+ (typeof value === "object" && value !== null) || typeof value === "function"
493
+ ) && typeof Reflect.get(value, "then") === "function";
494
+ }
495
+
496
+ export function createDirectStore<World extends JsonValue>(
497
+ initialWorld: World,
498
+ parseWorld: WorldParser<World>,
499
+ options: DirectStoreOptions<World> = {},
500
+ ): Result<DirectStore<World>, StoreError> {
501
+ const initial = parseAndCloneWorld(initialWorld, parseWorld);
502
+ if (!initial.ok) {
503
+ return err(storeError("invalid-world", initial.error.message));
504
+ }
505
+
506
+ let currentGeneration = generation(1);
507
+ let revision = 0;
508
+ let currentActivity = activity(0, 0, 0);
509
+ let snapshot = storeSnapshot(currentGeneration, revision, initial.value, currentActivity);
510
+ const listeners = new Set<() => void | PromiseLike<void>>();
511
+ const activeOperations = new Set<OperationId>();
512
+ let onListenerError: DirectStoreOptions<World>["onListenerError"];
513
+ let validateReplacements:
514
+ DirectStoreOptions<World>["validateReplacements"];
515
+ try {
516
+ onListenerError = options.onListenerError;
517
+ validateReplacements = options.validateReplacements;
518
+ } catch (reason) {
519
+ return err(storeError(
520
+ "invalid-world",
521
+ renderUnknownReason(reason, "Direct store options could not be inspected"),
522
+ ));
523
+ }
524
+ if (
525
+ onListenerError !== undefined
526
+ && typeof onListenerError !== "function"
527
+ ) {
528
+ return err(storeError("invalid-world", "Direct listener error reporting must be callable."));
529
+ }
530
+ if (
531
+ validateReplacements !== undefined
532
+ && typeof validateReplacements !== "function"
533
+ ) {
534
+ return err(storeError("invalid-world", "Direct replacement validation must be callable."));
535
+ }
536
+
537
+ const reportListenerError = (reason: unknown): void => {
538
+ if (onListenerError === undefined) return;
539
+ try {
540
+ const returned: unknown = onListenerError(reason);
541
+ if (isPromiseLike(returned)) {
542
+ void Promise.resolve(returned).catch(() => undefined);
543
+ }
544
+ } catch {
545
+ // A reporter is another listener boundary and cannot roll back committed state.
546
+ }
547
+ };
548
+
549
+ const publish = (world: World = snapshot.world): DirectStoreSnapshot<World> => {
550
+ revision += 1;
551
+ const committed = storeSnapshot(currentGeneration, revision, world, currentActivity);
552
+ snapshot = committed;
553
+ for (const listener of [...listeners]) {
554
+ try {
555
+ const returned: unknown = listener();
556
+ if (isPromiseLike(returned)) {
557
+ void Promise.resolve(returned).catch(reportListenerError);
558
+ }
559
+ } catch (reason) {
560
+ reportListenerError(reason);
561
+ }
562
+ }
563
+ return committed;
564
+ };
565
+
566
+ const stale = (expected: StoreGeneration, operation: OperationId | null = null): StoreError | null => (
567
+ expected === currentGeneration
568
+ ? null
569
+ : storeError(
570
+ "stale-generation",
571
+ `Generation ${String(expected)} is stale; current generation is ${String(currentGeneration)}`,
572
+ operation,
573
+ )
574
+ );
575
+
576
+ const validateOperation = (candidate: OperationId): Result<OperationId, StoreError> => {
577
+ const parsed = parseOperationId(candidate);
578
+ return parsed.ok
579
+ ? ok(parsed.value)
580
+ : err(storeError("invalid-operation", parsed.error.message));
581
+ };
582
+
583
+ const settleActivity = (
584
+ expected: StoreGeneration,
585
+ candidate: OperationId,
586
+ ): Result<DirectStoreSnapshot<World>, StoreError> => {
587
+ const operation = validateOperation(candidate);
588
+ if (!operation.ok) {
589
+ return operation;
590
+ }
591
+ const staleError = stale(expected, operation.value);
592
+ if (staleError !== null) {
593
+ return err(staleError);
594
+ }
595
+ if (!activeOperations.delete(operation.value)) {
596
+ return err(storeError("activity-not-found", `Activity is not active: ${operation.value}`, operation.value));
597
+ }
598
+ currentActivity = activity(
599
+ currentActivity.active - 1,
600
+ currentActivity.started,
601
+ currentActivity.settled + 1,
602
+ );
603
+ return ok(publish());
604
+ };
605
+
606
+ const store: DirectStore<World> = {
607
+ getSnapshot: () => snapshot,
608
+ subscribe: (listener: () => void | PromiseLike<void>) => {
609
+ listeners.add(listener);
610
+ return () => {
611
+ listeners.delete(listener);
612
+ };
613
+ },
614
+ transact: (expected, candidate, update) => {
615
+ const operation = validateOperation(candidate);
616
+ if (!operation.ok) {
617
+ return operation;
618
+ }
619
+ const staleError = stale(expected, operation.value);
620
+ if (staleError !== null) {
621
+ return err(staleError);
622
+ }
623
+ const baseSnapshot = snapshot;
624
+ const cloned = cloneJson(snapshot.world);
625
+ if (!cloned.ok) {
626
+ return err(storeError("invalid-world", cloned.error.message, operation.value));
627
+ }
628
+ let candidateWorld: World;
629
+ try {
630
+ // The current snapshot already passed parseWorld. Its JSON clone is an
631
+ // owned mutable draft and cannot alias a value returned by the parser.
632
+ const draft = cloned.value as World;
633
+ const returned = update(draft);
634
+ candidateWorld = returned === undefined ? draft : returned;
635
+ } catch (reason) {
636
+ return err(storeError("transaction-failed", renderUnknownReason(reason), operation.value));
637
+ }
638
+ const validated = parseAndCloneWorld(candidateWorld, parseWorld);
639
+ if (!validated.ok) {
640
+ return err(storeError("invalid-world", validated.error.message, operation.value));
641
+ }
642
+ const nextStaleError = stale(expected, operation.value);
643
+ if (nextStaleError !== null) {
644
+ return err(nextStaleError);
645
+ }
646
+ if (snapshot !== baseSnapshot) {
647
+ return err(storeError(
648
+ "transaction-conflict",
649
+ `Store revision changed during transaction ${operation.value}`,
650
+ operation.value,
651
+ ));
652
+ }
653
+ return ok(publish(validated.value));
654
+ },
655
+ transactReplacements: (expected, candidate, input) => {
656
+ const operation = validateOperation(candidate);
657
+ if (!operation.ok) {
658
+ return operation;
659
+ }
660
+ const staleError = stale(expected, operation.value);
661
+ if (staleError !== null) {
662
+ return err(staleError);
663
+ }
664
+ if (validateReplacements === undefined) {
665
+ return err(storeError(
666
+ "invalid-world",
667
+ "This Direct store does not define a primitive replacement validator.",
668
+ operation.value,
669
+ ));
670
+ }
671
+ const baseSnapshot = snapshot;
672
+ let replacements: readonly DirectStorePrimitiveReplacement[];
673
+ let candidateWorld: World;
674
+ try {
675
+ replacements = parsePrimitiveReplacements(input);
676
+ candidateWorld = applyPrimitiveReplacements(
677
+ baseSnapshot.world,
678
+ replacements,
679
+ );
680
+ const returned: unknown = validateReplacements(Object.freeze({
681
+ baseWorld: baseSnapshot.world,
682
+ candidateWorld,
683
+ generation: expected,
684
+ operation: operation.value,
685
+ replacements,
686
+ }));
687
+ if (returned !== undefined) {
688
+ if (isPromiseLike(returned)) {
689
+ void Promise.resolve(returned).catch(() => undefined);
690
+ }
691
+ throw new TypeError(
692
+ "Direct replacement validation must complete synchronously and return undefined.",
693
+ );
694
+ }
695
+ } catch (reason) {
696
+ return err(storeError(
697
+ "invalid-world",
698
+ renderUnknownReason(reason, "Direct primitive replacements are invalid"),
699
+ operation.value,
700
+ ));
701
+ }
702
+ const nextStaleError = stale(expected, operation.value);
703
+ if (nextStaleError !== null) {
704
+ return err(nextStaleError);
705
+ }
706
+ if (snapshot !== baseSnapshot) {
707
+ return err(storeError(
708
+ "transaction-conflict",
709
+ `Store revision changed during transaction ${operation.value}`,
710
+ operation.value,
711
+ ));
712
+ }
713
+ return ok(publish(candidateWorld));
714
+ },
715
+ reset: (world) => {
716
+ const validated = parseAndCloneWorld(world, parseWorld);
717
+ if (!validated.ok) {
718
+ return err(storeError("invalid-world", validated.error.message));
719
+ }
720
+ const nextGeneration = Number(currentGeneration) + 1;
721
+ if (!Number.isSafeInteger(nextGeneration)) {
722
+ return err(storeError("generation-overflow", "Store generation exceeds the safe integer range"));
723
+ }
724
+ currentGeneration = generation(nextGeneration);
725
+ activeOperations.clear();
726
+ currentActivity = activity(0, 0, 0);
727
+ return ok(publish(validated.value));
728
+ },
729
+ beginActivity: (expected, candidate) => {
730
+ const operation = validateOperation(candidate);
731
+ if (!operation.ok) {
732
+ return operation;
733
+ }
734
+ const staleError = stale(expected, operation.value);
735
+ if (staleError !== null) {
736
+ return err(staleError);
737
+ }
738
+ if (activeOperations.has(operation.value)) {
739
+ return err(storeError("duplicate-activity", `Activity is already active: ${operation.value}`, operation.value));
740
+ }
741
+ activeOperations.add(operation.value);
742
+ currentActivity = activity(
743
+ currentActivity.active + 1,
744
+ currentActivity.started + 1,
745
+ currentActivity.settled,
746
+ );
747
+ publish();
748
+ const lease: TypedActivityLease<World> = Object.freeze({
749
+ generation: expected,
750
+ operation: operation.value,
751
+ settle: () => settleActivity(expected, operation.value),
752
+ });
753
+ return ok(lease);
754
+ },
755
+ settleActivity,
756
+ isQuiescent: (expected) => {
757
+ const staleError = stale(expected);
758
+ return staleError === null ? ok(currentActivity.active === 0) : err(staleError);
759
+ },
760
+ whenQuiescent: (expected) => {
761
+ const staleError = stale(expected);
762
+ if (staleError !== null) {
763
+ return Promise.resolve(err(staleError));
764
+ }
765
+ if (currentActivity.active === 0) {
766
+ return Promise.resolve(ok(snapshot));
767
+ }
768
+ return new Promise((resolve) => {
769
+ const unsubscribe = store.subscribe(() => {
770
+ const nextStaleError = stale(expected);
771
+ if (nextStaleError !== null) {
772
+ unsubscribe();
773
+ resolve(err(nextStaleError));
774
+ } else if (currentActivity.active === 0) {
775
+ unsubscribe();
776
+ resolve(ok(snapshot));
777
+ }
778
+ });
779
+ });
780
+ },
781
+ };
782
+
783
+ return ok(Object.freeze(store));
784
+ }