reactor-effect-client 0.2.0 → 0.3.0-rc.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 +62 -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 +168 -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 +210 -125
- 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 +48 -0
- package/dist/h3/_internal/operations.d.ts.map +1 -0
- package/dist/h3/_internal/operations.js +230 -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 +108 -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 +208 -109
- 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.map +1 -1
- package/dist/stats.js +4 -5
- 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,10 +98,33 @@ 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));
|
|
111
129
|
const fail = (error) => {
|
|
112
130
|
if (fatalError !== undefined)
|
|
@@ -119,10 +137,14 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
119
137
|
for (const waiter of observedWaiters.values())
|
|
120
138
|
Deferred.doneUnsafe(waiter, Effect.fail(error));
|
|
121
139
|
emit({ _tag: "Diagnostic", error });
|
|
140
|
+
wake();
|
|
122
141
|
};
|
|
123
142
|
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
124
143
|
closed = true;
|
|
125
|
-
fail(
|
|
144
|
+
fail(ReactorError.fromCode("Closed", "H3 provider scope closed", {
|
|
145
|
+
operation: "H3 observation",
|
|
146
|
+
}));
|
|
147
|
+
operations.retire();
|
|
126
148
|
events.end();
|
|
127
149
|
pending.clear();
|
|
128
150
|
observedWaiters.clear();
|
|
@@ -133,25 +155,31 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
133
155
|
if (id === undefined)
|
|
134
156
|
return;
|
|
135
157
|
const entry = pending.get(id);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
const acceptance = entry === undefined ? undefined : acceptanceFor(entry, clip, source);
|
|
159
|
+
if (entry === undefined || acceptance === undefined) {
|
|
160
|
+
// After the reconcile window, or in a later transport generation, the
|
|
161
|
+
// evidence can still resolve the operation, never the acceptances.
|
|
162
|
+
operations.lateEvidence(id, clip, source);
|
|
140
163
|
return;
|
|
164
|
+
}
|
|
141
165
|
const previous = acceptances.get(entry.id);
|
|
142
166
|
if (previous !== undefined) {
|
|
143
167
|
if (previous.clip.clip_id !== clip.clip_id)
|
|
144
|
-
fail(
|
|
168
|
+
fail(ReactorError.fromCode("Protocol", "H3 acceptance identity named two clips", {
|
|
145
169
|
operation: "enqueue",
|
|
146
170
|
}));
|
|
147
171
|
return;
|
|
148
172
|
}
|
|
149
173
|
if (acceptances.size >= limits.acceptances) {
|
|
150
|
-
const first = acceptances.
|
|
151
|
-
if (!first.done)
|
|
152
|
-
acceptances.delete(first.value);
|
|
174
|
+
const first = acceptances.entries().next();
|
|
175
|
+
if (!first.done) {
|
|
176
|
+
acceptances.delete(first.value[0]);
|
|
177
|
+
retain(first.value[1], -1);
|
|
178
|
+
}
|
|
153
179
|
}
|
|
154
180
|
acceptances.set(entry.id, acceptance);
|
|
181
|
+
retain(acceptance, 1);
|
|
182
|
+
operations.accept(acceptance);
|
|
155
183
|
Deferred.doneUnsafe(entry.deferred, Effect.succeed(acceptance));
|
|
156
184
|
emit({ _tag: "Acceptance", acceptance });
|
|
157
185
|
};
|
|
@@ -159,15 +187,13 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
159
187
|
if (closed || fatalError !== undefined)
|
|
160
188
|
return;
|
|
161
189
|
try {
|
|
162
|
-
const before = state.
|
|
190
|
+
const before = state.transportGeneration;
|
|
163
191
|
let disposition = state.admit(source);
|
|
164
|
-
if (state.
|
|
192
|
+
if (state.transportGeneration !== before) {
|
|
165
193
|
uploads.clear();
|
|
166
194
|
for (const entry of pending.values())
|
|
167
195
|
if (entry.generation !== source.generation)
|
|
168
|
-
Deferred.doneUnsafe(entry.deferred, Effect.fail(
|
|
169
|
-
operation: "enqueue",
|
|
170
|
-
})));
|
|
196
|
+
Deferred.doneUnsafe(entry.deferred, Effect.fail(ReactorError.fromCode("Disconnected", "H3 acceptance crossed a transport generation", { operation: "enqueue" })));
|
|
171
197
|
}
|
|
172
198
|
if (source._tag !== "Model") {
|
|
173
199
|
emit({ _tag: "Session", source });
|
|
@@ -177,7 +203,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
177
203
|
if (source.kind === "ack") {
|
|
178
204
|
result =
|
|
179
205
|
disposition === "stale"
|
|
180
|
-
? Result.fail(
|
|
206
|
+
? Result.fail(ReactorError.fromCode("Disconnected", "H3 received a stale command acknowledgement"))
|
|
181
207
|
: Result.succeed(undefined);
|
|
182
208
|
emit({ _tag: "Acknowledged", source });
|
|
183
209
|
}
|
|
@@ -187,7 +213,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
187
213
|
disposition = state.apply(message, source);
|
|
188
214
|
result =
|
|
189
215
|
disposition === "stale"
|
|
190
|
-
? Result.fail(
|
|
216
|
+
? Result.fail(ReactorError.fromCode("Disconnected", "H3 received a stale command message"))
|
|
191
217
|
: Result.succeed(message);
|
|
192
218
|
if (disposition !== "stale" && message.type !== "unknown") {
|
|
193
219
|
if (message.type === "queue_update") {
|
|
@@ -200,6 +226,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
200
226
|
}
|
|
201
227
|
else if ("clip" in message.data)
|
|
202
228
|
accept(message.data.clip, source);
|
|
229
|
+
operations.observe(message, source);
|
|
203
230
|
}
|
|
204
231
|
emit({ _tag: "Message", message, source, disposition });
|
|
205
232
|
}
|
|
@@ -207,14 +234,17 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
207
234
|
const waiter = observedWaiters.get(source);
|
|
208
235
|
if (waiter !== undefined)
|
|
209
236
|
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", {
|
|
237
|
+
if (state.retained.bytes > limits.retained)
|
|
238
|
+
fail(ReactorError.fromCode("Overflow", "H3 retained observation byte bound exceeded", {
|
|
213
239
|
operation: "H3 observation",
|
|
214
240
|
}));
|
|
215
241
|
}
|
|
216
242
|
catch (cause) {
|
|
217
|
-
|
|
243
|
+
// The reducer rejects provider data with a ReactorError; anything else
|
|
244
|
+
// it throws is a bug, which stays a defect of the observation fiber.
|
|
245
|
+
if (!ReactorError.is(cause))
|
|
246
|
+
throw cause;
|
|
247
|
+
const error = cause;
|
|
218
248
|
if (source._tag === "Model") {
|
|
219
249
|
decoded.set(source, Result.fail(error));
|
|
220
250
|
const waiter = observedWaiters.get(source);
|
|
@@ -224,19 +254,43 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
224
254
|
fail(error);
|
|
225
255
|
}
|
|
226
256
|
};
|
|
227
|
-
yield* observation.events.pipe(Stream.runForEach((source) => Effect.sync(() =>
|
|
257
|
+
yield* observation.events.pipe(Stream.runForEach((source) => Effect.sync(() => {
|
|
258
|
+
reduce(source);
|
|
259
|
+
wake();
|
|
260
|
+
})), Effect.catch((error) => Effect.sync(() => fail(error))), Effect.andThen(Effect.sync(() => {
|
|
228
261
|
if (!closed)
|
|
229
|
-
fail(
|
|
262
|
+
fail(ReactorError.fromCode("Closed", "H3 session observation ended"));
|
|
230
263
|
})), Effect.forkScoped);
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
264
|
+
const refusal = (needsFacts) => closed
|
|
265
|
+
? ReactorError.fromCode("Closed", "H3 provider scope is closed")
|
|
266
|
+
: (fatalError ??
|
|
267
|
+
(needsFacts && state.availability !== "Ready"
|
|
268
|
+
? ReactorError.fromCode("InvalidState", "H3 provider needs current state and queue observations")
|
|
269
|
+
: undefined));
|
|
270
|
+
/**
|
|
271
|
+
* H3 replies to a command before it broadcasts the state and queue the
|
|
272
|
+
* command changed, so the provider is briefly synchronizing after every
|
|
273
|
+
* reply. A command that needs current facts waits, within its own reply
|
|
274
|
+
* deadline, for those broadcasts; it is refused, not submitted, only if
|
|
275
|
+
* they do not come or the provider fails.
|
|
276
|
+
*/
|
|
277
|
+
const synchronized = Effect.gen(function* () {
|
|
278
|
+
while (!closed && fatalError === undefined && state.availability === "Synchronizing") {
|
|
279
|
+
const waiter = Deferred.makeUnsafe();
|
|
280
|
+
synchronizing.add(waiter);
|
|
281
|
+
yield* Deferred.await(waiter).pipe(Effect.ensuring(Effect.sync(() => synchronizing.delete(waiter))));
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
/** Wait, within one reply deadline, while the provider is only synchronizing. */
|
|
285
|
+
const settled = Effect.suspend(() => refusal(false) === undefined && state.availability === "Synchronizing"
|
|
286
|
+
? synchronized.pipe(Effect.timeout(limits.command), Effect.ignore)
|
|
287
|
+
: Effect.void);
|
|
288
|
+
const active = (operation, needsFacts) => Effect.gen(function* () {
|
|
289
|
+
if (needsFacts)
|
|
290
|
+
yield* settled;
|
|
291
|
+
const error = refusal(needsFacts);
|
|
292
|
+
if (error !== undefined)
|
|
293
|
+
return yield* localFailure(operation, error);
|
|
240
294
|
});
|
|
241
295
|
const awaitObservation = (source) => Effect.suspend(() => {
|
|
242
296
|
const previous = decoded.get(source);
|
|
@@ -247,28 +301,31 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
247
301
|
if (fatalError !== undefined)
|
|
248
302
|
return Effect.fail(fatalError);
|
|
249
303
|
if (observedWaiters.size >= limits.pending)
|
|
250
|
-
return Effect.fail(
|
|
304
|
+
return Effect.fail(ReactorError.fromCode("Overflow", "H3 reply observer bound exceeded"));
|
|
251
305
|
const waiter = Deferred.makeUnsafe();
|
|
252
306
|
observedWaiters.set(source, waiter);
|
|
253
307
|
return Deferred.await(waiter).pipe(Effect.timeoutOrElse({
|
|
254
308
|
duration: limits.command,
|
|
255
|
-
orElse: () => Effect.fail(
|
|
309
|
+
orElse: () => Effect.fail(ReactorError.fromCode("Timeout", "H3 did not observe the command's exact returned envelope")),
|
|
256
310
|
}), Effect.ensuring(Effect.sync(() => {
|
|
257
311
|
observedWaiters.delete(source);
|
|
258
312
|
})));
|
|
259
313
|
});
|
|
260
314
|
const call = (operation, args, needsFacts = true) => Effect.gen(function* () {
|
|
261
315
|
yield* active(operation, needsFacts);
|
|
262
|
-
const source = yield* session.command(operation, args,
|
|
316
|
+
const source = yield* session.command(operation, args, { replyTimeout: limits.command });
|
|
263
317
|
const message = yield* awaitObservation(source).pipe(Effect.mapError((error) => uncertain(operation, source, "H3 reply observation failed", error)));
|
|
264
318
|
if (message?.type === "command_error" && message.data.command === operation)
|
|
265
|
-
return yield*
|
|
319
|
+
return yield* rejected(operation, source, message.data.reason);
|
|
266
320
|
return { source, message };
|
|
267
321
|
});
|
|
268
322
|
const named = (operation, args, needsFacts = true) => {
|
|
269
323
|
const expected = Commands[operation].reply;
|
|
270
324
|
return call(operation, args, needsFacts).pipe(Effect.flatMap(({ source, message }) => message?.type === expected
|
|
271
|
-
?
|
|
325
|
+
? // The broadcasts a reply implies follow it: the command settles once
|
|
326
|
+
// they are observed, so its caller reads its own effects. The state
|
|
327
|
+
// and queue reads are themselves that barrier and do not wait.
|
|
328
|
+
(needsFacts ? settled : Effect.void).pipe(Effect.as(Object.freeze({ value: message.data, source })))
|
|
272
329
|
: Effect.fail(uncertain(operation, source, `H3 ${operation} did not return ${expected}`))));
|
|
273
330
|
};
|
|
274
331
|
const checked = (operation, value) => pure(value).pipe(Effect.mapError((error) => localFailure(operation, error)));
|
|
@@ -276,7 +333,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
276
333
|
const getQueue = named("get_queue", {}, false);
|
|
277
334
|
const refresh = getState.pipe(Effect.andThen(getQueue), Effect.flatMap((reply) => state.snapshot()._tag === "Ready"
|
|
278
335
|
? Effect.void
|
|
279
|
-
: Effect.fail(
|
|
336
|
+
: Effect.fail(CommandFailure.from(ReactorError.fromCode("InvalidState", "H3 full snapshots changed during refresh"), {
|
|
280
337
|
operation: "H3 refresh",
|
|
281
338
|
outcome: "replied",
|
|
282
339
|
requestId: reply.source.requestId,
|
|
@@ -285,13 +342,13 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
285
342
|
const control = (operation, args) => call(operation, args).pipe(Effect.flatMap(({ source, message }) => source.kind === "ack"
|
|
286
343
|
? Effect.succeed(Object.freeze({ _tag: "Acknowledged", source }))
|
|
287
344
|
: message !== undefined && message.type !== "unknown"
|
|
288
|
-
? Effect.
|
|
345
|
+
? settled.pipe(Effect.as(Object.freeze({ _tag: "Reply", message, source })))
|
|
289
346
|
: Effect.fail(uncertain(operation, source, "Unexpected H3 control response"))));
|
|
290
347
|
const stage = (request, metadata) => Effect.gen(function* () {
|
|
291
348
|
yield* active("enqueue", true);
|
|
292
349
|
const before = state.snapshot();
|
|
293
350
|
if (before._tag !== "Ready")
|
|
294
|
-
return yield*
|
|
351
|
+
return yield* localFailure("enqueue", ReactorError.fromCode("InvalidState", "H3 is not ready"));
|
|
295
352
|
if (request.seconds !== undefined)
|
|
296
353
|
yield* checked("enqueue", () => seconds(request.seconds, {
|
|
297
354
|
min: before.state.clip_seconds_min,
|
|
@@ -306,20 +363,22 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
306
363
|
}
|
|
307
364
|
const digest = yield* crypto
|
|
308
365
|
.digest("SHA-256", material.bytes)
|
|
309
|
-
.pipe(Effect.mapError(() => localFailure("enqueue",
|
|
366
|
+
.pipe(Effect.mapError(() => localFailure("enqueue", ReactorError.fromCode("InvalidState", "Could not hash H3 reference"))));
|
|
310
367
|
const key = `${before.transportGeneration}:${hex(digest)}`;
|
|
311
368
|
const file = yield* uploadGate.withPermit(Effect.gen(function* () {
|
|
312
369
|
const cached = uploads.get(key);
|
|
313
370
|
if (cached !== undefined)
|
|
314
371
|
return cached;
|
|
315
372
|
const uploaded = yield* session
|
|
316
|
-
.upload(`h3-${hex(digest)}`, reference.mimeType, material.bytes,
|
|
373
|
+
.upload(`h3-${hex(digest)}`, reference.mimeType, material.bytes, {
|
|
374
|
+
uploadTimeout: limits.upload,
|
|
375
|
+
})
|
|
317
376
|
.pipe(Effect.mapError((error) => localFailure("enqueue", error)));
|
|
318
377
|
const file = yield* checked("enqueue", () => checkedUpload(uploaded.file));
|
|
319
378
|
if (file.size !== BigInt(reference.size) || file.mime_type !== reference.mimeType)
|
|
320
|
-
return yield*
|
|
379
|
+
return yield* localFailure("enqueue", ReactorError.fromCode("Protocol", "H3 upload returned different file facts"));
|
|
321
380
|
if (state.snapshot().transportGeneration !== before.transportGeneration)
|
|
322
|
-
return yield*
|
|
381
|
+
return yield* localFailure("enqueue", ReactorError.fromCode("Disconnected", "H3 reference upload crossed a generation"));
|
|
323
382
|
if (uploads.size >= limits.cache) {
|
|
324
383
|
const first = uploads.keys().next();
|
|
325
384
|
if (!first.done)
|
|
@@ -335,7 +394,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
335
394
|
});
|
|
336
395
|
const nextId = () => pure(() => {
|
|
337
396
|
if (counter === 0xffffffffffffffffn)
|
|
338
|
-
throw
|
|
397
|
+
throw ReactorError.fromCode("Overflow", "H3 submission identity space exhausted", {
|
|
339
398
|
outcome: "not-submitted",
|
|
340
399
|
});
|
|
341
400
|
return `${namespace}:${++counter}`;
|
|
@@ -343,7 +402,7 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
343
402
|
const prepared = (id, input, hooks) => Submission.make({
|
|
344
403
|
id,
|
|
345
404
|
prepare: Effect.gen(function* () {
|
|
346
|
-
const { request, metadata } = yield* input.pipe(Effect.mapError((error) =>
|
|
405
|
+
const { request, metadata } = yield* input.pipe(Effect.mapError((error) => preparationFailure("enqueue", error)));
|
|
347
406
|
const staged = yield* stage(request, metadata);
|
|
348
407
|
return {
|
|
349
408
|
...staged,
|
|
@@ -361,9 +420,11 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
361
420
|
const live = yield* session.ready.pipe(Effect.mapError((error) => localFailure("enqueue", error)));
|
|
362
421
|
if (entry.generation !== state.snapshot().transportGeneration ||
|
|
363
422
|
live.generation !== entry.generation)
|
|
364
|
-
return yield*
|
|
423
|
+
return yield* localFailure("enqueue", ReactorError.fromCode("Disconnected", "H3 prepared input crossed a transport generation"));
|
|
365
424
|
if (pending.size >= limits.pending)
|
|
366
|
-
return yield*
|
|
425
|
+
return yield* localFailure("enqueue", ReactorError.fromCode("Overflow", "H3 pending acceptance bound reached"));
|
|
426
|
+
// The operation's slot is taken before anything is sent.
|
|
427
|
+
yield* pure(() => operations.reserve(entry)).pipe(Effect.mapError((error) => localFailure("enqueue", error)));
|
|
367
428
|
// Acceptance registration is inside the same commit boundary as the
|
|
368
429
|
// orchestration hook and precedes Session.command's wire correlation.
|
|
369
430
|
pending.set(id, entry);
|
|
@@ -371,16 +432,17 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
371
432
|
yield* hooks.commit(id).pipe(Effect.onExit((exit) => Exit.isFailure(exit)
|
|
372
433
|
? Effect.sync(() => {
|
|
373
434
|
pending.delete(id);
|
|
435
|
+
operations.abandon(id);
|
|
374
436
|
})
|
|
375
437
|
: Effect.void));
|
|
376
438
|
}),
|
|
377
439
|
execute: ({ args, entry }) => {
|
|
378
440
|
const execution = Effect.gen(function* () {
|
|
379
|
-
const sent = yield* Effect.result(session.command("enqueue", args,
|
|
441
|
+
const sent = yield* Effect.result(session.command("enqueue", args, { replyTimeout: limits.command }));
|
|
380
442
|
let original;
|
|
381
443
|
if (Result.isFailure(sent)) {
|
|
382
444
|
if (sent.failure.context.outcome !== "unknown")
|
|
383
|
-
return yield*
|
|
445
|
+
return yield* sent.failure;
|
|
384
446
|
original = sent.failure;
|
|
385
447
|
}
|
|
386
448
|
else {
|
|
@@ -389,29 +451,48 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
389
451
|
if (Result.isSuccess(observed) &&
|
|
390
452
|
observed.success?.type === "command_error" &&
|
|
391
453
|
observed.success.data.command === "enqueue")
|
|
392
|
-
return yield*
|
|
454
|
+
return yield* rejected("enqueue", source, observed.success.data.reason);
|
|
393
455
|
original = uncertain("enqueue", source, "H3 enqueue has no proven clip acceptance", Result.isFailure(observed) ? observed.failure : undefined);
|
|
394
456
|
}
|
|
395
|
-
|
|
457
|
+
const acceptance = yield* Deferred.await(entry.deferred).pipe(Effect.timeoutOrElse({
|
|
396
458
|
duration: limits.reconcile,
|
|
397
459
|
orElse: () => Effect.fail(original),
|
|
398
|
-
}), Effect.mapError(() => original));
|
|
460
|
+
}), Effect.mapError(() => original), Effect.withSpan("reactor.h3.reconcile", {}, { captureStackTrace: false }));
|
|
461
|
+
// Like every command, an enqueue settles once the snapshots its reply
|
|
462
|
+
// implies are observed, so its caller finds the clip in the queue.
|
|
463
|
+
yield* settled;
|
|
464
|
+
return acceptance;
|
|
399
465
|
}).pipe(Effect.ensuring(Effect.sync(() => {
|
|
400
466
|
pending.delete(id);
|
|
401
|
-
})))
|
|
467
|
+
})), Effect.onExit((exit) => Effect.sync(() => {
|
|
468
|
+
operations.settle(id, exit);
|
|
469
|
+
})),
|
|
470
|
+
// The submission's own execution fiber carries the span, so it ends with
|
|
471
|
+
// the enqueue's outcome even when the caller stopped waiting.
|
|
472
|
+
Effect.onExit((exit) => {
|
|
473
|
+
const error = Exit.findError(exit);
|
|
474
|
+
return Effect.annotateCurrentSpan(Exit.isSuccess(exit)
|
|
475
|
+
? { "reactor.command.outcome": "replied" }
|
|
476
|
+
: error._tag === "Success"
|
|
477
|
+
? {
|
|
478
|
+
"reactor.command.outcome": error.success.context.outcome,
|
|
479
|
+
"error.type": error.success.reason._tag,
|
|
480
|
+
}
|
|
481
|
+
: {});
|
|
482
|
+
}), Effect.withSpan("reactor.h3.enqueue", { kind: "client", attributes: { "reactor.h3.submission.id": id } }, { captureStackTrace: false }));
|
|
402
483
|
return hooks.result === undefined
|
|
403
484
|
? execution
|
|
404
485
|
: Effect.result(execution).pipe(Effect.flatMap((result) => Effect.gen(function* () {
|
|
405
486
|
const hook = yield* Effect.exit(Effect.suspend(() => hooks.result(id, result)).pipe(Effect.interruptible, Effect.timeoutOrElse({
|
|
406
487
|
duration: limits.hook,
|
|
407
|
-
orElse: () => Effect.fail(
|
|
488
|
+
orElse: () => Effect.fail(ReactorError.fromCode("Timeout", "H3 result hook exceeded its deadline")),
|
|
408
489
|
})));
|
|
409
490
|
// Preserve the entire secondary Cause for deliberate inspection,
|
|
410
491
|
// including defects; never substitute it for primary remote evidence.
|
|
411
492
|
if (Exit.isFailure(hook))
|
|
412
493
|
emit({
|
|
413
494
|
_tag: "Diagnostic",
|
|
414
|
-
error:
|
|
495
|
+
error: ReactorError.fromCode("InvalidState", "H3 result hook failed", {
|
|
415
496
|
operation: "H3 result hook",
|
|
416
497
|
detail: hook.cause,
|
|
417
498
|
}),
|
|
@@ -432,9 +513,9 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
432
513
|
const prepareFrom = (preparation, hooks = {}) => Effect.gen(function* () {
|
|
433
514
|
yield* active("enqueue", true);
|
|
434
515
|
if (!Effect.isEffect(preparation))
|
|
435
|
-
return yield*
|
|
516
|
+
return yield* ReactorError.fromCode("InvalidInput", "H3 preparation must be an Effect", {
|
|
436
517
|
outcome: "not-submitted",
|
|
437
|
-
})
|
|
518
|
+
});
|
|
438
519
|
const id = yield* nextId();
|
|
439
520
|
return yield* prepared(id, preparation.pipe(Effect.flatMap((input) => pure(() => {
|
|
440
521
|
const request = captureRequest(input, limits.prompt);
|
|
@@ -443,14 +524,15 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
443
524
|
});
|
|
444
525
|
const contract = yield* session.schema.pipe(Effect.flatMap(({ openapi }) => pure(() => validateDeployment(openapi))), Effect.timeoutOrElse({
|
|
445
526
|
duration: limits.setup,
|
|
446
|
-
orElse: () => Effect.fail(
|
|
527
|
+
orElse: () => Effect.fail(ReactorError.fromCode("Timeout", "H3 schema read timed out")),
|
|
447
528
|
}));
|
|
448
529
|
yield* refresh.pipe(Effect.timeoutOrElse({
|
|
449
530
|
duration: limits.setup,
|
|
450
|
-
orElse: () => Effect.fail(
|
|
531
|
+
orElse: () => Effect.fail(ReactorError.fromCode("Timeout", "H3 initial state and queue reads timed out")),
|
|
451
532
|
}));
|
|
452
533
|
if (state.snapshot()._tag !== "Ready")
|
|
453
|
-
return yield*
|
|
534
|
+
return yield* (fatalError ??
|
|
535
|
+
ReactorError.fromCode("InvalidState", "H3 initial observations are unavailable"));
|
|
454
536
|
const provider = {
|
|
455
537
|
sessionId: session.id,
|
|
456
538
|
contract,
|
|
@@ -458,44 +540,47 @@ const build = (session, options) => Effect.gen(function* () {
|
|
|
458
540
|
observe: (bounds) => Effect.gen(function* () {
|
|
459
541
|
const stream = yield* events.subscribe(bounds);
|
|
460
542
|
const initial = state.snapshot();
|
|
461
|
-
|
|
543
|
+
// The reducer may apply a queued event between the subscription and
|
|
544
|
+
// this read; the snapshot then covers it, so it is not repeated.
|
|
545
|
+
return {
|
|
546
|
+
initial,
|
|
547
|
+
revision: initial.revision,
|
|
548
|
+
events: Stream.filter(stream, (event) => !covered(event, initial.revision)),
|
|
549
|
+
};
|
|
462
550
|
}),
|
|
463
551
|
events: (bounds) => events.stream(bounds),
|
|
464
552
|
failure: Deferred.await(fatal),
|
|
465
553
|
acceptances: Effect.sync(() => Object.freeze([...acceptances.values()])),
|
|
466
554
|
acceptance: (id) => Effect.sync(() => acceptances.get(id)),
|
|
555
|
+
operation: (submission) => operations.attach(submission.id),
|
|
467
556
|
prepare,
|
|
468
557
|
prepareFrom,
|
|
469
|
-
enqueue: (request) => prepare(request).pipe(Effect.mapError((error) => localFailure("enqueue", error)), Effect.flatMap((submission) => submission.submit)),
|
|
558
|
+
enqueue: (request) => prepare(request).pipe(Effect.mapError((error) => ReactorError.is(error) ? localFailure("enqueue", error) : error), Effect.flatMap((submission) => submission.submit)),
|
|
470
559
|
getState,
|
|
471
560
|
getQueue,
|
|
472
561
|
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")))),
|
|
562
|
+
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
563
|
move: (id, position) => checked("move", () => ({
|
|
477
564
|
clip_id: clipId(id),
|
|
478
565
|
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")))),
|
|
566
|
+
})).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
567
|
play: (id) => checked("play", () => ({ clip_id: id === undefined ? "" : clipId(id) })).pipe(Effect.flatMap((args) => control("play", args))),
|
|
483
568
|
stop: control("stop", {}),
|
|
484
569
|
setSeed: (value) => checked("set_seed", () => nonnegative(value, "seed")).pipe(Effect.flatMap((seed) => named("set_seed", { seed }))),
|
|
485
570
|
setClipSeconds: (value) => checked("set_clip_seconds", () => seconds(value)).pipe(Effect.flatMap((seconds) => named("set_clip_seconds", { seconds }))),
|
|
486
571
|
setCanvas: (aspect) => checked("set_canvas", () => {
|
|
487
572
|
if (!Object.hasOwn(canvases, aspect))
|
|
488
|
-
throw
|
|
573
|
+
throw ReactorError.fromCode("InvalidInput", "Unsupported H3 canvas aspect");
|
|
489
574
|
return { aspect };
|
|
490
575
|
}).pipe(Effect.flatMap((args) => named("set_canvas", args))),
|
|
491
576
|
setAutoplay: (enabled) => checked("set_autoplay", () => {
|
|
492
577
|
if (typeof enabled !== "boolean")
|
|
493
|
-
throw
|
|
578
|
+
throw ReactorError.fromCode("InvalidInput", "Autoplay must be boolean");
|
|
494
579
|
return { enabled };
|
|
495
580
|
}).pipe(Effect.flatMap((args) => named("set_autoplay", args))),
|
|
496
581
|
setFlushOnClipEnd: (enabled) => checked("set_flush_on_clip_end", () => {
|
|
497
582
|
if (typeof enabled !== "boolean")
|
|
498
|
-
throw
|
|
583
|
+
throw ReactorError.fromCode("InvalidInput", "Flush setting must be boolean");
|
|
499
584
|
return { enabled };
|
|
500
585
|
}).pipe(Effect.flatMap((args) => named("set_flush_on_clip_end", args))),
|
|
501
586
|
reset: named("reset", {}),
|