reactor-effect-client 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -6
- package/dist/FetchHttp.js +1 -1
- package/dist/FetchHttp.js.map +1 -1
- package/dist/PeerFactory.d.ts +13 -2
- package/dist/PeerFactory.d.ts.map +1 -1
- package/dist/PeerFactory.js.map +1 -1
- package/dist/PeerTypes.d.ts +2 -2
- package/dist/PeerTypes.d.ts.map +1 -1
- package/dist/Sequence.d.ts +18 -14
- package/dist/Sequence.d.ts.map +1 -1
- package/dist/Sequence.js +117 -122
- package/dist/Sequence.js.map +1 -1
- package/dist/SessionTypes.d.ts +172 -15
- package/dist/SessionTypes.d.ts.map +1 -1
- package/dist/SessionTypes.js +18 -1
- package/dist/SessionTypes.js.map +1 -1
- package/dist/bytes.js +3 -3
- package/dist/bytes.js.map +1 -1
- package/dist/contract.d.ts +120 -29
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +105 -73
- package/dist/contract.js.map +1 -1
- package/dist/coordinator/_internal/client.d.ts +111 -17
- package/dist/coordinator/_internal/client.d.ts.map +1 -1
- package/dist/coordinator/_internal/client.js +187 -74
- package/dist/coordinator/_internal/client.js.map +1 -1
- package/dist/coordinator/_internal/recording.d.ts +33 -0
- package/dist/coordinator/_internal/recording.d.ts.map +1 -0
- package/dist/coordinator/_internal/recording.js +123 -0
- package/dist/coordinator/_internal/recording.js.map +1 -0
- package/dist/coordinator/_internal/response.d.ts.map +1 -1
- package/dist/coordinator/_internal/response.js +5 -7
- package/dist/coordinator/_internal/response.js.map +1 -1
- package/dist/coordinator/_internal/schemas.d.ts +40 -6
- package/dist/coordinator/_internal/schemas.d.ts.map +1 -1
- package/dist/coordinator/_internal/schemas.js +94 -64
- package/dist/coordinator/_internal/schemas.js.map +1 -1
- package/dist/coordinator/index.d.ts +14 -2
- package/dist/coordinator/index.d.ts.map +1 -1
- package/dist/coordinator/index.js +14 -18
- package/dist/coordinator/index.js.map +1 -1
- package/dist/correlation.d.ts +15 -0
- package/dist/correlation.d.ts.map +1 -1
- package/dist/correlation.js +26 -8
- package/dist/correlation.js.map +1 -1
- package/dist/duration.d.ts +25 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +47 -0
- package/dist/duration.js.map +1 -0
- package/dist/errors.d.ts +1264 -49
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +698 -46
- package/dist/errors.js.map +1 -1
- package/dist/h3/_internal/client.d.ts +2 -2
- package/dist/h3/_internal/client.d.ts.map +1 -1
- package/dist/h3/_internal/client.js +280 -146
- package/dist/h3/_internal/client.js.map +1 -1
- package/dist/h3/_internal/contracts.d.ts.map +1 -1
- package/dist/h3/_internal/contracts.js +2 -2
- package/dist/h3/_internal/contracts.js.map +1 -1
- package/dist/h3/_internal/deployment.d.ts.map +1 -1
- package/dist/h3/_internal/deployment.js +5 -4
- package/dist/h3/_internal/deployment.js.map +1 -1
- package/dist/h3/_internal/evidence.js +1 -1
- package/dist/h3/_internal/evidence.js.map +1 -1
- package/dist/h3/_internal/operations.d.ts +58 -0
- package/dist/h3/_internal/operations.d.ts.map +1 -0
- package/dist/h3/_internal/operations.js +268 -0
- package/dist/h3/_internal/operations.js.map +1 -0
- package/dist/h3/_internal/references.d.ts.map +1 -1
- package/dist/h3/_internal/references.js +3 -8
- package/dist/h3/_internal/references.js.map +1 -1
- package/dist/h3/_internal/request.js +1 -1
- package/dist/h3/_internal/request.js.map +1 -1
- package/dist/h3/_internal/retained.d.ts +22 -0
- package/dist/h3/_internal/retained.d.ts.map +1 -0
- package/dist/h3/_internal/retained.js +89 -0
- package/dist/h3/_internal/retained.js.map +1 -0
- package/dist/h3/_internal/state.d.ts +15 -1
- package/dist/h3/_internal/state.d.ts.map +1 -1
- package/dist/h3/_internal/state.js +69 -29
- package/dist/h3/_internal/state.js.map +1 -1
- package/dist/h3/index.d.ts +1 -1
- package/dist/h3/index.d.ts.map +1 -1
- package/dist/h3/messages.d.ts +118 -109
- package/dist/h3/messages.d.ts.map +1 -1
- package/dist/h3/messages.js +73 -51
- package/dist/h3/messages.js.map +1 -1
- package/dist/h3/types.d.ts +109 -13
- package/dist/h3/types.d.ts.map +1 -1
- package/dist/host.d.ts +6 -6
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +4 -5
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +13 -6
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +34 -14
- package/dist/json.js.map +1 -1
- package/dist/media-stream.d.ts.map +1 -1
- package/dist/media-stream.js +9 -11
- package/dist/media-stream.js.map +1 -1
- package/dist/observation.d.ts +9 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +20 -18
- package/dist/observation.js.map +1 -1
- package/dist/orchestration/h3-source.d.ts +32 -4
- package/dist/orchestration/h3-source.d.ts.map +1 -1
- package/dist/orchestration/h3-source.js +47 -14
- package/dist/orchestration/h3-source.js.map +1 -1
- package/dist/orchestration/index.d.ts +9 -6
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +5 -3
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/media-buffer.d.ts +12 -4
- package/dist/orchestration/media-buffer.d.ts.map +1 -1
- package/dist/orchestration/media-buffer.js +31 -11
- package/dist/orchestration/media-buffer.js.map +1 -1
- package/dist/orchestration/open-h3.d.ts +65 -0
- package/dist/orchestration/open-h3.d.ts.map +1 -0
- package/dist/orchestration/open-h3.js +65 -0
- package/dist/orchestration/open-h3.js.map +1 -0
- package/dist/orchestration/references.d.ts +5 -1
- package/dist/orchestration/references.d.ts.map +1 -1
- package/dist/orchestration/references.js +37 -34
- package/dist/orchestration/references.js.map +1 -1
- package/dist/orchestration/renewal-state.js +1 -1
- package/dist/orchestration/renewal-state.js.map +1 -1
- package/dist/orchestration/renewal.d.ts +64 -64
- package/dist/orchestration/renewal.d.ts.map +1 -1
- package/dist/orchestration/renewal.js +149 -88
- package/dist/orchestration/renewal.js.map +1 -1
- package/dist/orchestration/request.d.ts +11 -12
- package/dist/orchestration/request.d.ts.map +1 -1
- package/dist/orchestration/request.js +63 -66
- package/dist/orchestration/request.js.map +1 -1
- package/dist/orchestration/routing.d.ts.map +1 -1
- package/dist/orchestration/routing.js +17 -14
- package/dist/orchestration/routing.js.map +1 -1
- package/dist/orchestration/source-slot.d.ts +15 -5
- package/dist/orchestration/source-slot.d.ts.map +1 -1
- package/dist/orchestration/source-slot.js +40 -21
- package/dist/orchestration/source-slot.js.map +1 -1
- package/dist/orchestration/types.d.ts +594 -39
- package/dist/orchestration/types.d.ts.map +1 -1
- package/dist/orchestration/types.js +20 -0
- package/dist/orchestration/types.js.map +1 -1
- package/dist/protobuf.js +2 -2
- package/dist/protobuf.js.map +1 -1
- package/dist/session/_internal/acquire.d.ts +4 -6
- package/dist/session/_internal/acquire.d.ts.map +1 -1
- package/dist/session/_internal/acquire.js +40 -50
- package/dist/session/_internal/acquire.js.map +1 -1
- package/dist/session/_internal/cleanup.d.ts +2 -1
- package/dist/session/_internal/cleanup.d.ts.map +1 -1
- package/dist/session/_internal/cleanup.js +23 -15
- package/dist/session/_internal/cleanup.js.map +1 -1
- package/dist/session/_internal/lifecycle.d.ts +5 -0
- package/dist/session/_internal/lifecycle.d.ts.map +1 -1
- package/dist/session/_internal/lifecycle.js +16 -6
- package/dist/session/_internal/lifecycle.js.map +1 -1
- package/dist/session/_internal/remote.d.ts.map +1 -1
- package/dist/session/_internal/remote.js +16 -7
- package/dist/session/_internal/remote.js.map +1 -1
- package/dist/session/_internal/uploads.d.ts +4 -1
- package/dist/session/_internal/uploads.d.ts.map +1 -1
- package/dist/session/_internal/uploads.js +25 -27
- package/dist/session/_internal/uploads.js.map +1 -1
- package/dist/session/commands.d.ts +1 -21
- package/dist/session/commands.d.ts.map +1 -1
- package/dist/session/commands.js +1 -12
- package/dist/session/commands.js.map +1 -1
- package/dist/session/files.d.ts +10 -2
- package/dist/session/files.d.ts.map +1 -1
- package/dist/session/files.js +11 -16
- package/dist/session/files.js.map +1 -1
- package/dist/session/index.d.ts +6 -6
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js.map +1 -1
- package/dist/session/media.d.ts +53 -6
- package/dist/session/media.d.ts.map +1 -1
- package/dist/session/recorder.d.ts +24 -0
- package/dist/session/recorder.d.ts.map +1 -0
- package/dist/session/recorder.js +19 -0
- package/dist/session/recorder.js.map +1 -0
- package/dist/session.d.ts +5 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +213 -110
- package/dist/session.js.map +1 -1
- package/dist/simulation/_internal/source.d.ts.map +1 -1
- package/dist/simulation/_internal/source.js +57 -48
- package/dist/simulation/_internal/source.js.map +1 -1
- package/dist/simulation/index.d.ts +4 -5
- package/dist/simulation/index.d.ts.map +1 -1
- package/dist/simulation/index.js +3 -4
- package/dist/simulation/index.js.map +1 -1
- package/dist/simulation/types.d.ts +16 -2
- package/dist/simulation/types.d.ts.map +1 -1
- package/dist/stats.d.ts +19 -0
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +79 -8
- package/dist/stats.js.map +1 -1
- package/package.json +5 -4
- package/notices/owned-webrtc/Effect-MIT.txt +0 -21
- package/notices/owned-webrtc/IETF.txt +0 -43
- package/notices/owned-webrtc/LICENSE +0 -21
- package/notices/owned-webrtc/UPSTREAM.md +0 -20
- package/notices/owned-webrtc/VP8-ADAPTATION.md +0 -32
- package/notices/owned-webrtc/VP8-AUTHORS.txt +0 -42
- package/notices/owned-webrtc/VP8-LICENSE.txt +0 -25
- package/notices/owned-webrtc/VP8-PATENTS.txt +0 -21
- package/notices/owned-webrtc/Werift-MIT.txt +0 -21
- package/notices/owned-webrtc/libvpx-v1.12.0-LICENSE.txt +0 -28
- package/notices/owned-webrtc/libvpx-v1.12.0-PATENTS.txt +0 -22
- package/notices/owned-webrtc/rfc3711.txt +0 -3139
- package/notices/owned-webrtc/str0m-MIT.txt +0 -7
|
@@ -6,87 +6,82 @@ import * as Result from "effect/Result";
|
|
|
6
6
|
import * as Scope from "effect/Scope";
|
|
7
7
|
import * as Semaphore from "effect/Semaphore";
|
|
8
8
|
import * as Stream from "effect/Stream";
|
|
9
|
-
import {
|
|
9
|
+
import { CommandFailure, parsed, PolicyFailure, positiveLimit, ReactorError, Remote, } from "../../errors.js";
|
|
10
|
+
import { duration } from "../../duration.js";
|
|
10
11
|
import { Observations } from "../../observation.js";
|
|
11
|
-
import { CommandFailure } from "../../session/commands.js";
|
|
12
12
|
import * as Submission from "../../Submission.js";
|
|
13
13
|
import { decodeMessage } from "../messages.js";
|
|
14
|
+
import { Operations } from "./operations.js";
|
|
14
15
|
import { canvases } from "../profile.js";
|
|
15
16
|
import { Commands } from "./contracts.js";
|
|
16
17
|
import { validateDeployment } from "./deployment.js";
|
|
17
18
|
import { acceptanceFor, encodeMetadata, submissionFromMetadata } from "./evidence.js";
|
|
18
19
|
import { checkedUpload, referenceMaterial } from "./references.js";
|
|
19
20
|
import { captureRequest, clipId, enqueueArguments, nonnegative, seconds } from "./request.js";
|
|
21
|
+
import { sizeOf } from "./retained.js";
|
|
20
22
|
import { ProviderState } from "./state.js";
|
|
21
|
-
const pure = (evaluate) =>
|
|
22
|
-
const localFailure = (operation, cause) =>
|
|
23
|
-
|
|
23
|
+
const pure = (evaluate) => parsed(evaluate);
|
|
24
|
+
const localFailure = (operation, cause) => CommandFailure.from(cause, { ...cause.context, operation, outcome: "not-submitted" });
|
|
25
|
+
/** A caller's local refusal already proves no dispatch; anything else becomes one. */
|
|
26
|
+
const preparationFailure = (operation, cause) => ReactorError.is(cause) || CommandFailure.is(cause) ? localFailure(operation, cause) : cause;
|
|
27
|
+
const uncertain = (operation, source, message, cause) => CommandFailure.from(cause ?? ReactorError.fromCode("UnexpectedReply", message), {
|
|
24
28
|
...(cause?.context ?? {}),
|
|
25
29
|
operation,
|
|
26
30
|
outcome: "unknown",
|
|
27
31
|
requestId: source.requestId,
|
|
28
32
|
generation: source.generation,
|
|
29
33
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
/** The refusal reason is provider free text with no stable codes: kept for diagnosis only. */
|
|
35
|
+
const rejected = (operation, source, reason) => new CommandFailure({
|
|
36
|
+
reason: new Remote({ _tag: "Remote", message: `H3 ${operation} was refused`, body: reason }),
|
|
37
|
+
context: {
|
|
38
|
+
operation,
|
|
39
|
+
outcome: "replied",
|
|
40
|
+
requestId: source.requestId,
|
|
41
|
+
generation: source.generation,
|
|
42
|
+
},
|
|
35
43
|
});
|
|
36
44
|
const hex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (ArrayBuffer.isView(value))
|
|
52
|
-
bytes += value.byteLength;
|
|
53
|
-
else if (value instanceof ArrayBuffer)
|
|
54
|
-
bytes += value.byteLength;
|
|
55
|
-
else if (value instanceof Map) {
|
|
56
|
-
bytes += value.size * 32;
|
|
57
|
-
for (const [key, child] of value) {
|
|
58
|
-
pending.push(key);
|
|
59
|
-
pending.push(child);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
else if (value instanceof Set) {
|
|
63
|
-
bytes += value.size * 16;
|
|
64
|
-
for (const child of value)
|
|
65
|
-
pending.push(child);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
bytes += 32;
|
|
69
|
-
for (const child of Object.values(value))
|
|
70
|
-
pending.push(child);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
else
|
|
74
|
-
bytes += 8;
|
|
45
|
+
/**
|
|
46
|
+
* Whether a snapshot at `revision` already reflects `event`: the reducer
|
|
47
|
+
* handles session events in sequence order. An acceptance is local evidence
|
|
48
|
+
* that no snapshot holds, and a diagnostic without a source has no sequence,
|
|
49
|
+
* so neither is ever covered.
|
|
50
|
+
*/
|
|
51
|
+
const covered = (event, revision) => {
|
|
52
|
+
switch (event._tag) {
|
|
53
|
+
case "Acceptance":
|
|
54
|
+
return false;
|
|
55
|
+
case "Diagnostic":
|
|
56
|
+
return event.source !== undefined && event.source.sequence <= revision;
|
|
57
|
+
default:
|
|
58
|
+
return event.source.sequence <= revision;
|
|
75
59
|
}
|
|
76
|
-
return bytes;
|
|
77
60
|
};
|
|
78
61
|
const build = (session, options) => Effect.gen(function* () {
|
|
79
62
|
const scope = yield* Effect.scope;
|
|
80
63
|
const crypto = yield* Crypto.Crypto;
|
|
81
64
|
const limits = yield* pure(() => ({
|
|
82
|
-
command:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
65
|
+
command: duration(options.replyTimeout ?? "15 seconds", "H3 reply timeout", {
|
|
66
|
+
maximum: "10 minutes",
|
|
67
|
+
}),
|
|
68
|
+
upload: duration(options.uploadTimeout ?? "60 seconds", "H3 upload timeout", {
|
|
69
|
+
maximum: "10 minutes",
|
|
70
|
+
}),
|
|
71
|
+
setup: duration(options.setupTimeout ?? "60 seconds", "H3 setup timeout", {
|
|
72
|
+
maximum: "10 minutes",
|
|
73
|
+
}),
|
|
74
|
+
reconcile: duration(options.reconcileWindow ?? "5 seconds", "H3 reconciliation window", {
|
|
75
|
+
maximum: "1 minute",
|
|
76
|
+
}),
|
|
77
|
+
hook: duration(options.resultHookTimeout ?? "1 second", "H3 result hook timeout", {
|
|
78
|
+
maximum: "1 minute",
|
|
79
|
+
}),
|
|
86
80
|
pending: positiveLimit(options.maxPending ?? 128, "H3 pending acceptance bound", 4096),
|
|
87
81
|
clips: positiveLimit(options.maxTrackedClips ?? 4096, "H3 clip observation bound", 16384),
|
|
88
82
|
acceptances: positiveLimit(options.maxAcceptances ?? 1024, "H3 local acceptance bound", 16384),
|
|
89
83
|
cache: positiveLimit(options.maxCachedUploads ?? 256, "H3 upload cache bound", 4096),
|
|
84
|
+
operations: positiveLimit(options.maxOperations ?? 1024, "H3 clip operation bound", 16384),
|
|
90
85
|
prompt: positiveLimit(options.maxPromptBytes ?? 1048576, "H3 prompt byte bound", 4194304),
|
|
91
86
|
retained: positiveLimit(options.maxRetainedBytes ?? 16777216, "H3 retained byte bound", 67108864),
|
|
92
87
|
}));
|
|
@@ -95,7 +90,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
95
90
|
yield* session.ready;
|
|
96
91
|
const observation = yield* session.observe(options.observation);
|
|
97
92
|
const state = new ProviderState(session.id, observation.initial.generation, observation.revision, limits.clips);
|
|
98
|
-
const namespace = hex(yield* crypto.randomBytes(16).pipe(Effect.mapError(() =>
|
|
93
|
+
const namespace = hex(yield* crypto.randomBytes(16).pipe(Effect.mapError(() => ReactorError.fromCode("InvalidState", "Could not allocate H3 acceptance namespace", {
|
|
99
94
|
outcome: "not-submitted",
|
|
100
95
|
}))));
|
|
101
96
|
let counter = 0n, closed = false, fatalError;
|
|
@@ -103,26 +98,83 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
103
98
|
const events = new Observations();
|
|
104
99
|
const pending = new Map();
|
|
105
100
|
const acceptances = new Map();
|
|
101
|
+
/** Acceptances count toward the reducer's retained bytes while the map holds them. */
|
|
102
|
+
const retain = (acceptance, direction) => {
|
|
103
|
+
state.retained.add(direction * (64 + acceptance.submissionId.length * 3));
|
|
104
|
+
if (direction === 1) {
|
|
105
|
+
state.retained.hold(acceptance.clip);
|
|
106
|
+
state.retained.hold(acceptance.evidence.source);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
state.retained.drop(acceptance.clip);
|
|
110
|
+
state.retained.drop(acceptance.evidence.source);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const operations = new Operations(limits.operations);
|
|
106
114
|
const decoded = new WeakMap();
|
|
107
115
|
const observedWaiters = new Map();
|
|
108
116
|
const uploads = new Map();
|
|
109
117
|
const uploadGate = yield* Semaphore.make(1);
|
|
118
|
+
// Callers waiting for the provider to synchronize; every applied event and
|
|
119
|
+
// any failure wakes them to look again. A woken caller may wait again at
|
|
120
|
+
// once, so the set is emptied before any waiter is released.
|
|
121
|
+
const synchronizing = new Set();
|
|
122
|
+
const wake = () => {
|
|
123
|
+
const waiters = [...synchronizing];
|
|
124
|
+
synchronizing.clear();
|
|
125
|
+
for (const waiter of waiters)
|
|
126
|
+
Deferred.doneUnsafe(waiter, Effect.void);
|
|
127
|
+
};
|
|
110
128
|
const emit = (event) => events.emit(event, sizeOf(event));
|
|
129
|
+
/** Decide a submission's acceptance: record it, resolve its waiters and announce it. */
|
|
130
|
+
const record = (entry, acceptance) => {
|
|
131
|
+
entry.held = undefined;
|
|
132
|
+
if (acceptances.size >= limits.acceptances) {
|
|
133
|
+
const first = acceptances.entries().next();
|
|
134
|
+
if (!first.done) {
|
|
135
|
+
acceptances.delete(first.value[0]);
|
|
136
|
+
retain(first.value[1], -1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
acceptances.set(entry.id, acceptance);
|
|
140
|
+
retain(acceptance, 1);
|
|
141
|
+
operations.accept(acceptance);
|
|
142
|
+
Deferred.doneUnsafe(entry.deferred, Effect.succeed(acceptance));
|
|
143
|
+
emit({ _tag: "Acceptance", acceptance });
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Held evidence decides the acceptance once the reply cannot: it came
|
|
147
|
+
* without correlating the clip, or the wait for it ended short of a
|
|
148
|
+
* definite failure, which discards the evidence instead.
|
|
149
|
+
*/
|
|
150
|
+
const decideHeld = (entry) => {
|
|
151
|
+
if (entry.held !== undefined)
|
|
152
|
+
record(entry, entry.held);
|
|
153
|
+
};
|
|
111
154
|
const fail = (error) => {
|
|
112
155
|
if (fatalError !== undefined)
|
|
113
156
|
return;
|
|
114
157
|
fatalError = error;
|
|
115
158
|
state.unavailable(error);
|
|
159
|
+
// Recording held evidence resumes its waiters at once: by now the provider
|
|
160
|
+
// refuses their new work, and they can read the acceptance before anyone
|
|
161
|
+
// learns of the failure.
|
|
162
|
+
for (const entry of pending.values())
|
|
163
|
+
decideHeld(entry);
|
|
116
164
|
Deferred.doneUnsafe(fatal, Effect.succeed(error));
|
|
117
165
|
for (const entry of pending.values())
|
|
118
166
|
Deferred.doneUnsafe(entry.deferred, Effect.fail(error));
|
|
119
167
|
for (const waiter of observedWaiters.values())
|
|
120
168
|
Deferred.doneUnsafe(waiter, Effect.fail(error));
|
|
121
169
|
emit({ _tag: "Diagnostic", error });
|
|
170
|
+
wake();
|
|
122
171
|
};
|
|
123
172
|
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
124
173
|
closed = true;
|
|
125
|
-
fail(
|
|
174
|
+
fail(ReactorError.fromCode("Closed", "H3 provider scope closed", {
|
|
175
|
+
operation: "H3 observation",
|
|
176
|
+
}));
|
|
177
|
+
operations.retire();
|
|
126
178
|
events.end();
|
|
127
179
|
pending.clear();
|
|
128
180
|
observedWaiters.clear();
|
|
@@ -133,41 +185,47 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
133
185
|
if (id === undefined)
|
|
134
186
|
return;
|
|
135
187
|
const entry = pending.get(id);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
188
|
+
const acceptance = entry === undefined ? undefined : acceptanceFor(entry, clip, source);
|
|
189
|
+
if (entry === undefined || acceptance === undefined) {
|
|
190
|
+
// After the reconcile window, or in a later transport generation, the
|
|
191
|
+
// evidence can still resolve the operation, never the acceptances.
|
|
192
|
+
operations.lateEvidence(id, clip, source);
|
|
140
193
|
return;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
194
|
+
}
|
|
195
|
+
const previous = acceptances.get(entry.id) ?? entry.held;
|
|
196
|
+
if (previous !== undefined && previous.clip.clip_id !== clip.clip_id) {
|
|
197
|
+
fail(ReactorError.fromCode("Protocol", "H3 acceptance identity named two clips", {
|
|
198
|
+
operation: "enqueue",
|
|
199
|
+
}));
|
|
147
200
|
return;
|
|
148
201
|
}
|
|
149
|
-
if (acceptances.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
202
|
+
if (acceptances.has(entry.id))
|
|
203
|
+
return;
|
|
204
|
+
// Until the reply is observed, it may still correlate the acceptance; the
|
|
205
|
+
// clip's facts are recorded meanwhile.
|
|
206
|
+
if (acceptance.evidence.kind === "metadata" && entry.awaiting) {
|
|
207
|
+
if (entry.held === undefined) {
|
|
208
|
+
entry.held = acceptance;
|
|
209
|
+
operations.identify(acceptance);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
153
212
|
}
|
|
154
|
-
|
|
155
|
-
Deferred.doneUnsafe(entry.deferred, Effect.succeed(acceptance));
|
|
156
|
-
emit({ _tag: "Acceptance", acceptance });
|
|
213
|
+
record(entry, acceptance);
|
|
157
214
|
};
|
|
158
215
|
const reduce = (source) => {
|
|
159
216
|
if (closed || fatalError !== undefined)
|
|
160
217
|
return;
|
|
161
218
|
try {
|
|
162
|
-
const before = state.
|
|
219
|
+
const before = state.transportGeneration;
|
|
163
220
|
let disposition = state.admit(source);
|
|
164
|
-
if (state.
|
|
221
|
+
if (state.transportGeneration !== before) {
|
|
165
222
|
uploads.clear();
|
|
166
223
|
for (const entry of pending.values())
|
|
167
|
-
if (entry.generation !== source.generation)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
})));
|
|
224
|
+
if (entry.generation !== source.generation) {
|
|
225
|
+
// No reply to an earlier generation's enqueue can correlate now.
|
|
226
|
+
decideHeld(entry);
|
|
227
|
+
Deferred.doneUnsafe(entry.deferred, Effect.fail(ReactorError.fromCode("Disconnected", "H3 acceptance crossed a transport generation", { operation: "enqueue" })));
|
|
228
|
+
}
|
|
171
229
|
}
|
|
172
230
|
if (source._tag !== "Model") {
|
|
173
231
|
emit({ _tag: "Session", source });
|
|
@@ -177,7 +235,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
177
235
|
if (source.kind === "ack") {
|
|
178
236
|
result =
|
|
179
237
|
disposition === "stale"
|
|
180
|
-
? Result.fail(
|
|
238
|
+
? Result.fail(ReactorError.fromCode("Disconnected", "H3 received a stale command acknowledgement"))
|
|
181
239
|
: Result.succeed(undefined);
|
|
182
240
|
emit({ _tag: "Acknowledged", source });
|
|
183
241
|
}
|
|
@@ -187,7 +245,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
187
245
|
disposition = state.apply(message, source);
|
|
188
246
|
result =
|
|
189
247
|
disposition === "stale"
|
|
190
|
-
? Result.fail(
|
|
248
|
+
? Result.fail(ReactorError.fromCode("Disconnected", "H3 received a stale command message"))
|
|
191
249
|
: Result.succeed(message);
|
|
192
250
|
if (disposition !== "stale" && message.type !== "unknown") {
|
|
193
251
|
if (message.type === "queue_update") {
|
|
@@ -200,6 +258,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
200
258
|
}
|
|
201
259
|
else if ("clip" in message.data)
|
|
202
260
|
accept(message.data.clip, source);
|
|
261
|
+
operations.observe(message, source);
|
|
203
262
|
}
|
|
204
263
|
emit({ _tag: "Message", message, source, disposition });
|
|
205
264
|
}
|
|
@@ -207,14 +266,17 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
207
266
|
const waiter = observedWaiters.get(source);
|
|
208
267
|
if (waiter !== undefined)
|
|
209
268
|
Deferred.doneUnsafe(waiter, Result.isSuccess(result) ? Effect.succeed(result.success) : Effect.fail(result.failure));
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
fail(new ReactorError("Overflow", "H3 retained observation byte bound exceeded", {
|
|
269
|
+
if (state.retained.bytes > limits.retained)
|
|
270
|
+
fail(ReactorError.fromCode("Overflow", "H3 retained observation byte bound exceeded", {
|
|
213
271
|
operation: "H3 observation",
|
|
214
272
|
}));
|
|
215
273
|
}
|
|
216
274
|
catch (cause) {
|
|
217
|
-
|
|
275
|
+
// The reducer rejects provider data with a ReactorError; anything else
|
|
276
|
+
// it throws is a bug, which stays a defect of the observation fiber.
|
|
277
|
+
if (!ReactorError.is(cause))
|
|
278
|
+
throw cause;
|
|
279
|
+
const error = cause;
|
|
218
280
|
if (source._tag === "Model") {
|
|
219
281
|
decoded.set(source, Result.fail(error));
|
|
220
282
|
const waiter = observedWaiters.get(source);
|
|
@@ -224,19 +286,50 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
224
286
|
fail(error);
|
|
225
287
|
}
|
|
226
288
|
};
|
|
227
|
-
yield* observation.events.pipe(Stream.runForEach((source) => Effect.sync(() =>
|
|
289
|
+
yield* observation.events.pipe(Stream.runForEach((source) => Effect.sync(() => {
|
|
290
|
+
reduce(source);
|
|
291
|
+
wake();
|
|
292
|
+
})), Effect.catch((error) => Effect.sync(() => fail(error))), Effect.andThen(Effect.sync(() => {
|
|
228
293
|
if (!closed)
|
|
229
|
-
fail(
|
|
294
|
+
fail(ReactorError.fromCode("Closed", "H3 session observation ended"));
|
|
230
295
|
})), Effect.forkScoped);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
296
|
+
// Enqueues execute in a scope of their own. Finalizers run last-added first,
|
|
297
|
+
// so a closing provider refuses new work before that scope interrupts them.
|
|
298
|
+
const executions = yield* Scope.fork(scope);
|
|
299
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
300
|
+
closed = true;
|
|
301
|
+
}));
|
|
302
|
+
const refusal = (needsFacts) => closed
|
|
303
|
+
? ReactorError.fromCode("Closed", "H3 provider scope is closed")
|
|
304
|
+
: (fatalError ??
|
|
305
|
+
(needsFacts && state.availability !== "Ready"
|
|
306
|
+
? ReactorError.fromCode("InvalidState", "H3 provider needs current state and queue observations")
|
|
307
|
+
: undefined));
|
|
308
|
+
/**
|
|
309
|
+
* H3 documents that a command replies before it broadcasts the state and
|
|
310
|
+
* queue it changed (hosted H3 broadcasts an enqueue's queue first), so the
|
|
311
|
+
* provider may be briefly synchronizing after a reply. A command that needs
|
|
312
|
+
* current facts waits, within its own reply deadline, for those
|
|
313
|
+
* broadcasts; it is refused, not submitted, only if they do not come or
|
|
314
|
+
* the provider fails.
|
|
315
|
+
*/
|
|
316
|
+
const synchronized = Effect.gen(function* () {
|
|
317
|
+
while (!closed && fatalError === undefined && state.availability === "Synchronizing") {
|
|
318
|
+
const waiter = Deferred.makeUnsafe();
|
|
319
|
+
synchronizing.add(waiter);
|
|
320
|
+
yield* Deferred.await(waiter).pipe(Effect.ensuring(Effect.sync(() => synchronizing.delete(waiter))));
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
/** Wait, within one reply deadline, while the provider is only synchronizing. */
|
|
324
|
+
const settled = Effect.suspend(() => refusal(false) === undefined && state.availability === "Synchronizing"
|
|
325
|
+
? synchronized.pipe(Effect.timeout(limits.command), Effect.ignore)
|
|
326
|
+
: Effect.void);
|
|
327
|
+
const active = (operation, needsFacts) => Effect.gen(function* () {
|
|
328
|
+
if (needsFacts)
|
|
329
|
+
yield* settled;
|
|
330
|
+
const error = refusal(needsFacts);
|
|
331
|
+
if (error !== undefined)
|
|
332
|
+
return yield* localFailure(operation, error);
|
|
240
333
|
});
|
|
241
334
|
const awaitObservation = (source) => Effect.suspend(() => {
|
|
242
335
|
const previous = decoded.get(source);
|
|
@@ -247,28 +340,31 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
247
340
|
if (fatalError !== undefined)
|
|
248
341
|
return Effect.fail(fatalError);
|
|
249
342
|
if (observedWaiters.size >= limits.pending)
|
|
250
|
-
return Effect.fail(
|
|
343
|
+
return Effect.fail(ReactorError.fromCode("Overflow", "H3 reply observer bound exceeded"));
|
|
251
344
|
const waiter = Deferred.makeUnsafe();
|
|
252
345
|
observedWaiters.set(source, waiter);
|
|
253
346
|
return Deferred.await(waiter).pipe(Effect.timeoutOrElse({
|
|
254
347
|
duration: limits.command,
|
|
255
|
-
orElse: () => Effect.fail(
|
|
348
|
+
orElse: () => Effect.fail(ReactorError.fromCode("Timeout", "H3 did not observe the command's exact returned envelope")),
|
|
256
349
|
}), Effect.ensuring(Effect.sync(() => {
|
|
257
350
|
observedWaiters.delete(source);
|
|
258
351
|
})));
|
|
259
352
|
});
|
|
260
353
|
const call = (operation, args, needsFacts = true) => Effect.gen(function* () {
|
|
261
354
|
yield* active(operation, needsFacts);
|
|
262
|
-
const source = yield* session.command(operation, args,
|
|
355
|
+
const source = yield* session.command(operation, args, { replyTimeout: limits.command });
|
|
263
356
|
const message = yield* awaitObservation(source).pipe(Effect.mapError((error) => uncertain(operation, source, "H3 reply observation failed", error)));
|
|
264
357
|
if (message?.type === "command_error" && message.data.command === operation)
|
|
265
|
-
return yield*
|
|
358
|
+
return yield* rejected(operation, source, message.data.reason);
|
|
266
359
|
return { source, message };
|
|
267
360
|
});
|
|
268
361
|
const named = (operation, args, needsFacts = true) => {
|
|
269
362
|
const expected = Commands[operation].reply;
|
|
270
363
|
return call(operation, args, needsFacts).pipe(Effect.flatMap(({ source, message }) => message?.type === expected
|
|
271
|
-
?
|
|
364
|
+
? // The broadcasts a reply implies follow it: the command settles once
|
|
365
|
+
// they are observed, so its caller reads its own effects. The state
|
|
366
|
+
// and queue reads are themselves that barrier and do not wait.
|
|
367
|
+
(needsFacts ? settled : Effect.void).pipe(Effect.as(Object.freeze({ value: message.data, source })))
|
|
272
368
|
: Effect.fail(uncertain(operation, source, `H3 ${operation} did not return ${expected}`))));
|
|
273
369
|
};
|
|
274
370
|
const checked = (operation, value) => pure(value).pipe(Effect.mapError((error) => localFailure(operation, error)));
|
|
@@ -276,7 +372,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
276
372
|
const getQueue = named("get_queue", {}, false);
|
|
277
373
|
const refresh = getState.pipe(Effect.andThen(getQueue), Effect.flatMap((reply) => state.snapshot()._tag === "Ready"
|
|
278
374
|
? Effect.void
|
|
279
|
-
: Effect.fail(
|
|
375
|
+
: Effect.fail(CommandFailure.from(ReactorError.fromCode("InvalidState", "H3 full snapshots changed during refresh"), {
|
|
280
376
|
operation: "H3 refresh",
|
|
281
377
|
outcome: "replied",
|
|
282
378
|
requestId: reply.source.requestId,
|
|
@@ -285,13 +381,13 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
285
381
|
const control = (operation, args) => call(operation, args).pipe(Effect.flatMap(({ source, message }) => source.kind === "ack"
|
|
286
382
|
? Effect.succeed(Object.freeze({ _tag: "Acknowledged", source }))
|
|
287
383
|
: message !== undefined && message.type !== "unknown"
|
|
288
|
-
? Effect.
|
|
384
|
+
? settled.pipe(Effect.as(Object.freeze({ _tag: "Reply", message, source })))
|
|
289
385
|
: Effect.fail(uncertain(operation, source, "Unexpected H3 control response"))));
|
|
290
386
|
const stage = (request, metadata) => Effect.gen(function* () {
|
|
291
387
|
yield* active("enqueue", true);
|
|
292
388
|
const before = state.snapshot();
|
|
293
389
|
if (before._tag !== "Ready")
|
|
294
|
-
return yield*
|
|
390
|
+
return yield* localFailure("enqueue", ReactorError.fromCode("InvalidState", "H3 is not ready"));
|
|
295
391
|
if (request.seconds !== undefined)
|
|
296
392
|
yield* checked("enqueue", () => seconds(request.seconds, {
|
|
297
393
|
min: before.state.clip_seconds_min,
|
|
@@ -306,20 +402,22 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
306
402
|
}
|
|
307
403
|
const digest = yield* crypto
|
|
308
404
|
.digest("SHA-256", material.bytes)
|
|
309
|
-
.pipe(Effect.mapError(() => localFailure("enqueue",
|
|
405
|
+
.pipe(Effect.mapError(() => localFailure("enqueue", ReactorError.fromCode("InvalidState", "Could not hash H3 reference"))));
|
|
310
406
|
const key = `${before.transportGeneration}:${hex(digest)}`;
|
|
311
407
|
const file = yield* uploadGate.withPermit(Effect.gen(function* () {
|
|
312
408
|
const cached = uploads.get(key);
|
|
313
409
|
if (cached !== undefined)
|
|
314
410
|
return cached;
|
|
315
411
|
const uploaded = yield* session
|
|
316
|
-
.upload(`h3-${hex(digest)}`, reference.mimeType, material.bytes,
|
|
412
|
+
.upload(`h3-${hex(digest)}`, reference.mimeType, material.bytes, {
|
|
413
|
+
uploadTimeout: limits.upload,
|
|
414
|
+
})
|
|
317
415
|
.pipe(Effect.mapError((error) => localFailure("enqueue", error)));
|
|
318
416
|
const file = yield* checked("enqueue", () => checkedUpload(uploaded.file));
|
|
319
417
|
if (file.size !== BigInt(reference.size) || file.mime_type !== reference.mimeType)
|
|
320
|
-
return yield*
|
|
418
|
+
return yield* localFailure("enqueue", ReactorError.fromCode("Protocol", "H3 upload returned different file facts"));
|
|
321
419
|
if (state.snapshot().transportGeneration !== before.transportGeneration)
|
|
322
|
-
return yield*
|
|
420
|
+
return yield* localFailure("enqueue", ReactorError.fromCode("Disconnected", "H3 reference upload crossed a generation"));
|
|
323
421
|
if (uploads.size >= limits.cache) {
|
|
324
422
|
const first = uploads.keys().next();
|
|
325
423
|
if (!first.done)
|
|
@@ -335,7 +433,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
335
433
|
});
|
|
336
434
|
const nextId = () => pure(() => {
|
|
337
435
|
if (counter === 0xffffffffffffffffn)
|
|
338
|
-
throw
|
|
436
|
+
throw ReactorError.fromCode("Overflow", "H3 submission identity space exhausted", {
|
|
339
437
|
outcome: "not-submitted",
|
|
340
438
|
});
|
|
341
439
|
return `${namespace}:${++counter}`;
|
|
@@ -343,27 +441,29 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
343
441
|
const prepared = (id, input, hooks) => Submission.make({
|
|
344
442
|
id,
|
|
345
443
|
prepare: Effect.gen(function* () {
|
|
346
|
-
const { request, metadata } = yield* input.pipe(Effect.mapError((error) =>
|
|
444
|
+
const { request, metadata } = yield* input.pipe(Effect.mapError((error) => preparationFailure("enqueue", error)));
|
|
347
445
|
const staged = yield* stage(request, metadata);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
},
|
|
446
|
+
const entry = {
|
|
447
|
+
id,
|
|
448
|
+
metadata,
|
|
449
|
+
prompt: request.prompt,
|
|
450
|
+
generation: staged.generation,
|
|
451
|
+
deferred: Deferred.makeUnsafe(),
|
|
452
|
+
awaiting: true,
|
|
453
|
+
held: undefined,
|
|
357
454
|
};
|
|
455
|
+
return { ...staged, entry };
|
|
358
456
|
}),
|
|
359
457
|
commit: ({ entry }) => Effect.gen(function* () {
|
|
360
458
|
yield* active("enqueue", true);
|
|
361
459
|
const live = yield* session.ready.pipe(Effect.mapError((error) => localFailure("enqueue", error)));
|
|
362
460
|
if (entry.generation !== state.snapshot().transportGeneration ||
|
|
363
461
|
live.generation !== entry.generation)
|
|
364
|
-
return yield*
|
|
462
|
+
return yield* localFailure("enqueue", ReactorError.fromCode("Disconnected", "H3 prepared input crossed a transport generation"));
|
|
365
463
|
if (pending.size >= limits.pending)
|
|
366
|
-
return yield*
|
|
464
|
+
return yield* localFailure("enqueue", ReactorError.fromCode("Overflow", "H3 pending acceptance bound reached"));
|
|
465
|
+
// The operation's slot is taken before anything is sent.
|
|
466
|
+
yield* pure(() => operations.reserve(entry)).pipe(Effect.mapError((error) => localFailure("enqueue", error)));
|
|
367
467
|
// Acceptance registration is inside the same commit boundary as the
|
|
368
468
|
// orchestration hook and precedes Session.command's wire correlation.
|
|
369
469
|
pending.set(id, entry);
|
|
@@ -371,16 +471,17 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
371
471
|
yield* hooks.commit(id).pipe(Effect.onExit((exit) => Exit.isFailure(exit)
|
|
372
472
|
? Effect.sync(() => {
|
|
373
473
|
pending.delete(id);
|
|
474
|
+
operations.abandon(id);
|
|
374
475
|
})
|
|
375
476
|
: Effect.void));
|
|
376
477
|
}),
|
|
377
478
|
execute: ({ args, entry }) => {
|
|
378
479
|
const execution = Effect.gen(function* () {
|
|
379
|
-
const sent = yield* Effect.result(session.command("enqueue", args,
|
|
480
|
+
const sent = yield* Effect.result(session.command("enqueue", args, { replyTimeout: limits.command }));
|
|
380
481
|
let original;
|
|
381
482
|
if (Result.isFailure(sent)) {
|
|
382
483
|
if (sent.failure.context.outcome !== "unknown")
|
|
383
|
-
return yield*
|
|
484
|
+
return yield* sent.failure;
|
|
384
485
|
original = sent.failure;
|
|
385
486
|
}
|
|
386
487
|
else {
|
|
@@ -389,29 +490,58 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
389
490
|
if (Result.isSuccess(observed) &&
|
|
390
491
|
observed.success?.type === "command_error" &&
|
|
391
492
|
observed.success.data.command === "enqueue")
|
|
392
|
-
return yield*
|
|
493
|
+
return yield* rejected("enqueue", source, observed.success.data.reason);
|
|
393
494
|
original = uncertain("enqueue", source, "H3 enqueue has no proven clip acceptance", Result.isFailure(observed) ? observed.failure : undefined);
|
|
394
495
|
}
|
|
395
|
-
|
|
496
|
+
// The reply was observed or is lost: evidence by metadata that came
|
|
497
|
+
// while it was awaited now decides, and later evidence at once.
|
|
498
|
+
entry.awaiting = false;
|
|
499
|
+
decideHeld(entry);
|
|
500
|
+
const acceptance = yield* Deferred.await(entry.deferred).pipe(Effect.timeoutOrElse({
|
|
396
501
|
duration: limits.reconcile,
|
|
397
502
|
orElse: () => Effect.fail(original),
|
|
398
|
-
}), Effect.mapError(() => original));
|
|
399
|
-
|
|
503
|
+
}), Effect.mapError(() => original), Effect.withSpan("reactor.h3.reconcile", {}, { captureStackTrace: false }));
|
|
504
|
+
// Like every command, an enqueue settles once the snapshots its reply
|
|
505
|
+
// implies are observed, so its caller finds the clip in the queue.
|
|
506
|
+
yield* settled;
|
|
507
|
+
return acceptance;
|
|
508
|
+
}).pipe(Effect.onExit((exit) => Effect.sync(() => {
|
|
509
|
+
// However the enqueue ends, interrupted as the provider closes or
|
|
510
|
+
// otherwise, the evidence it saw still decides; only a definite
|
|
511
|
+
// failure discards it.
|
|
512
|
+
const error = Exit.findError(exit);
|
|
513
|
+
if (!(error._tag === "Success" && error.success.context.outcome !== "unknown"))
|
|
514
|
+
decideHeld(entry);
|
|
400
515
|
pending.delete(id);
|
|
401
|
-
})))
|
|
516
|
+
})), Effect.onExit((exit) => Effect.sync(() => {
|
|
517
|
+
operations.settle(id, exit);
|
|
518
|
+
})),
|
|
519
|
+
// The submission's own execution fiber carries the span, so it ends with
|
|
520
|
+
// the enqueue's outcome even when the caller stopped waiting.
|
|
521
|
+
Effect.onExit((exit) => {
|
|
522
|
+
const error = Exit.findError(exit);
|
|
523
|
+
return Effect.annotateCurrentSpan(Exit.isSuccess(exit)
|
|
524
|
+
? { "reactor.command.outcome": "replied" }
|
|
525
|
+
: error._tag === "Success"
|
|
526
|
+
? {
|
|
527
|
+
"reactor.command.outcome": error.success.context.outcome,
|
|
528
|
+
"error.type": error.success.reason._tag,
|
|
529
|
+
}
|
|
530
|
+
: {});
|
|
531
|
+
}), Effect.withSpan("reactor.h3.enqueue", { kind: "client", attributes: { "reactor.h3.submission.id": id } }, { captureStackTrace: false }));
|
|
402
532
|
return hooks.result === undefined
|
|
403
533
|
? execution
|
|
404
534
|
: Effect.result(execution).pipe(Effect.flatMap((result) => Effect.gen(function* () {
|
|
405
535
|
const hook = yield* Effect.exit(Effect.suspend(() => hooks.result(id, result)).pipe(Effect.interruptible, Effect.timeoutOrElse({
|
|
406
536
|
duration: limits.hook,
|
|
407
|
-
orElse: () => Effect.fail(
|
|
537
|
+
orElse: () => Effect.fail(ReactorError.fromCode("Timeout", "H3 result hook exceeded its deadline")),
|
|
408
538
|
})));
|
|
409
539
|
// Preserve the entire secondary Cause for deliberate inspection,
|
|
410
540
|
// including defects; never substitute it for primary remote evidence.
|
|
411
541
|
if (Exit.isFailure(hook))
|
|
412
542
|
emit({
|
|
413
543
|
_tag: "Diagnostic",
|
|
414
|
-
error:
|
|
544
|
+
error: ReactorError.fromCode("InvalidState", "H3 result hook failed", {
|
|
415
545
|
operation: "H3 result hook",
|
|
416
546
|
detail: hook.cause,
|
|
417
547
|
}),
|
|
@@ -421,7 +551,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
421
551
|
: Effect.fail(result.failure);
|
|
422
552
|
})));
|
|
423
553
|
},
|
|
424
|
-
}).pipe(Scope.provide(
|
|
554
|
+
}).pipe(Scope.provide(executions));
|
|
425
555
|
const prepare = (input, hooks = {}) => Effect.gen(function* () {
|
|
426
556
|
yield* active("enqueue", true);
|
|
427
557
|
const request = yield* pure(() => captureRequest(input, limits.prompt));
|
|
@@ -432,9 +562,9 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
432
562
|
const prepareFrom = (preparation, hooks = {}) => Effect.gen(function* () {
|
|
433
563
|
yield* active("enqueue", true);
|
|
434
564
|
if (!Effect.isEffect(preparation))
|
|
435
|
-
return yield*
|
|
565
|
+
return yield* ReactorError.fromCode("InvalidInput", "H3 preparation must be an Effect", {
|
|
436
566
|
outcome: "not-submitted",
|
|
437
|
-
})
|
|
567
|
+
});
|
|
438
568
|
const id = yield* nextId();
|
|
439
569
|
return yield* prepared(id, preparation.pipe(Effect.flatMap((input) => pure(() => {
|
|
440
570
|
const request = captureRequest(input, limits.prompt);
|
|
@@ -443,14 +573,15 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
443
573
|
});
|
|
444
574
|
const contract = yield* session.schema.pipe(Effect.flatMap(({ openapi }) => pure(() => validateDeployment(openapi))), Effect.timeoutOrElse({
|
|
445
575
|
duration: limits.setup,
|
|
446
|
-
orElse: () => Effect.fail(
|
|
576
|
+
orElse: () => Effect.fail(ReactorError.fromCode("Timeout", "H3 schema read timed out")),
|
|
447
577
|
}));
|
|
448
578
|
yield* refresh.pipe(Effect.timeoutOrElse({
|
|
449
579
|
duration: limits.setup,
|
|
450
|
-
orElse: () => Effect.fail(
|
|
580
|
+
orElse: () => Effect.fail(ReactorError.fromCode("Timeout", "H3 initial state and queue reads timed out")),
|
|
451
581
|
}));
|
|
452
582
|
if (state.snapshot()._tag !== "Ready")
|
|
453
|
-
return yield*
|
|
583
|
+
return yield* (fatalError ??
|
|
584
|
+
ReactorError.fromCode("InvalidState", "H3 initial observations are unavailable"));
|
|
454
585
|
const provider = {
|
|
455
586
|
sessionId: session.id,
|
|
456
587
|
contract,
|
|
@@ -458,44 +589,47 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
458
589
|
observe: (bounds) => Effect.gen(function* () {
|
|
459
590
|
const stream = yield* events.subscribe(bounds);
|
|
460
591
|
const initial = state.snapshot();
|
|
461
|
-
|
|
592
|
+
// The reducer may apply a queued event between the subscription and
|
|
593
|
+
// this read; the snapshot then covers it, so it is not repeated.
|
|
594
|
+
return {
|
|
595
|
+
initial,
|
|
596
|
+
revision: initial.revision,
|
|
597
|
+
events: Stream.filter(stream, (event) => !covered(event, initial.revision)),
|
|
598
|
+
};
|
|
462
599
|
}),
|
|
463
600
|
events: (bounds) => events.stream(bounds),
|
|
464
601
|
failure: Deferred.await(fatal),
|
|
465
602
|
acceptances: Effect.sync(() => Object.freeze([...acceptances.values()])),
|
|
466
603
|
acceptance: (id) => Effect.sync(() => acceptances.get(id)),
|
|
604
|
+
operation: (submission) => operations.attach(submission.id),
|
|
467
605
|
prepare,
|
|
468
606
|
prepareFrom,
|
|
469
|
-
enqueue: (request) => prepare(request).pipe(Effect.mapError((error) => localFailure("enqueue", error)), Effect.flatMap((submission) => submission.submit)),
|
|
607
|
+
enqueue: (request) => prepare(request).pipe(Effect.mapError((error) => ReactorError.is(error) ? localFailure("enqueue", error) : error), Effect.flatMap((submission) => submission.submit)),
|
|
470
608
|
getState,
|
|
471
609
|
getQueue,
|
|
472
610
|
refresh,
|
|
473
|
-
pop: (id) => checked("pop", () => clipId(id)).pipe(Effect.flatMap((id) => named("pop", { clip_id: id })), Effect.
|
|
474
|
-
? Effect.succeed(reply)
|
|
475
|
-
: Effect.fail(uncertain("pop", reply.source, "H3 pop returned a different clip")))),
|
|
611
|
+
pop: (id) => checked("pop", () => clipId(id)).pipe(Effect.flatMap((id) => named("pop", { clip_id: id })), Effect.filterOrFail((reply) => reply.value.clip.clip_id === id, (reply) => uncertain("pop", reply.source, "H3 pop returned a different clip"))),
|
|
476
612
|
move: (id, position) => checked("move", () => ({
|
|
477
613
|
clip_id: clipId(id),
|
|
478
614
|
position: nonnegative(position, "position"),
|
|
479
|
-
})).pipe(Effect.flatMap((args) => named("move", args)), Effect.
|
|
480
|
-
? Effect.succeed(reply)
|
|
481
|
-
: Effect.fail(uncertain("move", reply.source, "H3 move returned a different clip")))),
|
|
615
|
+
})).pipe(Effect.flatMap((args) => named("move", args)), Effect.filterOrFail((reply) => reply.value.clip.clip_id === id, (reply) => uncertain("move", reply.source, "H3 move returned a different clip"))),
|
|
482
616
|
play: (id) => checked("play", () => ({ clip_id: id === undefined ? "" : clipId(id) })).pipe(Effect.flatMap((args) => control("play", args))),
|
|
483
617
|
stop: control("stop", {}),
|
|
484
618
|
setSeed: (value) => checked("set_seed", () => nonnegative(value, "seed")).pipe(Effect.flatMap((seed) => named("set_seed", { seed }))),
|
|
485
619
|
setClipSeconds: (value) => checked("set_clip_seconds", () => seconds(value)).pipe(Effect.flatMap((seconds) => named("set_clip_seconds", { seconds }))),
|
|
486
620
|
setCanvas: (aspect) => checked("set_canvas", () => {
|
|
487
621
|
if (!Object.hasOwn(canvases, aspect))
|
|
488
|
-
throw
|
|
622
|
+
throw ReactorError.fromCode("InvalidInput", "Unsupported H3 canvas aspect");
|
|
489
623
|
return { aspect };
|
|
490
624
|
}).pipe(Effect.flatMap((args) => named("set_canvas", args))),
|
|
491
625
|
setAutoplay: (enabled) => checked("set_autoplay", () => {
|
|
492
626
|
if (typeof enabled !== "boolean")
|
|
493
|
-
throw
|
|
627
|
+
throw ReactorError.fromCode("InvalidInput", "Autoplay must be boolean");
|
|
494
628
|
return { enabled };
|
|
495
629
|
}).pipe(Effect.flatMap((args) => named("set_autoplay", args))),
|
|
496
630
|
setFlushOnClipEnd: (enabled) => checked("set_flush_on_clip_end", () => {
|
|
497
631
|
if (typeof enabled !== "boolean")
|
|
498
|
-
throw
|
|
632
|
+
throw ReactorError.fromCode("InvalidInput", "Flush setting must be boolean");
|
|
499
633
|
return { enabled };
|
|
500
634
|
}).pipe(Effect.flatMap((args) => named("set_flush_on_clip_end", args))),
|
|
501
635
|
reset: named("reset", {}),
|