@opengeni/core 0.21.10 → 0.24.1
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/dist/access/index.d.ts +1 -1
- package/dist/application/new-session-drafts.d.ts +1 -1
- package/dist/application/session-commands.d.ts +2 -2
- package/dist/billing/limits.d.ts +1 -1
- package/dist/chunk-6WKPWE5S.js +2380 -0
- package/dist/chunk-6WKPWE5S.js.map +1 -0
- package/dist/chunk-JJU6I5XD.js +2713 -0
- package/dist/chunk-JJU6I5XD.js.map +1 -0
- package/dist/chunk-NYPMQ7NO.js +2270 -0
- package/dist/chunk-NYPMQ7NO.js.map +1 -0
- package/dist/dependencies.d.ts +26 -6
- package/dist/domain/capabilities.d.ts +28 -3
- package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
- package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
- package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
- package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
- package/dist/domain/editable-artifacts/errors.d.ts +47 -0
- package/dist/domain/editable-artifacts/hash.d.ts +11 -0
- package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
- package/dist/domain/editable-artifacts/index.d.ts +14 -0
- package/dist/domain/editable-artifacts/ports.d.ts +341 -0
- package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
- package/dist/domain/editable-artifacts/service.d.ts +105 -0
- package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
- package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
- package/dist/domain/editable-artifacts/types.d.ts +410 -0
- package/dist/domain/memory-slack-delivery.d.ts +19 -0
- package/dist/domain/packs.d.ts +29 -1
- package/dist/domain/personal-connection-delegations.d.ts +9 -1
- package/dist/domain/scheduled-tasks.d.ts +2 -2
- package/dist/domain/session-tool-policy.d.ts +2 -2
- package/dist/domain/sessions.d.ts +14 -6
- package/dist/domain/skill-imports.d.ts +32 -0
- package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
- package/dist/domain/timeline-annotations.d.ts +5 -0
- package/dist/domain/video-generation-capabilities.d.ts +9 -0
- package/dist/domain/video-generation.d.ts +47 -0
- package/dist/editable-artifact-live/application.d.ts +69 -0
- package/dist/editable-artifact-live/errors.d.ts +1 -0
- package/dist/editable-artifact-live/index.d.ts +8 -0
- package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
- package/dist/editable-artifact-live/ports.d.ts +100 -0
- package/dist/editable-artifact-live/server.d.ts +71 -0
- package/dist/editable-artifact-live/ticket.d.ts +40 -0
- package/dist/editable-artifact-live/types.d.ts +213 -0
- package/dist/editable-artifact-live/wire.d.ts +34 -0
- package/dist/editable-artifact-live.d.ts +2 -0
- package/dist/editable-artifact-live.js +58 -0
- package/dist/editable-artifact-live.js.map +1 -0
- package/dist/editable-artifacts.d.ts +2 -0
- package/dist/editable-artifacts.js +179 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/index.d.ts +35 -26
- package/dist/index.js +2792 -609
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +1 -1
- package/dist/rigs/index.d.ts +1 -1
- package/dist/rigs/provider-images.d.ts +31 -0
- package/dist/sandbox/routing.d.ts +1 -0
- package/dist/session-authorization.d.ts +1 -1
- package/dist/workflow-wake-contract.d.ts +7 -0
- package/package.json +18 -10
- package/src/application/new-session-drafts.ts +2 -0
- package/src/application/session-commands.ts +188 -107
- package/src/dependencies.ts +30 -2
- package/src/domain/capabilities.ts +604 -220
- package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
- package/src/domain/durable-learning-slack-publication.ts +202 -0
- package/src/domain/editable-artifacts/agent-application.ts +687 -0
- package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
- package/src/domain/editable-artifacts/durable-export.ts +734 -0
- package/src/domain/editable-artifacts/errors.ts +115 -0
- package/src/domain/editable-artifacts/hash.ts +131 -0
- package/src/domain/editable-artifacts/in-memory.ts +1662 -0
- package/src/domain/editable-artifacts/index.ts +14 -0
- package/src/domain/editable-artifacts/ports.ts +462 -0
- package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
- package/src/domain/editable-artifacts/service.ts +2402 -0
- package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
- package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
- package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
- package/src/domain/editable-artifacts/types.ts +835 -0
- package/src/domain/insights.ts +136 -33
- package/src/domain/memory-slack-delivery.ts +204 -0
- package/src/domain/memory-slack-publication.ts +5 -2
- package/src/domain/packs.ts +266 -5
- package/src/domain/personal-connection-delegations.ts +97 -6
- package/src/domain/scheduled-tasks.ts +155 -29
- package/src/domain/session-tool-policy.ts +8 -2
- package/src/domain/sessions.ts +274 -131
- package/src/domain/skill-imports.ts +350 -0
- package/src/domain/slack-publication-secret-safety.ts +42 -0
- package/src/domain/timeline-annotations.ts +204 -0
- package/src/domain/video-generation-capabilities.ts +69 -0
- package/src/domain/video-generation.ts +249 -0
- package/src/editable-artifact-live/application.ts +182 -0
- package/src/editable-artifact-live/errors.ts +4 -0
- package/src/editable-artifact-live/index.ts +8 -0
- package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
- package/src/editable-artifact-live/ports.ts +139 -0
- package/src/editable-artifact-live/server.ts +1707 -0
- package/src/editable-artifact-live/ticket.ts +217 -0
- package/src/editable-artifact-live/types.ts +286 -0
- package/src/editable-artifact-live/wire.ts +95 -0
- package/src/editable-artifact-live.ts +2 -0
- package/src/editable-artifacts.ts +2 -0
- package/src/index.ts +13 -0
- package/src/rigs/index.ts +26 -19
- package/src/rigs/provider-images.ts +110 -0
- package/src/sandbox/routing.ts +6 -3
- package/src/session-authorization.ts +3 -0
- package/src/workflow-wake-contract.ts +8 -0
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertNonnegativeSafeInteger,
|
|
3
|
+
assertPositiveSafeInteger,
|
|
4
|
+
editableArtifactId,
|
|
5
|
+
editableArtifactOutboxId,
|
|
6
|
+
editableArtifactScope,
|
|
7
|
+
editableArtifactSnapshotId,
|
|
8
|
+
editableArtifactStateHash,
|
|
9
|
+
type EditableArtifactLiveOutboxRecord,
|
|
10
|
+
type EditableArtifactModality,
|
|
11
|
+
type EditableArtifactOutboxId,
|
|
12
|
+
type EditableArtifactScope,
|
|
13
|
+
} from "../domain/editable-artifacts/types";
|
|
14
|
+
import type { EditableArtifactLiveClockPort, EditableArtifactLiveSchedulerPort } from "./ports";
|
|
15
|
+
|
|
16
|
+
const textEncoder = new TextEncoder();
|
|
17
|
+
|
|
18
|
+
export const EDITABLE_ARTIFACT_HINT_MAX_BYTES = 16 * 1024;
|
|
19
|
+
export const EDITABLE_ARTIFACT_HINT_SUBJECT_PREFIX = "editable_artifacts.v1";
|
|
20
|
+
|
|
21
|
+
type EditableArtifactBrokerHeadHintCommon = Readonly<{
|
|
22
|
+
kind: "head_advanced";
|
|
23
|
+
schemaVersion: 1;
|
|
24
|
+
scope: EditableArtifactScope;
|
|
25
|
+
artifactId: string;
|
|
26
|
+
modality: EditableArtifactModality;
|
|
27
|
+
headSequence: number;
|
|
28
|
+
stateHash: string;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
type EditableArtifactBrokerSnapshotHintCommon = Readonly<{
|
|
32
|
+
kind: "snapshot_available";
|
|
33
|
+
schemaVersion: 1;
|
|
34
|
+
scope: EditableArtifactScope;
|
|
35
|
+
artifactId: string;
|
|
36
|
+
modality: EditableArtifactModality;
|
|
37
|
+
headSequence: number;
|
|
38
|
+
stateHash: string;
|
|
39
|
+
snapshotId: string;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
export type EditableArtifactBrokerHint =
|
|
43
|
+
| (EditableArtifactBrokerHeadHintCommon &
|
|
44
|
+
Readonly<{ modality: "spreadsheet"; operationProtocolVersion: number }>)
|
|
45
|
+
| (EditableArtifactBrokerHeadHintCommon &
|
|
46
|
+
Readonly<{
|
|
47
|
+
modality: "document" | "presentation";
|
|
48
|
+
commitProtocolVersion: number;
|
|
49
|
+
}>)
|
|
50
|
+
| (EditableArtifactBrokerSnapshotHintCommon &
|
|
51
|
+
Readonly<{ modality: "spreadsheet"; operationProtocolVersion: number }>)
|
|
52
|
+
| (EditableArtifactBrokerSnapshotHintCommon &
|
|
53
|
+
Readonly<{ modality: "document" | "presentation" }>);
|
|
54
|
+
|
|
55
|
+
export type EditableArtifactEncodedBrokerHint = Readonly<{
|
|
56
|
+
subject: string;
|
|
57
|
+
payload: Uint8Array;
|
|
58
|
+
hint: EditableArtifactBrokerHint;
|
|
59
|
+
}>;
|
|
60
|
+
|
|
61
|
+
export interface EditableArtifactOutboxDispatcherStorePort {
|
|
62
|
+
claimLiveOutbox(input: {
|
|
63
|
+
owner: string;
|
|
64
|
+
leaseDurationMs: number;
|
|
65
|
+
limit: number;
|
|
66
|
+
}): Promise<readonly EditableArtifactLiveOutboxRecord[]>;
|
|
67
|
+
renewLiveOutbox(input: {
|
|
68
|
+
outboxId: EditableArtifactOutboxId;
|
|
69
|
+
owner: string;
|
|
70
|
+
attemptCount: number;
|
|
71
|
+
leaseDurationMs: number;
|
|
72
|
+
}): Promise<void>;
|
|
73
|
+
markLiveOutboxPublished(input: {
|
|
74
|
+
outboxId: EditableArtifactOutboxId;
|
|
75
|
+
owner: string;
|
|
76
|
+
attemptCount: number;
|
|
77
|
+
}): Promise<void>;
|
|
78
|
+
retryLiveOutbox(input: {
|
|
79
|
+
outboxId: EditableArtifactOutboxId;
|
|
80
|
+
owner: string;
|
|
81
|
+
attemptCount: number;
|
|
82
|
+
retryDelayMs: number;
|
|
83
|
+
errorCode: EditableArtifactOutboxRetryErrorCode;
|
|
84
|
+
}): Promise<void>;
|
|
85
|
+
deadLetterLiveOutbox(input: {
|
|
86
|
+
outboxId: EditableArtifactOutboxId;
|
|
87
|
+
owner: string;
|
|
88
|
+
attemptCount: number;
|
|
89
|
+
errorCode: EditableArtifactOutboxDeadLetterErrorCode;
|
|
90
|
+
}): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Resolves only after the broker has acknowledged every preceding publish. */
|
|
94
|
+
export interface EditableArtifactHintBrokerPort {
|
|
95
|
+
publish(
|
|
96
|
+
input: Readonly<{
|
|
97
|
+
subject: string;
|
|
98
|
+
payload: Uint8Array;
|
|
99
|
+
signal: AbortSignal;
|
|
100
|
+
}>,
|
|
101
|
+
): Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface EditableArtifactOutboxRandomPort {
|
|
105
|
+
unit(): number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type EditableArtifactOutboxMetricOutcome =
|
|
109
|
+
| "claimed"
|
|
110
|
+
| "published"
|
|
111
|
+
| "retried"
|
|
112
|
+
| "dead_lettered"
|
|
113
|
+
| "lease_lost"
|
|
114
|
+
| "claim_failed"
|
|
115
|
+
| "mark_failed"
|
|
116
|
+
| "retry_store_failed"
|
|
117
|
+
| "dead_letter_store_failed";
|
|
118
|
+
|
|
119
|
+
/** Labels are closed unions; artifact/workspace identities must never become metric labels. */
|
|
120
|
+
export interface EditableArtifactOutboxMetricsPort {
|
|
121
|
+
increment(outcome: EditableArtifactOutboxMetricOutcome, count?: number): void;
|
|
122
|
+
observePublishSeconds(outcome: "acked" | "failed" | "timed_out", seconds: number): void;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface EditableArtifactOutboxLoggerPort {
|
|
126
|
+
warn(message: string, attributes: Readonly<Record<string, unknown>>): void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type EditableArtifactOutboxRetryErrorCode =
|
|
130
|
+
| "broker_unavailable"
|
|
131
|
+
| "broker_backpressure"
|
|
132
|
+
| "publish_timeout";
|
|
133
|
+
|
|
134
|
+
export type EditableArtifactOutboxDeadLetterErrorCode = "invalid_hint" | "oversized_hint";
|
|
135
|
+
|
|
136
|
+
export type EditableArtifactOutboxDispatcherOptions = Readonly<{
|
|
137
|
+
owner: string;
|
|
138
|
+
batchSize?: number;
|
|
139
|
+
concurrency?: number;
|
|
140
|
+
leaseDurationMs?: number;
|
|
141
|
+
leaseRenewIntervalMs?: number;
|
|
142
|
+
pollIntervalMs?: number;
|
|
143
|
+
publishTimeoutMs?: number;
|
|
144
|
+
retryBaseMs?: number;
|
|
145
|
+
retryMaxMs?: number;
|
|
146
|
+
retryJitterRatio?: number;
|
|
147
|
+
claimBackoffBaseMs?: number;
|
|
148
|
+
claimBackoffMaxMs?: number;
|
|
149
|
+
maxHintBytes?: number;
|
|
150
|
+
}>;
|
|
151
|
+
|
|
152
|
+
export type EditableArtifactOutboxDispatcherDependencies = Readonly<{
|
|
153
|
+
store: EditableArtifactOutboxDispatcherStorePort;
|
|
154
|
+
broker: EditableArtifactHintBrokerPort;
|
|
155
|
+
clock: EditableArtifactLiveClockPort;
|
|
156
|
+
scheduler: EditableArtifactLiveSchedulerPort;
|
|
157
|
+
random: EditableArtifactOutboxRandomPort;
|
|
158
|
+
metrics?: EditableArtifactOutboxMetricsPort;
|
|
159
|
+
logger?: EditableArtifactOutboxLoggerPort;
|
|
160
|
+
}>;
|
|
161
|
+
|
|
162
|
+
export type EditableArtifactOutboxDispatchSummary = Readonly<{
|
|
163
|
+
claimed: number;
|
|
164
|
+
published: number;
|
|
165
|
+
retried: number;
|
|
166
|
+
deadLettered: number;
|
|
167
|
+
leaseLost: number;
|
|
168
|
+
markFailed: number;
|
|
169
|
+
claimFailed: boolean;
|
|
170
|
+
}>;
|
|
171
|
+
|
|
172
|
+
type NormalizedOptions = Required<EditableArtifactOutboxDispatcherOptions>;
|
|
173
|
+
type MutableSummary = {
|
|
174
|
+
claimed: number;
|
|
175
|
+
published: number;
|
|
176
|
+
retried: number;
|
|
177
|
+
deadLettered: number;
|
|
178
|
+
leaseLost: number;
|
|
179
|
+
markFailed: number;
|
|
180
|
+
claimFailed: boolean;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const DEFAULTS = Object.freeze({
|
|
184
|
+
batchSize: 32,
|
|
185
|
+
concurrency: 8,
|
|
186
|
+
leaseDurationMs: 30_000,
|
|
187
|
+
leaseRenewIntervalMs: 10_000,
|
|
188
|
+
pollIntervalMs: 500,
|
|
189
|
+
publishTimeoutMs: 5_000,
|
|
190
|
+
retryBaseMs: 500,
|
|
191
|
+
retryMaxMs: 60_000,
|
|
192
|
+
retryJitterRatio: 0.2,
|
|
193
|
+
claimBackoffBaseMs: 500,
|
|
194
|
+
claimBackoffMaxMs: 30_000,
|
|
195
|
+
maxHintBytes: EDITABLE_ARTIFACT_HINT_MAX_BYTES,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export class EditableArtifactLiveOutboxDispatcher {
|
|
199
|
+
private readonly options: NormalizedOptions;
|
|
200
|
+
private activeDispatch: Promise<EditableArtifactOutboxDispatchSummary> | null = null;
|
|
201
|
+
|
|
202
|
+
constructor(
|
|
203
|
+
private readonly dependencies: EditableArtifactOutboxDispatcherDependencies,
|
|
204
|
+
options: EditableArtifactOutboxDispatcherOptions,
|
|
205
|
+
) {
|
|
206
|
+
this.options = normalizeOptions(options);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Long-running resilient loop. Abort stops new claims; an already-claimed
|
|
211
|
+
* bounded batch drains before this promise resolves.
|
|
212
|
+
*/
|
|
213
|
+
async run(signal: AbortSignal): Promise<void> {
|
|
214
|
+
let consecutiveClaimFailures = 0;
|
|
215
|
+
while (!signal.aborted) {
|
|
216
|
+
const summary = await this.dispatchOnce();
|
|
217
|
+
if (summary.claimFailed) consecutiveClaimFailures += 1;
|
|
218
|
+
else consecutiveClaimFailures = 0;
|
|
219
|
+
if (signal.aborted) break;
|
|
220
|
+
const delay = summary.claimFailed
|
|
221
|
+
? exponentialDelay(
|
|
222
|
+
consecutiveClaimFailures,
|
|
223
|
+
this.options.claimBackoffBaseMs,
|
|
224
|
+
this.options.claimBackoffMaxMs,
|
|
225
|
+
this.options.retryJitterRatio,
|
|
226
|
+
this.dependencies.random,
|
|
227
|
+
)
|
|
228
|
+
: summary.claimed === 0
|
|
229
|
+
? this.options.pollIntervalMs
|
|
230
|
+
: 0;
|
|
231
|
+
if (delay > 0 && !(await sleepUntilNextPoll(this.dependencies.scheduler, delay, signal))) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
await this.drain();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Concurrent callers share one bounded claim/dispatch pass. */
|
|
239
|
+
dispatchOnce(): Promise<EditableArtifactOutboxDispatchSummary> {
|
|
240
|
+
if (this.activeDispatch) return this.activeDispatch;
|
|
241
|
+
const dispatch = this.dispatchBatch();
|
|
242
|
+
this.activeDispatch = dispatch;
|
|
243
|
+
void dispatch.then(
|
|
244
|
+
() => {
|
|
245
|
+
if (this.activeDispatch === dispatch) this.activeDispatch = null;
|
|
246
|
+
},
|
|
247
|
+
() => {
|
|
248
|
+
if (this.activeDispatch === dispatch) this.activeDispatch = null;
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
return dispatch;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async drain(): Promise<void> {
|
|
255
|
+
await this.activeDispatch;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async dispatchBatch(): Promise<EditableArtifactOutboxDispatchSummary> {
|
|
259
|
+
const summary = mutableSummary();
|
|
260
|
+
let records: readonly EditableArtifactLiveOutboxRecord[];
|
|
261
|
+
try {
|
|
262
|
+
records = await this.dependencies.store.claimLiveOutbox({
|
|
263
|
+
owner: this.options.owner,
|
|
264
|
+
leaseDurationMs: this.options.leaseDurationMs,
|
|
265
|
+
limit: this.options.batchSize,
|
|
266
|
+
});
|
|
267
|
+
if (!Array.isArray(records) || records.length > this.options.batchSize) {
|
|
268
|
+
throw new TypeError("Outbox store returned an invalid claim batch");
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
summary.claimFailed = true;
|
|
272
|
+
this.metric("claim_failed");
|
|
273
|
+
this.warn("Editable artifact outbox claim failed", "claim_failed", error);
|
|
274
|
+
return freezeSummary(summary);
|
|
275
|
+
}
|
|
276
|
+
summary.claimed = records.length;
|
|
277
|
+
if (records.length > 0) this.metric("claimed", records.length);
|
|
278
|
+
let cursor = 0;
|
|
279
|
+
const worker = async () => {
|
|
280
|
+
for (;;) {
|
|
281
|
+
const index = cursor;
|
|
282
|
+
cursor += 1;
|
|
283
|
+
const record = records[index];
|
|
284
|
+
if (!record) return;
|
|
285
|
+
await this.dispatchRecord(record, summary);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
await Promise.all(
|
|
289
|
+
Array.from(
|
|
290
|
+
{ length: Math.min(records.length, this.options.concurrency) },
|
|
291
|
+
async () => await worker(),
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
return freezeSummary(summary);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private async dispatchRecord(
|
|
298
|
+
record: EditableArtifactLiveOutboxRecord,
|
|
299
|
+
summary: MutableSummary,
|
|
300
|
+
): Promise<void> {
|
|
301
|
+
let outboxId: EditableArtifactOutboxId;
|
|
302
|
+
try {
|
|
303
|
+
outboxId = validateClaimedRecord(record, this.options.owner);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
await this.deadLetter(record, "invalid_hint", summary, error);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
let encoded: EditableArtifactEncodedBrokerHint;
|
|
309
|
+
try {
|
|
310
|
+
encoded = encodeEditableArtifactBrokerHint(record, this.options.maxHintBytes);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
await this.deadLetter(
|
|
313
|
+
record,
|
|
314
|
+
error instanceof EditableArtifactHintEncodingError && error.code === "oversized_hint"
|
|
315
|
+
? "oversized_hint"
|
|
316
|
+
: "invalid_hint",
|
|
317
|
+
summary,
|
|
318
|
+
error,
|
|
319
|
+
);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const publishAbort = new AbortController();
|
|
324
|
+
const renewalAbort = new AbortController();
|
|
325
|
+
let leaseLost = false;
|
|
326
|
+
const renewal = this.renewWhilePublishing(record, renewalAbort.signal, publishAbort, () => {
|
|
327
|
+
leaseLost = true;
|
|
328
|
+
}).catch((error: unknown) => {
|
|
329
|
+
leaseLost = true;
|
|
330
|
+
publishAbort.abort();
|
|
331
|
+
this.warn("Editable artifact outbox renewal loop failed", "lease_lost", error);
|
|
332
|
+
});
|
|
333
|
+
let publishError: unknown = null;
|
|
334
|
+
let timedOut = false;
|
|
335
|
+
const startedAt = this.dependencies.clock.now().getTime();
|
|
336
|
+
try {
|
|
337
|
+
await withTimeout(
|
|
338
|
+
this.dependencies.broker.publish({
|
|
339
|
+
subject: encoded.subject,
|
|
340
|
+
payload: encoded.payload.slice(),
|
|
341
|
+
signal: publishAbort.signal,
|
|
342
|
+
}),
|
|
343
|
+
this.options.publishTimeoutMs,
|
|
344
|
+
this.dependencies.scheduler,
|
|
345
|
+
publishAbort,
|
|
346
|
+
() => {
|
|
347
|
+
timedOut = true;
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
publishError = error;
|
|
352
|
+
} finally {
|
|
353
|
+
renewalAbort.abort();
|
|
354
|
+
await renewal;
|
|
355
|
+
this.observePublish(timedOut ? "timed_out" : publishError ? "failed" : "acked", startedAt);
|
|
356
|
+
}
|
|
357
|
+
if (leaseLost) {
|
|
358
|
+
summary.leaseLost += 1;
|
|
359
|
+
this.metric("lease_lost");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (publishError) {
|
|
363
|
+
const errorCode: EditableArtifactOutboxRetryErrorCode = timedOut
|
|
364
|
+
? "publish_timeout"
|
|
365
|
+
: brokerErrorCode(publishError);
|
|
366
|
+
await this.retry(record, errorCode, summary, publishError);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Extend once after broker ACK so the fenced mark cannot race a lease that
|
|
371
|
+
// was nearly exhausted during publication.
|
|
372
|
+
try {
|
|
373
|
+
await this.dependencies.store.renewLiveOutbox({
|
|
374
|
+
outboxId,
|
|
375
|
+
owner: this.options.owner,
|
|
376
|
+
attemptCount: record.attemptCount,
|
|
377
|
+
leaseDurationMs: this.options.leaseDurationMs,
|
|
378
|
+
});
|
|
379
|
+
} catch (error) {
|
|
380
|
+
summary.leaseLost += 1;
|
|
381
|
+
this.metric("lease_lost");
|
|
382
|
+
this.warn("Editable artifact outbox lease was lost after broker ACK", "lease_lost", error);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
await this.dependencies.store.markLiveOutboxPublished({
|
|
387
|
+
outboxId,
|
|
388
|
+
owner: this.options.owner,
|
|
389
|
+
attemptCount: record.attemptCount,
|
|
390
|
+
});
|
|
391
|
+
summary.published += 1;
|
|
392
|
+
this.metric("published");
|
|
393
|
+
} catch (error) {
|
|
394
|
+
// The publish may have succeeded. Never turn an unknown mark outcome into
|
|
395
|
+
// an eager retry; lease expiry safely permits an at-least-once duplicate.
|
|
396
|
+
summary.markFailed += 1;
|
|
397
|
+
this.metric("mark_failed");
|
|
398
|
+
this.warn("Editable artifact outbox publish mark failed", "mark_failed", error);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private async renewWhilePublishing(
|
|
403
|
+
record: EditableArtifactLiveOutboxRecord,
|
|
404
|
+
signal: AbortSignal,
|
|
405
|
+
publishAbort: AbortController,
|
|
406
|
+
onLeaseLost: () => void,
|
|
407
|
+
): Promise<void> {
|
|
408
|
+
for (;;) {
|
|
409
|
+
if (
|
|
410
|
+
!(await sleepUntilNextPoll(
|
|
411
|
+
this.dependencies.scheduler,
|
|
412
|
+
this.options.leaseRenewIntervalMs,
|
|
413
|
+
signal,
|
|
414
|
+
))
|
|
415
|
+
) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
await this.dependencies.store.renewLiveOutbox({
|
|
420
|
+
outboxId: editableArtifactOutboxId(record.outboxId),
|
|
421
|
+
owner: this.options.owner,
|
|
422
|
+
attemptCount: record.attemptCount,
|
|
423
|
+
leaseDurationMs: this.options.leaseDurationMs,
|
|
424
|
+
});
|
|
425
|
+
} catch (error) {
|
|
426
|
+
onLeaseLost();
|
|
427
|
+
publishAbort.abort();
|
|
428
|
+
this.warn("Editable artifact outbox lease renewal failed", "lease_lost", error);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private async retry(
|
|
435
|
+
record: EditableArtifactLiveOutboxRecord,
|
|
436
|
+
errorCode: EditableArtifactOutboxRetryErrorCode,
|
|
437
|
+
summary: MutableSummary,
|
|
438
|
+
error: unknown,
|
|
439
|
+
): Promise<void> {
|
|
440
|
+
const retryDelayMs = exponentialDelay(
|
|
441
|
+
record.attemptCount,
|
|
442
|
+
this.options.retryBaseMs,
|
|
443
|
+
this.options.retryMaxMs,
|
|
444
|
+
this.options.retryJitterRatio,
|
|
445
|
+
this.dependencies.random,
|
|
446
|
+
);
|
|
447
|
+
try {
|
|
448
|
+
await this.dependencies.store.retryLiveOutbox({
|
|
449
|
+
outboxId: editableArtifactOutboxId(record.outboxId),
|
|
450
|
+
owner: this.options.owner,
|
|
451
|
+
attemptCount: record.attemptCount,
|
|
452
|
+
retryDelayMs,
|
|
453
|
+
errorCode,
|
|
454
|
+
});
|
|
455
|
+
summary.retried += 1;
|
|
456
|
+
this.metric("retried");
|
|
457
|
+
} catch (retryError) {
|
|
458
|
+
this.metric("retry_store_failed");
|
|
459
|
+
this.warn(
|
|
460
|
+
"Editable artifact outbox retry persistence failed",
|
|
461
|
+
"retry_store_failed",
|
|
462
|
+
retryError,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
this.warn("Editable artifact hint publish failed", errorCode, error);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private async deadLetter(
|
|
469
|
+
record: EditableArtifactLiveOutboxRecord,
|
|
470
|
+
errorCode: EditableArtifactOutboxDeadLetterErrorCode,
|
|
471
|
+
summary: MutableSummary,
|
|
472
|
+
error: unknown,
|
|
473
|
+
): Promise<void> {
|
|
474
|
+
try {
|
|
475
|
+
await this.dependencies.store.deadLetterLiveOutbox({
|
|
476
|
+
outboxId: editableArtifactOutboxId(record.outboxId),
|
|
477
|
+
owner: this.options.owner,
|
|
478
|
+
attemptCount: record.attemptCount,
|
|
479
|
+
errorCode,
|
|
480
|
+
});
|
|
481
|
+
summary.deadLettered += 1;
|
|
482
|
+
this.metric("dead_lettered");
|
|
483
|
+
} catch (deadLetterError) {
|
|
484
|
+
this.metric("dead_letter_store_failed");
|
|
485
|
+
this.warn(
|
|
486
|
+
"Editable artifact outbox dead-letter persistence failed",
|
|
487
|
+
"dead_letter_store_failed",
|
|
488
|
+
deadLetterError,
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
this.warn("Editable artifact outbox contains an undeliverable hint", errorCode, error);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private metric(outcome: EditableArtifactOutboxMetricOutcome, count = 1): void {
|
|
495
|
+
try {
|
|
496
|
+
this.dependencies.metrics?.increment(outcome, count);
|
|
497
|
+
} catch {
|
|
498
|
+
// Metrics cannot affect delivery.
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private observePublish(outcome: "acked" | "failed" | "timed_out", startedAt: number): void {
|
|
503
|
+
let elapsed = 0;
|
|
504
|
+
const now = this.dependencies.clock.now().getTime();
|
|
505
|
+
if (Number.isFinite(now) && Number.isFinite(startedAt)) elapsed = Math.max(0, now - startedAt);
|
|
506
|
+
try {
|
|
507
|
+
this.dependencies.metrics?.observePublishSeconds(outcome, elapsed / 1_000);
|
|
508
|
+
} catch {
|
|
509
|
+
// Metrics cannot affect delivery.
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private warn(message: string, code: string, error: unknown): void {
|
|
514
|
+
try {
|
|
515
|
+
this.dependencies.logger?.warn(message, {
|
|
516
|
+
code,
|
|
517
|
+
errorClass: error instanceof Error ? error.name : "UnknownError",
|
|
518
|
+
});
|
|
519
|
+
} catch {
|
|
520
|
+
// Logging cannot affect delivery.
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export class EditableArtifactHintEncodingError extends Error {
|
|
526
|
+
constructor(
|
|
527
|
+
readonly code: EditableArtifactOutboxDeadLetterErrorCode,
|
|
528
|
+
message: string,
|
|
529
|
+
) {
|
|
530
|
+
super(message);
|
|
531
|
+
this.name = "EditableArtifactHintEncodingError";
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function encodeEditableArtifactBrokerHint(
|
|
536
|
+
record: EditableArtifactLiveOutboxRecord,
|
|
537
|
+
maxBytes = EDITABLE_ARTIFACT_HINT_MAX_BYTES,
|
|
538
|
+
): EditableArtifactEncodedBrokerHint {
|
|
539
|
+
assertPositiveSafeInteger(maxBytes, "maximum broker hint bytes");
|
|
540
|
+
let hint: EditableArtifactBrokerHint;
|
|
541
|
+
try {
|
|
542
|
+
const event = record.event;
|
|
543
|
+
const scope = editableArtifactScope(event.scope);
|
|
544
|
+
const artifactId = editableArtifactId(event.artifactId);
|
|
545
|
+
const stateHash = editableArtifactStateHash(event.stateHash);
|
|
546
|
+
if (event.schemaVersion !== 1) throw new TypeError("Unsupported artifact hint schema");
|
|
547
|
+
if (event.kind === "transaction_committed") {
|
|
548
|
+
assertPositiveSafeInteger(event.sequenceEnd, "hint head sequence");
|
|
549
|
+
const common = {
|
|
550
|
+
kind: "head_advanced",
|
|
551
|
+
schemaVersion: 1,
|
|
552
|
+
scope,
|
|
553
|
+
artifactId,
|
|
554
|
+
modality: event.modality,
|
|
555
|
+
headSequence: event.sequenceEnd,
|
|
556
|
+
stateHash,
|
|
557
|
+
} as const;
|
|
558
|
+
if (event.modality === "spreadsheet") {
|
|
559
|
+
assertPositiveSafeInteger(
|
|
560
|
+
event.operationProtocolVersion,
|
|
561
|
+
"hint operation protocol version",
|
|
562
|
+
);
|
|
563
|
+
hint = Object.freeze({
|
|
564
|
+
...common,
|
|
565
|
+
modality: "spreadsheet",
|
|
566
|
+
operationProtocolVersion: event.operationProtocolVersion,
|
|
567
|
+
});
|
|
568
|
+
} else {
|
|
569
|
+
assertPositiveSafeInteger(event.commitProtocolVersion, "hint commit protocol version");
|
|
570
|
+
hint = Object.freeze({
|
|
571
|
+
...common,
|
|
572
|
+
modality: event.modality,
|
|
573
|
+
commitProtocolVersion: event.commitProtocolVersion,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
} else if (event.kind === "snapshot_published") {
|
|
577
|
+
assertNonnegativeSafeInteger(event.coveredHeadSequence, "snapshot hint head sequence");
|
|
578
|
+
const common = {
|
|
579
|
+
kind: "snapshot_available",
|
|
580
|
+
schemaVersion: 1,
|
|
581
|
+
scope,
|
|
582
|
+
artifactId,
|
|
583
|
+
modality: event.modality,
|
|
584
|
+
headSequence: event.coveredHeadSequence,
|
|
585
|
+
stateHash,
|
|
586
|
+
snapshotId: editableArtifactSnapshotId(event.snapshotId),
|
|
587
|
+
} as const;
|
|
588
|
+
if (event.modality === "spreadsheet") {
|
|
589
|
+
assertPositiveSafeInteger(
|
|
590
|
+
event.operationProtocolVersion,
|
|
591
|
+
"hint operation protocol version",
|
|
592
|
+
);
|
|
593
|
+
hint = Object.freeze({
|
|
594
|
+
...common,
|
|
595
|
+
modality: "spreadsheet",
|
|
596
|
+
operationProtocolVersion: event.operationProtocolVersion,
|
|
597
|
+
});
|
|
598
|
+
} else {
|
|
599
|
+
hint = Object.freeze({ ...common, modality: event.modality });
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
throw new TypeError("Unknown editable artifact outbox event");
|
|
603
|
+
}
|
|
604
|
+
const payload = textEncoder.encode(JSON.stringify(hint));
|
|
605
|
+
if (payload.byteLength > maxBytes) {
|
|
606
|
+
throw new EditableArtifactHintEncodingError(
|
|
607
|
+
"oversized_hint",
|
|
608
|
+
"Editable artifact broker hint exceeds its byte limit",
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
return Object.freeze({
|
|
612
|
+
subject: editableArtifactLiveHintSubject(scope, artifactId),
|
|
613
|
+
payload,
|
|
614
|
+
hint,
|
|
615
|
+
});
|
|
616
|
+
} catch (error) {
|
|
617
|
+
if (error instanceof EditableArtifactHintEncodingError) throw error;
|
|
618
|
+
throw new EditableArtifactHintEncodingError(
|
|
619
|
+
"invalid_hint",
|
|
620
|
+
"Editable artifact outbox event cannot be projected to a broker hint",
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function editableArtifactLiveHintSubject(
|
|
626
|
+
scope: EditableArtifactScope,
|
|
627
|
+
artifactIdInput: string,
|
|
628
|
+
): string {
|
|
629
|
+
const normalized = editableArtifactScope(scope);
|
|
630
|
+
const artifactId = editableArtifactId(artifactIdInput);
|
|
631
|
+
const subject = [
|
|
632
|
+
EDITABLE_ARTIFACT_HINT_SUBJECT_PREFIX,
|
|
633
|
+
base64Url(textEncoder.encode(normalized.accountId)),
|
|
634
|
+
base64Url(textEncoder.encode(normalized.workspaceId)),
|
|
635
|
+
artifactId,
|
|
636
|
+
].join(".");
|
|
637
|
+
if (textEncoder.encode(subject).byteLength > 1_024) {
|
|
638
|
+
throw new EditableArtifactHintEncodingError(
|
|
639
|
+
"invalid_hint",
|
|
640
|
+
"Editable artifact hint subject exceeds its byte limit",
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
return subject;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function decodeEditableArtifactBrokerHint(
|
|
647
|
+
payload: Uint8Array,
|
|
648
|
+
expected: Readonly<{ scope: EditableArtifactScope; artifactId: string }>,
|
|
649
|
+
maxBytes = EDITABLE_ARTIFACT_HINT_MAX_BYTES,
|
|
650
|
+
): EditableArtifactBrokerHint {
|
|
651
|
+
if (!(payload instanceof Uint8Array) || payload.byteLength < 2 || payload.byteLength > maxBytes) {
|
|
652
|
+
throw new EditableArtifactHintEncodingError(
|
|
653
|
+
"oversized_hint",
|
|
654
|
+
"Editable artifact broker hint exceeds its byte limit",
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(payload);
|
|
659
|
+
const value = JSON.parse(text) as unknown;
|
|
660
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
661
|
+
throw new TypeError("hint must be an object");
|
|
662
|
+
}
|
|
663
|
+
const record = value as Record<string, unknown>;
|
|
664
|
+
if (JSON.stringify(record) !== text) throw new TypeError("hint must be canonical JSON");
|
|
665
|
+
const kind = record.kind;
|
|
666
|
+
const modality = record.modality;
|
|
667
|
+
if (modality !== "spreadsheet" && modality !== "document" && modality !== "presentation") {
|
|
668
|
+
throw new TypeError("hint modality invalid");
|
|
669
|
+
}
|
|
670
|
+
const keys = Object.keys(record);
|
|
671
|
+
const expectedKeys =
|
|
672
|
+
kind === "head_advanced"
|
|
673
|
+
? modality === "spreadsheet"
|
|
674
|
+
? [
|
|
675
|
+
"kind",
|
|
676
|
+
"schemaVersion",
|
|
677
|
+
"scope",
|
|
678
|
+
"artifactId",
|
|
679
|
+
"modality",
|
|
680
|
+
"headSequence",
|
|
681
|
+
"stateHash",
|
|
682
|
+
"operationProtocolVersion",
|
|
683
|
+
]
|
|
684
|
+
: [
|
|
685
|
+
"kind",
|
|
686
|
+
"schemaVersion",
|
|
687
|
+
"scope",
|
|
688
|
+
"artifactId",
|
|
689
|
+
"modality",
|
|
690
|
+
"headSequence",
|
|
691
|
+
"stateHash",
|
|
692
|
+
"commitProtocolVersion",
|
|
693
|
+
]
|
|
694
|
+
: modality === "spreadsheet"
|
|
695
|
+
? [
|
|
696
|
+
"kind",
|
|
697
|
+
"schemaVersion",
|
|
698
|
+
"scope",
|
|
699
|
+
"artifactId",
|
|
700
|
+
"modality",
|
|
701
|
+
"headSequence",
|
|
702
|
+
"stateHash",
|
|
703
|
+
"snapshotId",
|
|
704
|
+
"operationProtocolVersion",
|
|
705
|
+
]
|
|
706
|
+
: [
|
|
707
|
+
"kind",
|
|
708
|
+
"schemaVersion",
|
|
709
|
+
"scope",
|
|
710
|
+
"artifactId",
|
|
711
|
+
"modality",
|
|
712
|
+
"headSequence",
|
|
713
|
+
"stateHash",
|
|
714
|
+
"snapshotId",
|
|
715
|
+
];
|
|
716
|
+
if (
|
|
717
|
+
keys.length !== expectedKeys.length ||
|
|
718
|
+
keys.some((key, index) => key !== expectedKeys[index])
|
|
719
|
+
) {
|
|
720
|
+
throw new TypeError("hint fields invalid");
|
|
721
|
+
}
|
|
722
|
+
if (record.schemaVersion !== 1) throw new TypeError("hint schema invalid");
|
|
723
|
+
const scope = editableArtifactScope(record.scope as EditableArtifactScope);
|
|
724
|
+
const artifactId = editableArtifactId(record.artifactId as string);
|
|
725
|
+
if (
|
|
726
|
+
scope.accountId !== expected.scope.accountId ||
|
|
727
|
+
scope.workspaceId !== expected.scope.workspaceId ||
|
|
728
|
+
artifactId !== editableArtifactId(expected.artifactId)
|
|
729
|
+
) {
|
|
730
|
+
throw new TypeError("hint authority mismatch");
|
|
731
|
+
}
|
|
732
|
+
assertNonnegativeSafeInteger(record.headSequence as number, "hint head sequence");
|
|
733
|
+
if (modality === "spreadsheet") {
|
|
734
|
+
assertPositiveSafeInteger(
|
|
735
|
+
record.operationProtocolVersion as number,
|
|
736
|
+
"hint operation protocol version",
|
|
737
|
+
);
|
|
738
|
+
} else if (kind === "head_advanced") {
|
|
739
|
+
assertPositiveSafeInteger(
|
|
740
|
+
record.commitProtocolVersion as number,
|
|
741
|
+
"hint commit protocol version",
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
editableArtifactStateHash(record.stateHash as string);
|
|
745
|
+
if (kind === "head_advanced") return Object.freeze(record) as EditableArtifactBrokerHint;
|
|
746
|
+
if (kind === "snapshot_available") {
|
|
747
|
+
editableArtifactSnapshotId(record.snapshotId as string);
|
|
748
|
+
return Object.freeze(record) as EditableArtifactBrokerHint;
|
|
749
|
+
}
|
|
750
|
+
throw new TypeError("hint kind invalid");
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (error instanceof EditableArtifactHintEncodingError) throw error;
|
|
753
|
+
throw new EditableArtifactHintEncodingError(
|
|
754
|
+
"invalid_hint",
|
|
755
|
+
"Editable artifact broker hint is invalid",
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export class MathEditableArtifactOutboxRandom implements EditableArtifactOutboxRandomPort {
|
|
761
|
+
unit(): number {
|
|
762
|
+
return Math.random();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function validateClaimedRecord(
|
|
767
|
+
record: EditableArtifactLiveOutboxRecord,
|
|
768
|
+
owner: string,
|
|
769
|
+
): EditableArtifactOutboxId {
|
|
770
|
+
const outboxId = editableArtifactOutboxId(record.outboxId);
|
|
771
|
+
if (record.state !== "publishing") throw new TypeError("Claimed outbox row is not publishing");
|
|
772
|
+
if (record.leaseOwner !== owner) throw new TypeError("Claimed outbox row has another owner");
|
|
773
|
+
assertPositiveSafeInteger(record.attemptCount, "outbox attempt count");
|
|
774
|
+
if (record.leaseExpiresAt === null) throw new TypeError("Claimed outbox row has no lease expiry");
|
|
775
|
+
return outboxId;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function normalizeOptions(options: EditableArtifactOutboxDispatcherOptions): NormalizedOptions {
|
|
779
|
+
const owner = boundedOwner(options.owner);
|
|
780
|
+
const normalized = {
|
|
781
|
+
owner,
|
|
782
|
+
batchSize: options.batchSize ?? DEFAULTS.batchSize,
|
|
783
|
+
concurrency: options.concurrency ?? DEFAULTS.concurrency,
|
|
784
|
+
leaseDurationMs: options.leaseDurationMs ?? DEFAULTS.leaseDurationMs,
|
|
785
|
+
leaseRenewIntervalMs: options.leaseRenewIntervalMs ?? DEFAULTS.leaseRenewIntervalMs,
|
|
786
|
+
pollIntervalMs: options.pollIntervalMs ?? DEFAULTS.pollIntervalMs,
|
|
787
|
+
publishTimeoutMs: options.publishTimeoutMs ?? DEFAULTS.publishTimeoutMs,
|
|
788
|
+
retryBaseMs: options.retryBaseMs ?? DEFAULTS.retryBaseMs,
|
|
789
|
+
retryMaxMs: options.retryMaxMs ?? DEFAULTS.retryMaxMs,
|
|
790
|
+
retryJitterRatio: options.retryJitterRatio ?? DEFAULTS.retryJitterRatio,
|
|
791
|
+
claimBackoffBaseMs: options.claimBackoffBaseMs ?? DEFAULTS.claimBackoffBaseMs,
|
|
792
|
+
claimBackoffMaxMs: options.claimBackoffMaxMs ?? DEFAULTS.claimBackoffMaxMs,
|
|
793
|
+
maxHintBytes: options.maxHintBytes ?? DEFAULTS.maxHintBytes,
|
|
794
|
+
};
|
|
795
|
+
for (const [label, value] of Object.entries(normalized)) {
|
|
796
|
+
if (label === "owner" || label === "retryJitterRatio") continue;
|
|
797
|
+
if (typeof value !== "number") throw new TypeError(`${label} must be numeric`);
|
|
798
|
+
assertPositiveSafeInteger(value, label);
|
|
799
|
+
}
|
|
800
|
+
if (normalized.batchSize > 256) throw new TypeError("outbox batch size exceeds 256");
|
|
801
|
+
if (normalized.concurrency > normalized.batchSize)
|
|
802
|
+
throw new TypeError("outbox concurrency exceeds batch size");
|
|
803
|
+
if (normalized.leaseDurationMs < 5_000 || normalized.leaseDurationMs > 300_000)
|
|
804
|
+
throw new TypeError("outbox lease duration must be 5-300 seconds");
|
|
805
|
+
if (normalized.leaseRenewIntervalMs * 2 >= normalized.leaseDurationMs)
|
|
806
|
+
throw new TypeError("outbox lease renewal interval must be less than half the lease");
|
|
807
|
+
if (normalized.publishTimeoutMs >= normalized.leaseDurationMs)
|
|
808
|
+
throw new TypeError("outbox publish timeout must be shorter than the lease");
|
|
809
|
+
if (normalized.retryBaseMs > normalized.retryMaxMs)
|
|
810
|
+
throw new TypeError("outbox retry base exceeds retry maximum");
|
|
811
|
+
if (normalized.claimBackoffBaseMs > normalized.claimBackoffMaxMs)
|
|
812
|
+
throw new TypeError("outbox claim backoff base exceeds its maximum");
|
|
813
|
+
if (
|
|
814
|
+
!Number.isFinite(normalized.retryJitterRatio) ||
|
|
815
|
+
normalized.retryJitterRatio < 0 ||
|
|
816
|
+
normalized.retryJitterRatio > 1
|
|
817
|
+
) {
|
|
818
|
+
throw new TypeError("outbox retry jitter ratio must be between zero and one");
|
|
819
|
+
}
|
|
820
|
+
if (normalized.maxHintBytes > EDITABLE_ARTIFACT_HINT_MAX_BYTES)
|
|
821
|
+
throw new TypeError("outbox hint byte limit exceeds the protocol maximum");
|
|
822
|
+
return Object.freeze(normalized);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function boundedOwner(value: string): string {
|
|
826
|
+
const bytes = textEncoder.encode(value).byteLength;
|
|
827
|
+
if (bytes < 1 || bytes > 200 || value.trim() !== value || !/^[A-Za-z0-9._:-]+$/u.test(value)) {
|
|
828
|
+
throw new TypeError("outbox owner is malformed");
|
|
829
|
+
}
|
|
830
|
+
return value;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function exponentialDelay(
|
|
834
|
+
attempt: number,
|
|
835
|
+
base: number,
|
|
836
|
+
maximum: number,
|
|
837
|
+
jitterRatio: number,
|
|
838
|
+
random: EditableArtifactOutboxRandomPort,
|
|
839
|
+
): number {
|
|
840
|
+
const exponent = Math.min(30, Math.max(0, attempt - 1));
|
|
841
|
+
const withoutJitter = Math.min(maximum, base * 2 ** exponent);
|
|
842
|
+
const sample = random.unit();
|
|
843
|
+
const unit = Number.isFinite(sample) && sample >= 0 && sample < 1 ? sample : 0.5;
|
|
844
|
+
const jitter = Math.floor(withoutJitter * jitterRatio * unit);
|
|
845
|
+
return Math.min(maximum, withoutJitter + jitter);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function withTimeout(
|
|
849
|
+
operation: Promise<void>,
|
|
850
|
+
timeoutMs: number,
|
|
851
|
+
scheduler: EditableArtifactLiveSchedulerPort,
|
|
852
|
+
operationAbort: AbortController,
|
|
853
|
+
onTimeout: () => void,
|
|
854
|
+
): Promise<void> {
|
|
855
|
+
const timeoutAbort = new AbortController();
|
|
856
|
+
try {
|
|
857
|
+
await Promise.race([
|
|
858
|
+
operation,
|
|
859
|
+
scheduler.sleep(timeoutMs, timeoutAbort.signal).then(() => {
|
|
860
|
+
onTimeout();
|
|
861
|
+
operationAbort.abort();
|
|
862
|
+
throw new Error("Editable artifact broker publish timed out");
|
|
863
|
+
}),
|
|
864
|
+
]);
|
|
865
|
+
} finally {
|
|
866
|
+
timeoutAbort.abort();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function sleepUntilNextPoll(
|
|
871
|
+
scheduler: EditableArtifactLiveSchedulerPort,
|
|
872
|
+
milliseconds: number,
|
|
873
|
+
signal: AbortSignal,
|
|
874
|
+
): Promise<boolean> {
|
|
875
|
+
try {
|
|
876
|
+
await scheduler.sleep(milliseconds, signal);
|
|
877
|
+
return true;
|
|
878
|
+
} catch (error) {
|
|
879
|
+
if (signal.aborted) return false;
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function brokerErrorCode(error: unknown): EditableArtifactOutboxRetryErrorCode {
|
|
885
|
+
if (
|
|
886
|
+
typeof error === "object" &&
|
|
887
|
+
error !== null &&
|
|
888
|
+
"code" in error &&
|
|
889
|
+
error.code === "broker_backpressure"
|
|
890
|
+
) {
|
|
891
|
+
return "broker_backpressure";
|
|
892
|
+
}
|
|
893
|
+
return "broker_unavailable";
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function mutableSummary(): MutableSummary {
|
|
897
|
+
return {
|
|
898
|
+
claimed: 0,
|
|
899
|
+
published: 0,
|
|
900
|
+
retried: 0,
|
|
901
|
+
deadLettered: 0,
|
|
902
|
+
leaseLost: 0,
|
|
903
|
+
markFailed: 0,
|
|
904
|
+
claimFailed: false,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function freezeSummary(summary: MutableSummary): EditableArtifactOutboxDispatchSummary {
|
|
909
|
+
return Object.freeze({ ...summary });
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function base64Url(bytes: Uint8Array): string {
|
|
913
|
+
let binary = "";
|
|
914
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
915
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
916
|
+
}
|