@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.
- package/LICENSE +21 -0
- package/README.md +436 -0
- package/dist/core/index.js +162 -0
- package/dist/index-1csg00w4.js +1167 -0
- package/dist/index-6mdfd2ey.js +464 -0
- package/dist/index-7n1h75n6.js +616 -0
- package/dist/index.js +232 -0
- package/dist/react.js +32 -0
- package/dist/testing/index.js +1069 -0
- package/dist/tooling/bombadil.js +2117 -0
- package/dist/tooling/browser-verification-entry.js +1499 -0
- package/dist/tooling/bundle-boundary.js +119 -0
- package/dist/web.js +605 -0
- package/package.json +179 -0
- package/skills/direct/AGENTS.md +13 -0
- package/skills/direct/SKILL.md +49 -0
- package/skills/direct/agents/openai.yaml +4 -0
- package/skills/direct/references/adoption.md +131 -0
- package/skills/direct/references/install.md +91 -0
- package/skills/direct/references/verification.md +247 -0
- package/src/core/coverage.ts +336 -0
- package/src/core/definition.ts +378 -0
- package/src/core/effects.ts +88 -0
- package/src/core/fixture.ts +185 -0
- package/src/core/ids.ts +77 -0
- package/src/core/index.ts +13 -0
- package/src/core/json-value.ts +7 -0
- package/src/core/json.ts +593 -0
- package/src/core/query.ts +230 -0
- package/src/core/reason.ts +16 -0
- package/src/core/resource.ts +10 -0
- package/src/core/result.ts +19 -0
- package/src/core/runtime.ts +229 -0
- package/src/core/scenario.ts +149 -0
- package/src/core/store.ts +784 -0
- package/src/index.ts +51 -0
- package/src/react.ts +54 -0
- package/src/testing/activity.ts +228 -0
- package/src/testing/coverage-binding.ts +99 -0
- package/src/testing/evidence.ts +59 -0
- package/src/testing/index.ts +22 -0
- package/src/testing/manifest.ts +559 -0
- package/src/testing/probe.ts +446 -0
- package/src/testing/scripted-transport.ts +775 -0
- package/src/testing/session.ts +525 -0
- package/src/tooling/bombadil-campaign.ts +288 -0
- package/src/tooling/bombadil-internal.d.ts +46 -0
- package/src/tooling/bombadil-runner.ts +1424 -0
- package/src/tooling/bombadil.ts +27 -0
- package/src/tooling/browser-verification-entry.ts +32 -0
- package/src/tooling/browser-verification.ts +916 -0
- package/src/tooling/bundle-boundary.ts +159 -0
- package/src/web/browser-bridge.ts +296 -0
- package/src/web/browser.ts +277 -0
- package/src/web/fetch-firewall.ts +251 -0
- package/src/web.ts +27 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import type { DirectDefinition } from "../core/definition.js";
|
|
2
|
+
import {
|
|
3
|
+
parseTaggedStableHash,
|
|
4
|
+
type TaggedStableHash,
|
|
5
|
+
} from "../core/json.js";
|
|
6
|
+
import type { JsonValue } from "../core/json-value.js";
|
|
7
|
+
import { renderUnknownReason } from "../core/reason.js";
|
|
8
|
+
import { err, isRecord, ok, type Result } from "../core/result.js";
|
|
9
|
+
import {
|
|
10
|
+
createLogicalRuntime,
|
|
11
|
+
parseLogicalRuntimeSnapshot,
|
|
12
|
+
type LogicalRuntime,
|
|
13
|
+
type LogicalRuntimeSnapshot,
|
|
14
|
+
type LogicalSleep,
|
|
15
|
+
} from "../core/runtime.js";
|
|
16
|
+
import {
|
|
17
|
+
createDirectStore,
|
|
18
|
+
type DirectStore,
|
|
19
|
+
type DirectStoreOptions,
|
|
20
|
+
type StoreError,
|
|
21
|
+
} from "../core/store.js";
|
|
22
|
+
import type { ActiveDirect, QueryError } from "../core/query.js";
|
|
23
|
+
import { createDirectActivityScope, type DirectActivityScope } from "./activity.js";
|
|
24
|
+
import {
|
|
25
|
+
createDirectProbe,
|
|
26
|
+
type DirectCounterSource,
|
|
27
|
+
type DirectProbe,
|
|
28
|
+
type DirectProbeError,
|
|
29
|
+
} from "./probe.js";
|
|
30
|
+
import {
|
|
31
|
+
createDirectSessionManifest,
|
|
32
|
+
type DirectSessionManifest,
|
|
33
|
+
} from "./manifest.js";
|
|
34
|
+
|
|
35
|
+
/** A synchronous cleanup callback. Promise-returning teardown is intentionally unsupported. */
|
|
36
|
+
export type DirectSessionCleanup = () => undefined;
|
|
37
|
+
|
|
38
|
+
export interface DirectSessionRegistrationError {
|
|
39
|
+
readonly code: "invalid-cleanup" | "session-disposed";
|
|
40
|
+
readonly message: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type DirectSessionActivation =
|
|
44
|
+
| { readonly kind: "query"; readonly source: string }
|
|
45
|
+
| { readonly kind: "scenario"; readonly scenario: unknown };
|
|
46
|
+
|
|
47
|
+
export interface DirectSessionContext<World extends JsonValue, Route extends string> {
|
|
48
|
+
readonly activation: ActiveDirect<World, Route>;
|
|
49
|
+
readonly world: World;
|
|
50
|
+
readonly store: DirectStore<World>;
|
|
51
|
+
readonly clock: LogicalRuntime;
|
|
52
|
+
readonly activity: DirectActivityScope;
|
|
53
|
+
/** Aborted before registered cleanup callbacks run. */
|
|
54
|
+
readonly signal: AbortSignal;
|
|
55
|
+
/** Register synchronous cleanup during harness and observation construction. */
|
|
56
|
+
readonly onDispose: (cleanup: DirectSessionCleanup) => undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DirectSessionObservation {
|
|
60
|
+
readonly pending?: readonly DirectCounterSource[];
|
|
61
|
+
readonly violations?: readonly DirectCounterSource[];
|
|
62
|
+
readonly readRemainingWork?: () => JsonValue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface DirectSessionOptions<
|
|
66
|
+
World extends JsonValue,
|
|
67
|
+
Route extends string,
|
|
68
|
+
Harness,
|
|
69
|
+
> {
|
|
70
|
+
readonly definition: DirectDefinition<World, Route>;
|
|
71
|
+
readonly activation: DirectSessionActivation;
|
|
72
|
+
/** Runs synchronously. Direct does not await a Promise returned as the harness value. */
|
|
73
|
+
readonly create: (context: DirectSessionContext<World, Route>) => Harness;
|
|
74
|
+
/** Runs synchronously after harness construction. Omit when no additional counters are needed. */
|
|
75
|
+
readonly observe?: (
|
|
76
|
+
harness: Harness,
|
|
77
|
+
context: DirectSessionContext<World, Route>,
|
|
78
|
+
) => DirectSessionObservation;
|
|
79
|
+
readonly sleep?: LogicalSleep;
|
|
80
|
+
readonly storeOptions?: DirectStoreOptions<World>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type DirectSessionError =
|
|
84
|
+
| {
|
|
85
|
+
readonly code: "invalid-options";
|
|
86
|
+
readonly message: string;
|
|
87
|
+
readonly queryError: null;
|
|
88
|
+
readonly storeError: null;
|
|
89
|
+
readonly probeError: null;
|
|
90
|
+
readonly cleanupErrors: readonly string[];
|
|
91
|
+
}
|
|
92
|
+
| {
|
|
93
|
+
readonly code: "activation-failed";
|
|
94
|
+
readonly message: string;
|
|
95
|
+
readonly queryError: QueryError;
|
|
96
|
+
readonly storeError: null;
|
|
97
|
+
readonly probeError: null;
|
|
98
|
+
readonly cleanupErrors: readonly string[];
|
|
99
|
+
}
|
|
100
|
+
| {
|
|
101
|
+
readonly code: "store-failed";
|
|
102
|
+
readonly message: string;
|
|
103
|
+
readonly queryError: null;
|
|
104
|
+
readonly storeError: StoreError;
|
|
105
|
+
readonly probeError: null;
|
|
106
|
+
readonly cleanupErrors: readonly string[];
|
|
107
|
+
}
|
|
108
|
+
| {
|
|
109
|
+
readonly code: "harness-failed" | "observation-failed";
|
|
110
|
+
readonly message: string;
|
|
111
|
+
readonly queryError: null;
|
|
112
|
+
readonly storeError: null;
|
|
113
|
+
readonly probeError: null;
|
|
114
|
+
readonly cleanupErrors: readonly string[];
|
|
115
|
+
}
|
|
116
|
+
| {
|
|
117
|
+
readonly code: "probe-failed";
|
|
118
|
+
readonly message: string;
|
|
119
|
+
readonly queryError: null;
|
|
120
|
+
readonly storeError: null;
|
|
121
|
+
readonly probeError: DirectProbeError;
|
|
122
|
+
readonly cleanupErrors: readonly string[];
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export interface DirectSession<
|
|
126
|
+
World extends JsonValue,
|
|
127
|
+
Route extends string,
|
|
128
|
+
Harness,
|
|
129
|
+
> {
|
|
130
|
+
readonly activation: ActiveDirect<World, Route>;
|
|
131
|
+
readonly world: World;
|
|
132
|
+
readonly store: DirectStore<World>;
|
|
133
|
+
readonly clock: LogicalRuntime;
|
|
134
|
+
readonly activity: DirectActivityScope;
|
|
135
|
+
/** The product-owned deterministic ports and controls created for this session. */
|
|
136
|
+
readonly harness: Harness;
|
|
137
|
+
readonly probe: DirectProbe;
|
|
138
|
+
/** Exact, versioned, driver-neutral discovery data for this session. */
|
|
139
|
+
readonly manifest: DirectSessionManifest;
|
|
140
|
+
/** Exact, versioned proof catalog ready for the browser bridge. */
|
|
141
|
+
readonly coverage: DirectSessionManifest["coverage"];
|
|
142
|
+
readonly signal: AbortSignal;
|
|
143
|
+
/** Register additional synchronous teardown until the session is disposed. */
|
|
144
|
+
readonly onDispose: (
|
|
145
|
+
cleanup: DirectSessionCleanup,
|
|
146
|
+
) => Result<true, DirectSessionRegistrationError>;
|
|
147
|
+
/** Abort first, then invoke registered cleanup callbacks in reverse order. */
|
|
148
|
+
readonly dispose: () => undefined;
|
|
149
|
+
readonly isDisposed: () => boolean;
|
|
150
|
+
/** Cleanup failures are diagnostics; disposal still attempts every callback. */
|
|
151
|
+
readonly disposalErrors: () => readonly string[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function frozenMessages(messages: readonly string[]): readonly string[] {
|
|
155
|
+
return Object.freeze([...messages]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
159
|
+
return (
|
|
160
|
+
(typeof value === "object" && value !== null) || typeof value === "function"
|
|
161
|
+
) && typeof Reflect.get(value, "then") === "function";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function freezeCounterSources(
|
|
165
|
+
sources: readonly DirectCounterSource[],
|
|
166
|
+
): readonly DirectCounterSource[] {
|
|
167
|
+
return Object.freeze([...sources]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function prepareSessionObservation(
|
|
171
|
+
input: DirectSessionObservation,
|
|
172
|
+
): DirectSessionObservation {
|
|
173
|
+
const candidate: unknown = input;
|
|
174
|
+
if (!isRecord(candidate)) {
|
|
175
|
+
throw new Error("Direct observation must be an object");
|
|
176
|
+
}
|
|
177
|
+
const pending = input.pending;
|
|
178
|
+
const violations = input.violations;
|
|
179
|
+
const readRemainingWork = input.readRemainingWork;
|
|
180
|
+
if (pending !== undefined && !Array.isArray(pending)) {
|
|
181
|
+
throw new Error("Direct observation pending counters must be an array");
|
|
182
|
+
}
|
|
183
|
+
if (violations !== undefined && !Array.isArray(violations)) {
|
|
184
|
+
throw new Error("Direct observation violation counters must be an array");
|
|
185
|
+
}
|
|
186
|
+
if (readRemainingWork !== undefined && typeof readRemainingWork !== "function") {
|
|
187
|
+
throw new Error("Direct observation remaining-work reader must be a function");
|
|
188
|
+
}
|
|
189
|
+
return Object.freeze({
|
|
190
|
+
...(pending === undefined ? {} : { pending: freezeCounterSources(pending) }),
|
|
191
|
+
...(violations === undefined ? {} : { violations: freezeCounterSources(violations) }),
|
|
192
|
+
...(readRemainingWork === undefined ? {} : { readRemainingWork }),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
type SessionErrorWithoutCleanup = DirectSessionError extends infer Failure
|
|
197
|
+
? Failure extends DirectSessionError
|
|
198
|
+
? Omit<Failure, "cleanupErrors">
|
|
199
|
+
: never
|
|
200
|
+
: never;
|
|
201
|
+
|
|
202
|
+
function sessionError(
|
|
203
|
+
error: SessionErrorWithoutCleanup,
|
|
204
|
+
cleanupErrors: readonly string[] = [],
|
|
205
|
+
): DirectSessionError {
|
|
206
|
+
const failures = frozenMessages(cleanupErrors);
|
|
207
|
+
switch (error.code) {
|
|
208
|
+
case "invalid-options":
|
|
209
|
+
case "activation-failed":
|
|
210
|
+
case "store-failed":
|
|
211
|
+
case "harness-failed":
|
|
212
|
+
case "observation-failed":
|
|
213
|
+
case "probe-failed":
|
|
214
|
+
return Object.freeze({ ...error, cleanupErrors: failures });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function runCleanup(
|
|
219
|
+
controller: AbortController,
|
|
220
|
+
cleanups: readonly DirectSessionCleanup[],
|
|
221
|
+
): readonly string[] {
|
|
222
|
+
const failures: string[] = [];
|
|
223
|
+
try {
|
|
224
|
+
controller.abort();
|
|
225
|
+
} catch (reason) {
|
|
226
|
+
failures.push(`Abort failed: ${renderUnknownReason(reason)}`);
|
|
227
|
+
}
|
|
228
|
+
for (const cleanup of [...cleanups].reverse()) {
|
|
229
|
+
try {
|
|
230
|
+
const returned: unknown = cleanup();
|
|
231
|
+
if (returned !== undefined) {
|
|
232
|
+
if (isPromiseLike(returned)) {
|
|
233
|
+
void Promise.resolve(returned).catch(() => undefined);
|
|
234
|
+
}
|
|
235
|
+
failures.push("Direct cleanup must complete synchronously and return undefined");
|
|
236
|
+
}
|
|
237
|
+
} catch (reason) {
|
|
238
|
+
failures.push(renderUnknownReason(reason, "Direct cleanup failed"));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return frozenMessages(failures);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function activateSession<World extends JsonValue, Route extends string>(
|
|
245
|
+
definition: DirectDefinition<World, Route>,
|
|
246
|
+
activation: DirectSessionActivation,
|
|
247
|
+
): Result<ActiveDirect<World, Route>, QueryError> {
|
|
248
|
+
switch (activation.kind) {
|
|
249
|
+
case "query":
|
|
250
|
+
return definition.activate(activation.source);
|
|
251
|
+
case "scenario":
|
|
252
|
+
return definition.activateScenario(activation.scenario);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Construct one product-owned deterministic composition around a validated
|
|
258
|
+
* definition. Direct owns activation, logical time, activity, observation,
|
|
259
|
+
* cancellation, and teardown; the harness factory owns product semantic ports.
|
|
260
|
+
*/
|
|
261
|
+
export function createDirectSession<
|
|
262
|
+
World extends JsonValue,
|
|
263
|
+
Route extends string,
|
|
264
|
+
Harness,
|
|
265
|
+
>(
|
|
266
|
+
options: DirectSessionOptions<World, Route, Harness>,
|
|
267
|
+
): Result<DirectSession<World, Route, Harness>, DirectSessionError> {
|
|
268
|
+
let definition: DirectDefinition<World, Route>;
|
|
269
|
+
let requestedActivation: DirectSessionActivation;
|
|
270
|
+
let createHarness: DirectSessionOptions<World, Route, Harness>["create"];
|
|
271
|
+
let observeHarness: DirectSessionOptions<World, Route, Harness>["observe"];
|
|
272
|
+
let parseWorld: DirectDefinition<World, Route>["parseWorld"];
|
|
273
|
+
let sleep: LogicalSleep | undefined;
|
|
274
|
+
let storeOptions: DirectStoreOptions<World> | undefined;
|
|
275
|
+
try {
|
|
276
|
+
definition = options.definition;
|
|
277
|
+
parseWorld = definition.parseWorld;
|
|
278
|
+
const activationInput = options.activation;
|
|
279
|
+
if (activationInput.kind === "query") {
|
|
280
|
+
requestedActivation = Object.freeze({ kind: "query", source: activationInput.source });
|
|
281
|
+
} else if (activationInput.kind === "scenario") {
|
|
282
|
+
requestedActivation = Object.freeze({ kind: "scenario", scenario: activationInput.scenario });
|
|
283
|
+
} else {
|
|
284
|
+
throw new Error("Direct session activation kind must be query or scenario");
|
|
285
|
+
}
|
|
286
|
+
createHarness = options.create;
|
|
287
|
+
observeHarness = options.observe;
|
|
288
|
+
sleep = options.sleep;
|
|
289
|
+
const storeOptionsInput = options.storeOptions;
|
|
290
|
+
if (storeOptionsInput === undefined) {
|
|
291
|
+
storeOptions = undefined;
|
|
292
|
+
} else {
|
|
293
|
+
const onListenerError = storeOptionsInput.onListenerError;
|
|
294
|
+
const validateReplacements = storeOptionsInput.validateReplacements;
|
|
295
|
+
storeOptions = Object.freeze({
|
|
296
|
+
...(onListenerError === undefined ? {} : { onListenerError }),
|
|
297
|
+
...(validateReplacements === undefined
|
|
298
|
+
? {}
|
|
299
|
+
: { validateReplacements }),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
} catch (reason) {
|
|
303
|
+
return err(sessionError({
|
|
304
|
+
code: "invalid-options",
|
|
305
|
+
message: renderUnknownReason(reason, "Direct session options could not be inspected"),
|
|
306
|
+
queryError: null,
|
|
307
|
+
storeError: null,
|
|
308
|
+
probeError: null,
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let activationSource: ActiveDirect<World, Route>["source"];
|
|
313
|
+
let activationScenario: ActiveDirect<World, Route>["scenario"];
|
|
314
|
+
let activationRoute: Route;
|
|
315
|
+
let activationWorld: World;
|
|
316
|
+
let activationRuntime: LogicalRuntimeSnapshot;
|
|
317
|
+
let activationHash: TaggedStableHash;
|
|
318
|
+
try {
|
|
319
|
+
const activated = activateSession(definition, requestedActivation);
|
|
320
|
+
if (!activated.ok) {
|
|
321
|
+
const queryError: QueryError = Object.freeze({
|
|
322
|
+
code: activated.error.code,
|
|
323
|
+
message: activated.error.message,
|
|
324
|
+
});
|
|
325
|
+
return err(sessionError({
|
|
326
|
+
code: "activation-failed",
|
|
327
|
+
message: queryError.message,
|
|
328
|
+
queryError,
|
|
329
|
+
storeError: null,
|
|
330
|
+
probeError: null,
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
const candidate = activated.value;
|
|
334
|
+
if (candidate.kind !== "active") throw new Error("Direct activation kind must be active");
|
|
335
|
+
if (candidate.source !== "scenario" && candidate.source !== "fixture") {
|
|
336
|
+
throw new Error("Direct activation source must be scenario or fixture");
|
|
337
|
+
}
|
|
338
|
+
if (typeof candidate.scenario !== "string") {
|
|
339
|
+
throw new Error("Direct activation scenario must be a string");
|
|
340
|
+
}
|
|
341
|
+
if (typeof candidate.route !== "string") {
|
|
342
|
+
throw new Error("Direct activation route must be a string");
|
|
343
|
+
}
|
|
344
|
+
const parsedActivationHash = parseTaggedStableHash(candidate.activationHash);
|
|
345
|
+
if (!parsedActivationHash.ok) throw new Error(parsedActivationHash.error.message);
|
|
346
|
+
const parsedRuntime = parseLogicalRuntimeSnapshot(candidate.runtime);
|
|
347
|
+
if (!parsedRuntime.ok) throw new Error(parsedRuntime.error.message);
|
|
348
|
+
activationSource = candidate.source;
|
|
349
|
+
activationScenario = candidate.scenario;
|
|
350
|
+
activationRoute = candidate.route;
|
|
351
|
+
activationWorld = candidate.world;
|
|
352
|
+
activationRuntime = parsedRuntime.value;
|
|
353
|
+
activationHash = parsedActivationHash.value;
|
|
354
|
+
} catch (reason) {
|
|
355
|
+
return err(sessionError({
|
|
356
|
+
code: "invalid-options",
|
|
357
|
+
message: renderUnknownReason(reason, "Direct session activation failed unexpectedly"),
|
|
358
|
+
queryError: null,
|
|
359
|
+
storeError: null,
|
|
360
|
+
probeError: null,
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const store = storeOptions === undefined
|
|
365
|
+
? createDirectStore(activationWorld, parseWorld)
|
|
366
|
+
: createDirectStore(activationWorld, parseWorld, storeOptions);
|
|
367
|
+
if (!store.ok) {
|
|
368
|
+
return err(sessionError({
|
|
369
|
+
code: "store-failed",
|
|
370
|
+
message: store.error.message,
|
|
371
|
+
queryError: null,
|
|
372
|
+
storeError: store.error,
|
|
373
|
+
probeError: null,
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const clock = sleep === undefined
|
|
378
|
+
? createLogicalRuntime(activationRuntime)
|
|
379
|
+
: createLogicalRuntime(activationRuntime, sleep);
|
|
380
|
+
const activation: ActiveDirect<World, Route> = Object.freeze({
|
|
381
|
+
kind: "active",
|
|
382
|
+
source: activationSource,
|
|
383
|
+
scenario: activationScenario,
|
|
384
|
+
route: activationRoute,
|
|
385
|
+
world: store.value.getSnapshot().world,
|
|
386
|
+
runtime: clock.snapshot(),
|
|
387
|
+
activationHash,
|
|
388
|
+
});
|
|
389
|
+
const createdManifest = createDirectSessionManifest(definition, activation);
|
|
390
|
+
if (!createdManifest.ok) {
|
|
391
|
+
return err(sessionError({
|
|
392
|
+
code: "invalid-options",
|
|
393
|
+
message: createdManifest.error.message,
|
|
394
|
+
queryError: null,
|
|
395
|
+
storeError: null,
|
|
396
|
+
probeError: null,
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
const manifest = createdManifest.value;
|
|
400
|
+
const controller = new AbortController();
|
|
401
|
+
const activity = createDirectActivityScope(store.value, clock, { signal: controller.signal });
|
|
402
|
+
const cleanups: DirectSessionCleanup[] = [];
|
|
403
|
+
let registrationOpen = true;
|
|
404
|
+
const context: DirectSessionContext<World, Route> = Object.freeze({
|
|
405
|
+
activation,
|
|
406
|
+
world: activation.world,
|
|
407
|
+
store: store.value,
|
|
408
|
+
clock,
|
|
409
|
+
activity,
|
|
410
|
+
signal: controller.signal,
|
|
411
|
+
onDispose: (cleanup: DirectSessionCleanup): undefined => {
|
|
412
|
+
if (!registrationOpen) {
|
|
413
|
+
throw new Error("Direct cleanup must be registered during synchronous session construction");
|
|
414
|
+
}
|
|
415
|
+
cleanups.push(cleanup);
|
|
416
|
+
return undefined;
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
let harness: Harness;
|
|
421
|
+
try {
|
|
422
|
+
harness = createHarness(context);
|
|
423
|
+
if (isPromiseLike(harness)) {
|
|
424
|
+
void Promise.resolve(harness).catch(() => undefined);
|
|
425
|
+
throw new Error("Direct harness construction must complete synchronously");
|
|
426
|
+
}
|
|
427
|
+
} catch (reason) {
|
|
428
|
+
registrationOpen = false;
|
|
429
|
+
const cleanupErrors = runCleanup(controller, cleanups);
|
|
430
|
+
return err(sessionError({
|
|
431
|
+
code: "harness-failed",
|
|
432
|
+
message: renderUnknownReason(reason, "Direct harness construction failed"),
|
|
433
|
+
queryError: null,
|
|
434
|
+
storeError: null,
|
|
435
|
+
probeError: null,
|
|
436
|
+
}, cleanupErrors));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let observation: DirectSessionObservation;
|
|
440
|
+
try {
|
|
441
|
+
const observed = observeHarness === undefined
|
|
442
|
+
? Object.freeze({})
|
|
443
|
+
: observeHarness(harness, context);
|
|
444
|
+
if (isPromiseLike(observed)) {
|
|
445
|
+
void Promise.resolve(observed).catch(() => undefined);
|
|
446
|
+
throw new Error("Direct observation construction must complete synchronously");
|
|
447
|
+
}
|
|
448
|
+
observation = prepareSessionObservation(observed);
|
|
449
|
+
} catch (reason) {
|
|
450
|
+
registrationOpen = false;
|
|
451
|
+
const cleanupErrors = runCleanup(controller, cleanups);
|
|
452
|
+
return err(sessionError({
|
|
453
|
+
code: "observation-failed",
|
|
454
|
+
message: renderUnknownReason(reason, "Direct observation construction failed"),
|
|
455
|
+
queryError: null,
|
|
456
|
+
storeError: null,
|
|
457
|
+
probeError: null,
|
|
458
|
+
}, cleanupErrors));
|
|
459
|
+
}
|
|
460
|
+
registrationOpen = false;
|
|
461
|
+
|
|
462
|
+
const probe = createDirectProbe({
|
|
463
|
+
store: store.value,
|
|
464
|
+
activationHash: activation.activationHash,
|
|
465
|
+
...(observation.pending === undefined ? {} : { pending: observation.pending }),
|
|
466
|
+
...(observation.violations === undefined ? {} : { violations: observation.violations }),
|
|
467
|
+
...(observation.readRemainingWork === undefined
|
|
468
|
+
? {}
|
|
469
|
+
: { readRemainingWork: observation.readRemainingWork }),
|
|
470
|
+
});
|
|
471
|
+
if (!probe.ok) {
|
|
472
|
+
const cleanupErrors = runCleanup(controller, cleanups);
|
|
473
|
+
return err(sessionError({
|
|
474
|
+
code: "probe-failed",
|
|
475
|
+
message: probe.error.message,
|
|
476
|
+
queryError: null,
|
|
477
|
+
storeError: null,
|
|
478
|
+
probeError: probe.error,
|
|
479
|
+
}, cleanupErrors));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let disposed = false;
|
|
483
|
+
let disposalErrors: readonly string[] = Object.freeze([]);
|
|
484
|
+
const onDispose = (
|
|
485
|
+
cleanup: DirectSessionCleanup,
|
|
486
|
+
): Result<true, DirectSessionRegistrationError> => {
|
|
487
|
+
if (typeof cleanup !== "function") {
|
|
488
|
+
return err(Object.freeze({
|
|
489
|
+
code: "invalid-cleanup",
|
|
490
|
+
message: "Direct cleanup must be a function",
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
if (disposed) {
|
|
494
|
+
return err(Object.freeze({
|
|
495
|
+
code: "session-disposed",
|
|
496
|
+
message: "Cannot register cleanup on a disposed Direct session",
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
cleanups.push(cleanup);
|
|
500
|
+
return ok<true>(true);
|
|
501
|
+
};
|
|
502
|
+
const dispose = (): undefined => {
|
|
503
|
+
if (disposed) return undefined;
|
|
504
|
+
disposed = true;
|
|
505
|
+
disposalErrors = runCleanup(controller, cleanups);
|
|
506
|
+
return undefined;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
return ok(Object.freeze({
|
|
510
|
+
activation,
|
|
511
|
+
world: activation.world,
|
|
512
|
+
store: store.value,
|
|
513
|
+
clock,
|
|
514
|
+
activity,
|
|
515
|
+
harness,
|
|
516
|
+
probe: probe.value,
|
|
517
|
+
manifest,
|
|
518
|
+
coverage: manifest.coverage,
|
|
519
|
+
signal: controller.signal,
|
|
520
|
+
onDispose,
|
|
521
|
+
dispose,
|
|
522
|
+
isDisposed: () => disposed,
|
|
523
|
+
disposalErrors: () => disposalErrors,
|
|
524
|
+
}));
|
|
525
|
+
}
|