@effect-agent/storage-cloudflare 0.1.0-beta.9 → 0.1.0-beta.90
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/DoMemoryStore.d.mts +29 -0
- package/dist/DoMemoryStore.mjs +55 -0
- package/dist/DoMemoryStore.mjs.map +1 -0
- package/dist/DoMessageDeliveryStore.d.mts +17 -0
- package/dist/DoMessageDeliveryStore.mjs +32 -0
- package/dist/DoMessageDeliveryStore.mjs.map +1 -0
- package/dist/DoScheduleStore.d.mts +36 -0
- package/dist/DoScheduleStore.mjs +388 -0
- package/dist/DoScheduleStore.mjs.map +1 -0
- package/dist/DoStorageConfig.d.mts +45 -0
- package/dist/DoStorageConfig.mjs +52 -0
- package/dist/DoStorageConfig.mjs.map +1 -0
- package/dist/DoStorageError-Cmhvl4TQ.d.mts +98 -0
- package/dist/DoStorageError.d.mts +2 -0
- package/dist/DoStorageError.mjs +164 -0
- package/dist/DoStorageError.mjs.map +1 -0
- package/dist/DoStorageFailpoint.d.mts +17 -0
- package/dist/DoStorageFailpoint.mjs +14 -0
- package/dist/DoStorageFailpoint.mjs.map +1 -0
- package/dist/DoStorageFailpointTesting.d.mts +34 -0
- package/dist/DoStorageFailpointTesting.mjs +35 -0
- package/dist/DoStorageFailpointTesting.mjs.map +1 -0
- package/dist/DoStorageVersion-x5OOurvZ.d.mts +12 -0
- package/dist/DoStorageVersion.d.mts +2 -0
- package/dist/DoStorageVersion.mjs +8 -0
- package/dist/DoStorageVersion.mjs.map +1 -0
- package/dist/DoSubmissionLedger.d.mts +23 -0
- package/dist/DoSubmissionLedger.mjs +1849 -0
- package/dist/DoSubmissionLedger.mjs.map +1 -0
- package/dist/DoSubscriptionStore.d.mts +25 -0
- package/dist/DoSubscriptionStore.mjs +183 -0
- package/dist/DoSubscriptionStore.mjs.map +1 -0
- package/dist/DoThreadStore.d.mts +59 -0
- package/dist/DoThreadStore.mjs +465 -0
- package/dist/DoThreadStore.mjs.map +1 -0
- package/dist/MemoryProtocol.d.mts +38160 -0
- package/dist/MemoryProtocol.mjs +219 -0
- package/dist/MemoryProtocol.mjs.map +1 -0
- package/dist/PortProtocol.d.mts +3804 -0
- package/dist/PortProtocol.mjs +176 -0
- package/dist/PortProtocol.mjs.map +1 -0
- package/dist/PortRouting.d.mts +83 -0
- package/dist/PortRouting.mjs +410 -0
- package/dist/PortRouting.mjs.map +1 -0
- package/dist/do-journal-dJ01KSKf.mjs +991 -0
- package/dist/do-journal-dJ01KSKf.mjs.map +1 -0
- package/dist/index.d.mts +14 -1525
- package/dist/index.mjs +14 -3695
- package/dist/migrations-Ba_kOrSx.mjs +287 -0
- package/dist/migrations-Ba_kOrSx.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -45
- package/src/DoMemoryStore.ts +56 -0
- package/src/DoMessageDeliveryStore.ts +53 -0
- package/src/DoScheduleStore.ts +622 -0
- package/src/{errors.ts → DoStorageError.ts} +10 -3
- package/src/DoStorageFailpoint.ts +20 -0
- package/src/{do-storage-failpoint.ts → DoStorageFailpointTesting.ts} +5 -18
- package/src/DoStorageVersion.ts +2 -0
- package/src/{do-ledger.ts → DoSubmissionLedger.ts} +924 -225
- package/src/DoSubscriptionStore.ts +329 -0
- package/src/DoThreadStore.ts +1012 -0
- package/src/MemoryProtocol.ts +351 -0
- package/src/{port-protocol.ts → PortProtocol.ts} +45 -38
- package/src/{routing.ts → PortRouting.ts} +292 -264
- package/src/index.ts +13 -32
- package/src/{do-journal.ts → internal/do-journal.ts} +718 -234
- package/src/internal/message-delivery-schema.ts +24 -0
- package/src/{migrations.ts → internal/migrations.ts} +51 -35
- package/src/internal/recovery-checkpoint-schema.ts +17 -0
- package/dist/index.mjs.map +0 -1
- package/src/do-conversation-store.ts +0 -850
- /package/src/{do-storage-config.ts → DoStorageConfig.ts} +0 -0
|
@@ -1,23 +1,26 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Option, Predicate, Schema, Stream } from "effect";
|
|
1
2
|
import {
|
|
2
3
|
AdmissionIndeterminate,
|
|
3
|
-
|
|
4
|
+
AdmissionConflict,
|
|
5
|
+
AdmissionPolicyError,
|
|
4
6
|
ChildAttachmentSnapshot,
|
|
5
|
-
ConversationMaterialization,
|
|
6
|
-
ConversationNotMaterialized,
|
|
7
|
-
ConversationStore,
|
|
8
|
-
ConversationStoreError,
|
|
9
|
-
FenceRejected,
|
|
10
7
|
JoinedToHost,
|
|
11
8
|
LedgerError,
|
|
12
9
|
RecoverySnapshot,
|
|
13
10
|
SettlementConflict,
|
|
14
11
|
SubmissionLedger,
|
|
15
12
|
SubmissionLookupById,
|
|
16
|
-
type AdmissionConflict,
|
|
17
13
|
type SubmissionLookupByKey,
|
|
18
14
|
type SubmissionSnapshot,
|
|
19
|
-
} from "
|
|
20
|
-
import {
|
|
15
|
+
} from "effect-agent/submission-ledger";
|
|
16
|
+
import {
|
|
17
|
+
AppendConflict,
|
|
18
|
+
ThreadMaterialization,
|
|
19
|
+
ThreadNotMaterialized,
|
|
20
|
+
ThreadStore,
|
|
21
|
+
ThreadStoreError,
|
|
22
|
+
FenceRejected,
|
|
23
|
+
} from "effect-agent/thread-store";
|
|
21
24
|
|
|
22
25
|
import {
|
|
23
26
|
boundPortDiagnostic,
|
|
@@ -55,13 +58,13 @@ import {
|
|
|
55
58
|
type PortRequestEnvelope,
|
|
56
59
|
type PortResponse,
|
|
57
60
|
type PortResult,
|
|
58
|
-
} from "./
|
|
61
|
+
} from "./PortProtocol.ts";
|
|
59
62
|
|
|
60
|
-
type
|
|
63
|
+
type ThreadId = ThreadMaterialization["threadId"];
|
|
61
64
|
type SubmissionId = SubmissionSnapshot["submissionId"];
|
|
62
65
|
|
|
63
|
-
const
|
|
64
|
-
const
|
|
66
|
+
const ThreadIdSchema = ThreadMaterialization.fields.threadId;
|
|
67
|
+
const decodeThreadId = Schema.decodeUnknownEffect(ThreadIdSchema);
|
|
65
68
|
|
|
66
69
|
/**
|
|
67
70
|
* The ledger row bound routable Submission identities must respect (mirrors the local
|
|
@@ -76,7 +79,7 @@ const UUID_HEAD_PATTERN =
|
|
|
76
79
|
|
|
77
80
|
/**
|
|
78
81
|
* A transport could not deliver a port request to (or an answer from) the owning
|
|
79
|
-
*
|
|
82
|
+
* Thread's Durable Object. `retryable` carries the platform's own stub signal when one
|
|
80
83
|
* exists. This error never crosses the wire — it is the CALLER-side evidence that the
|
|
81
84
|
* authority was unreachable, which is exactly the case `AdmissionIndeterminate` was
|
|
82
85
|
* specified for (SUB-031).
|
|
@@ -91,20 +94,37 @@ export class PortTransportError extends Schema.TaggedError<PortTransportError>()
|
|
|
91
94
|
},
|
|
92
95
|
) {}
|
|
93
96
|
|
|
97
|
+
const safeTransportDiagnostic = (cause: unknown): string => {
|
|
98
|
+
try {
|
|
99
|
+
const message = cause instanceof Error ? cause.message : cause;
|
|
100
|
+
|
|
101
|
+
return boundPortDiagnostic(typeof message === "string" ? message : String(message));
|
|
102
|
+
} catch {
|
|
103
|
+
return "[unavailable transport diagnostic]";
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const transportRetryableSignal = (cause: unknown): boolean | undefined => {
|
|
108
|
+
if (!Predicate.isObjectKeyword(cause)) return undefined;
|
|
109
|
+
try {
|
|
110
|
+
const signal = Reflect.get(cause, "retryable");
|
|
111
|
+
|
|
112
|
+
return typeof signal === "boolean" ? signal : undefined;
|
|
113
|
+
} catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
94
118
|
/**
|
|
95
119
|
* Build a `PortTransportError` from an arbitrary thrown transport cause, preserving the
|
|
96
120
|
* platform stub's own `retryable` signal when present.
|
|
97
121
|
*/
|
|
98
122
|
export const portTransportFailure = (target: string, cause: unknown): PortTransportError => {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
if (typeof cause === "object" && cause !== null && "retryable" in cause) {
|
|
102
|
-
const signal = cause.retryable;
|
|
103
|
-
if (typeof signal === "boolean") retryable = signal;
|
|
104
|
-
}
|
|
123
|
+
const retryable = transportRetryableSignal(cause);
|
|
124
|
+
|
|
105
125
|
return PortTransportError.make({
|
|
106
126
|
target,
|
|
107
|
-
message:
|
|
127
|
+
message: safeTransportDiagnostic(cause),
|
|
108
128
|
...(retryable === undefined ? {} : { retryable }),
|
|
109
129
|
cause,
|
|
110
130
|
});
|
|
@@ -112,42 +132,42 @@ export const portTransportFailure = (target: string, cause: unknown): PortTransp
|
|
|
112
132
|
|
|
113
133
|
/**
|
|
114
134
|
* Delivery of Schema-encoded port envelopes to the Durable Object that owns a FOREIGN
|
|
115
|
-
*
|
|
135
|
+
* Thread (plan §1.3, D-P6-3). The shipped implementation (platform-cloudflare, WP3)
|
|
116
136
|
* calls the owner's `portCall` over native Durable Object JS RPC via
|
|
117
|
-
* `namespace.idFromName(
|
|
137
|
+
* `namespace.idFromName(threadId)`; the protocol is transport-agnostic and any carrier
|
|
118
138
|
* that moves the encoded envelopes verbatim satisfies this service. Implementations MUST
|
|
119
139
|
* surface every delivery problem as `PortTransportError` and must never fabricate an answer.
|
|
120
140
|
*/
|
|
121
|
-
export class
|
|
122
|
-
|
|
141
|
+
export class ThreadPortTransport extends Context.Service<
|
|
142
|
+
ThreadPortTransport,
|
|
123
143
|
{
|
|
124
144
|
readonly call: (
|
|
125
|
-
|
|
145
|
+
threadId: ThreadId,
|
|
126
146
|
request: PortRequestEnvelope,
|
|
127
147
|
) => Effect.Effect<unknown, PortTransportError>;
|
|
128
148
|
}
|
|
129
|
-
>()("@effect-agent/storage-cloudflare/
|
|
149
|
+
>()("@effect-agent/storage-cloudflare/ThreadPortTransport") {}
|
|
130
150
|
|
|
131
151
|
/** Construction options shared by both routed port Layers. */
|
|
132
152
|
export interface RoutedPortOptions {
|
|
133
153
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
154
|
+
* Decide whether this physical owner stores the addressed logical Thread. Local
|
|
155
|
+
* requests use the same SQLite facets; foreign requests use the transport or fail
|
|
156
|
+
* fast typed. The predicate determines placement, never caller authorization.
|
|
137
157
|
*/
|
|
138
|
-
readonly
|
|
158
|
+
readonly ownsThread: (threadId: ThreadId) => boolean;
|
|
139
159
|
}
|
|
140
160
|
|
|
141
161
|
/** Where one port request must execute. */
|
|
142
162
|
type RouteTarget =
|
|
143
163
|
| { readonly _tag: "local" }
|
|
144
|
-
| { readonly _tag: "foreign"; readonly
|
|
164
|
+
| { readonly _tag: "foreign"; readonly threadId: ThreadId };
|
|
145
165
|
|
|
146
166
|
const LOCAL: RouteTarget = { _tag: "local" };
|
|
147
167
|
|
|
148
168
|
/**
|
|
149
|
-
* Parse a DC-minted routable Submission identity — `{uuidv7}:{
|
|
150
|
-
* FIRST `:` because the
|
|
169
|
+
* Parse a DC-minted routable Submission identity — `{uuidv7}:{threadId}`, split at the
|
|
170
|
+
* FIRST `:` because the Thread tail may itself contain colons (D-P6-5). This adapter
|
|
151
171
|
* minted the format at admission and is the ONLY component that parses it; identities that do
|
|
152
172
|
* not carry the minted shape (no separator, non-UUID head, empty tail) fall back to the local
|
|
153
173
|
* facet, which is the only authority this Object can consult without inventing an owner.
|
|
@@ -155,7 +175,7 @@ const LOCAL: RouteTarget = { _tag: "local" };
|
|
|
155
175
|
* refused them at admission, so they cannot name any stored row anywhere.
|
|
156
176
|
*/
|
|
157
177
|
const routableSubmissionTarget = (
|
|
158
|
-
|
|
178
|
+
ownsThread: RoutedPortOptions["ownsThread"],
|
|
159
179
|
): ((operation: string, submissionId: string) => Effect.Effect<RouteTarget, LedgerError>) =>
|
|
160
180
|
Effect.fn("DoPortRouting.routableSubmissionTarget")(function* (
|
|
161
181
|
operation: string,
|
|
@@ -171,63 +191,49 @@ const routableSubmissionTarget = (
|
|
|
171
191
|
});
|
|
172
192
|
}
|
|
173
193
|
const separator = submissionId.indexOf(":");
|
|
194
|
+
|
|
174
195
|
if (separator === -1) return LOCAL;
|
|
175
196
|
if (!UUID_HEAD_PATTERN.test(submissionId.slice(0, separator))) return LOCAL;
|
|
176
197
|
const tail = submissionId.slice(separator + 1);
|
|
177
|
-
|
|
178
|
-
return yield*
|
|
179
|
-
Effect.map((
|
|
198
|
+
|
|
199
|
+
return yield* decodeThreadId(tail).pipe(
|
|
200
|
+
Effect.map((threadId): RouteTarget =>
|
|
201
|
+
ownsThread(threadId) ? LOCAL : { _tag: "foreign", threadId },
|
|
202
|
+
),
|
|
180
203
|
Effect.orElseSucceed(() => LOCAL),
|
|
181
204
|
);
|
|
182
205
|
});
|
|
183
206
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
result._tag === tag;
|
|
188
|
-
|
|
189
|
-
const isAdmitConflict = (failure: PortFailure): failure is AdmissionConflict =>
|
|
190
|
-
failure._tag === "AdmissionConflict";
|
|
191
|
-
const isAbortConflict = (failure: PortFailure): failure is SettlementConflict | JoinedToHost =>
|
|
192
|
-
failure._tag === "SettlementConflict" || failure._tag === "JoinedToHost";
|
|
193
|
-
const noExtraFailure = (_failure: PortFailure): _failure is never => false;
|
|
194
|
-
const isFenceRejected = (failure: PortFailure): failure is FenceRejected =>
|
|
195
|
-
failure._tag === "FenceRejected";
|
|
196
|
-
const isAppendFailure = (
|
|
197
|
-
failure: PortFailure,
|
|
198
|
-
): failure is ConversationNotMaterialized | AppendConflict | FenceRejected =>
|
|
199
|
-
failure._tag === "ConversationNotMaterialized" ||
|
|
200
|
-
failure._tag === "AppendConflict" ||
|
|
201
|
-
failure._tag === "FenceRejected";
|
|
202
|
-
const isNotMaterialized = (failure: PortFailure): failure is ConversationNotMaterialized =>
|
|
203
|
-
failure._tag === "ConversationNotMaterialized";
|
|
207
|
+
const NoAdditionalPortFailure = Schema.Never;
|
|
208
|
+
const AbortPortFailure = Schema.Union([SettlementConflict, JoinedToHost]);
|
|
209
|
+
const AppendPortFailure = Schema.Union([ThreadNotMaterialized, AppendConflict, FenceRejected]);
|
|
204
210
|
|
|
205
211
|
/**
|
|
206
212
|
* The fail-fast refusal for any foreign operation OUTSIDE the closed route-capable subset
|
|
207
213
|
* (plan §1.3): honesty over accidental distribution.
|
|
208
214
|
*/
|
|
209
|
-
const
|
|
215
|
+
const crossThreadLedgerError = (operation: string, target: string): LedgerError =>
|
|
210
216
|
LedgerError.make({
|
|
211
217
|
operation,
|
|
212
218
|
message:
|
|
213
|
-
`${operation} addressed to foreign
|
|
219
|
+
`${operation} addressed to foreign Thread ${target} is not route-capable; the ` +
|
|
214
220
|
"closed cross-Object subset is admit, markReady, lookup, resolveAdmission, " +
|
|
215
221
|
"requestAbort, and recordChildSettled. Every other ledger operation is lane-local by " +
|
|
216
|
-
"construction and must execute inside the owning
|
|
222
|
+
"construction and must execute inside the owning Thread's Durable Object.",
|
|
217
223
|
});
|
|
218
224
|
|
|
219
|
-
const
|
|
220
|
-
|
|
225
|
+
const crossThreadStoreError = (operation: string, target: string): ThreadStoreError =>
|
|
226
|
+
ThreadStoreError.make({
|
|
221
227
|
operation,
|
|
222
228
|
message:
|
|
223
|
-
`${operation} addressed to foreign
|
|
229
|
+
`${operation} addressed to foreign Thread ${target} is not route-capable; the ` +
|
|
224
230
|
"closed cross-Object subset is materialize, append, read (paged), inspectTail, and " +
|
|
225
231
|
"export. Observation and checkpoints are lane-local by construction and must execute " +
|
|
226
|
-
"inside the owning
|
|
232
|
+
"inside the owning Thread's Durable Object.",
|
|
227
233
|
});
|
|
228
234
|
|
|
229
|
-
const makeTransportCall = (transport:
|
|
230
|
-
Effect.fn("DoPortRouting.transportCall")(function* (target:
|
|
235
|
+
const makeTransportCall = (transport: ThreadPortTransport["Service"]) =>
|
|
236
|
+
Effect.fn("DoPortRouting.transportCall")(function* (target: ThreadId, call: PortRequest) {
|
|
231
237
|
const encoded = yield* encodePortRequest(call).pipe(
|
|
232
238
|
Effect.mapError((error) =>
|
|
233
239
|
PortProtocolError.make({
|
|
@@ -235,7 +241,9 @@ const makeTransportCall = (transport: ConversationPortTransport["Service"]) =>
|
|
|
235
241
|
}),
|
|
236
242
|
),
|
|
237
243
|
);
|
|
244
|
+
|
|
238
245
|
const raw = yield* transport.call(target, encoded);
|
|
246
|
+
|
|
239
247
|
return yield* decodePortResponse(raw).pipe(
|
|
240
248
|
Effect.mapError((error) =>
|
|
241
249
|
PortProtocolError.make({
|
|
@@ -251,9 +259,9 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
251
259
|
options: RoutedPortOptions,
|
|
252
260
|
) {
|
|
253
261
|
const local = yield* SubmissionLedger;
|
|
254
|
-
const transport = yield*
|
|
262
|
+
const transport = yield* ThreadPortTransport;
|
|
255
263
|
const transportCall: TransportCall = makeTransportCall(transport);
|
|
256
|
-
const submissionTarget = routableSubmissionTarget(options.
|
|
264
|
+
const submissionTarget = routableSubmissionTarget(options.ownsThread);
|
|
257
265
|
|
|
258
266
|
const routeFailure =
|
|
259
267
|
(operation: string, target: string) =>
|
|
@@ -261,7 +269,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
261
269
|
LedgerError.make({
|
|
262
270
|
operation,
|
|
263
271
|
message: boundPortDiagnostic(
|
|
264
|
-
`Routed ${operation} to the
|
|
272
|
+
`Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`,
|
|
265
273
|
),
|
|
266
274
|
cause: error,
|
|
267
275
|
});
|
|
@@ -272,31 +280,31 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
272
280
|
* any failure outside the operation's declared surface — including protocol anomalies — is
|
|
273
281
|
* folded into a `LedgerError` naming the anomaly instead of being erased or re-thrown raw.
|
|
274
282
|
*/
|
|
275
|
-
const foreignLedgerCall = <
|
|
283
|
+
const foreignLedgerCall = <ResultSchema extends Schema.Top, FailureSchema extends Schema.Top>(
|
|
276
284
|
operation: string,
|
|
277
|
-
target:
|
|
285
|
+
target: ThreadId,
|
|
278
286
|
call: PortRequest,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
): Effect.Effect<
|
|
282
|
-
|
|
287
|
+
resultSchema: ResultSchema,
|
|
288
|
+
failureSchema: FailureSchema,
|
|
289
|
+
): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | LedgerError> => {
|
|
290
|
+
const isExpectedResult = Schema.is(resultSchema);
|
|
291
|
+
const isExpectedFailure = Schema.is(failureSchema);
|
|
292
|
+
|
|
293
|
+
return transportCall(target, call).pipe(
|
|
283
294
|
Effect.mapError(routeFailure(operation, target)),
|
|
284
295
|
Effect.flatMap(
|
|
285
|
-
(
|
|
286
|
-
response,
|
|
287
|
-
): Effect.Effect<
|
|
288
|
-
Extract<PortResult, { readonly _tag: Tag }>,
|
|
289
|
-
ExpectedFailure | LedgerError
|
|
290
|
-
> => {
|
|
296
|
+
(response): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | LedgerError> => {
|
|
291
297
|
if (response._tag === "PortFailed") {
|
|
292
298
|
const failure = response.failure;
|
|
299
|
+
|
|
293
300
|
if (isExpectedFailure(failure)) return Effect.fail(failure);
|
|
294
301
|
if (failure._tag === "LedgerError") return Effect.fail(failure);
|
|
302
|
+
|
|
295
303
|
return Effect.fail(
|
|
296
304
|
LedgerError.make({
|
|
297
305
|
operation,
|
|
298
306
|
message: boundPortDiagnostic(
|
|
299
|
-
`The
|
|
307
|
+
`The Thread Object owning ${target} answered ${operation} with the ` +
|
|
300
308
|
`out-of-contract failure ${failure._tag}: ${failure.message}`,
|
|
301
309
|
),
|
|
302
310
|
cause: failure,
|
|
@@ -304,16 +312,18 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
304
312
|
);
|
|
305
313
|
}
|
|
306
314
|
const result = response.result;
|
|
307
|
-
|
|
315
|
+
|
|
316
|
+
if (!isExpectedResult(result)) {
|
|
308
317
|
return Effect.fail(
|
|
309
318
|
LedgerError.make({
|
|
310
319
|
operation,
|
|
311
320
|
message:
|
|
312
|
-
`The
|
|
313
|
-
`mismatched result ${result._tag}
|
|
321
|
+
`The Thread Object owning ${target} answered ${operation} with the ` +
|
|
322
|
+
`mismatched result ${result._tag}.`,
|
|
314
323
|
}),
|
|
315
324
|
);
|
|
316
325
|
}
|
|
326
|
+
|
|
317
327
|
return Effect.succeed(result);
|
|
318
328
|
},
|
|
319
329
|
),
|
|
@@ -321,6 +331,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
321
331
|
attributes: { operation, target },
|
|
322
332
|
}),
|
|
323
333
|
);
|
|
334
|
+
};
|
|
324
335
|
|
|
325
336
|
/**
|
|
326
337
|
* Routed `resolveAdmission` — where the S2 tri-state becomes real (plan §1.3): when the
|
|
@@ -331,7 +342,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
331
342
|
* reached and reported its own storage failure.
|
|
332
343
|
*/
|
|
333
344
|
const resolveForeignAdmission = (
|
|
334
|
-
target:
|
|
345
|
+
target: ThreadId,
|
|
335
346
|
request: SubmissionLookupByKey,
|
|
336
347
|
): Effect.Effect<
|
|
337
348
|
| AdmissionIndeterminate
|
|
@@ -342,10 +353,11 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
342
353
|
Effect.flatMap((response) => {
|
|
343
354
|
if (response._tag === "PortFailed") {
|
|
344
355
|
if (response.failure._tag === "LedgerError") return Effect.fail(response.failure);
|
|
356
|
+
|
|
345
357
|
return Effect.succeed(
|
|
346
358
|
AdmissionIndeterminate.make({
|
|
347
359
|
reason: boundPortDiagnostic(
|
|
348
|
-
`The
|
|
360
|
+
`The Thread Object owning ${target} answered resolveAdmission with the ` +
|
|
349
361
|
`out-of-contract failure ${response.failure._tag}: ${response.failure.message}`,
|
|
350
362
|
),
|
|
351
363
|
}),
|
|
@@ -355,12 +367,13 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
355
367
|
return Effect.succeed(
|
|
356
368
|
AdmissionIndeterminate.make({
|
|
357
369
|
reason: boundPortDiagnostic(
|
|
358
|
-
`The
|
|
370
|
+
`The Thread Object owning ${target} answered resolveAdmission with the ` +
|
|
359
371
|
`mismatched result ${response.result._tag}.`,
|
|
360
372
|
),
|
|
361
373
|
}),
|
|
362
374
|
);
|
|
363
375
|
}
|
|
376
|
+
|
|
364
377
|
return Effect.succeed(response.result.resolution);
|
|
365
378
|
}),
|
|
366
379
|
Effect.catchTags({
|
|
@@ -368,7 +381,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
368
381
|
Effect.succeed(
|
|
369
382
|
AdmissionIndeterminate.make({
|
|
370
383
|
reason: boundPortDiagnostic(
|
|
371
|
-
`The
|
|
384
|
+
`The Thread Object owning ${target} is unreachable: ${error.message}`,
|
|
372
385
|
),
|
|
373
386
|
}),
|
|
374
387
|
),
|
|
@@ -376,7 +389,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
376
389
|
Effect.succeed(
|
|
377
390
|
AdmissionIndeterminate.make({
|
|
378
391
|
reason: boundPortDiagnostic(
|
|
379
|
-
`The answer of the
|
|
392
|
+
`The answer of the Thread Object owning ${target} could not be ` +
|
|
380
393
|
`understood: ${error.message}`,
|
|
381
394
|
),
|
|
382
395
|
}),
|
|
@@ -387,15 +400,15 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
387
400
|
|
|
388
401
|
const foreignLookupById = (
|
|
389
402
|
operation: string,
|
|
390
|
-
target:
|
|
403
|
+
target: ThreadId,
|
|
391
404
|
submissionId: SubmissionId,
|
|
392
405
|
): Effect.Effect<Option.Option<SubmissionSnapshot>, LedgerError> =>
|
|
393
406
|
foreignLedgerCall(
|
|
394
407
|
operation,
|
|
395
408
|
target,
|
|
396
409
|
LedgerLookupCall.make({ request: SubmissionLookupById.make({ submissionId }) }),
|
|
397
|
-
|
|
398
|
-
|
|
410
|
+
LedgerLookupResult,
|
|
411
|
+
NoAdditionalPortFailure,
|
|
399
412
|
).pipe(
|
|
400
413
|
Effect.map((result) =>
|
|
401
414
|
result.submission === undefined ? Option.none() : Option.some(result.submission),
|
|
@@ -413,18 +426,24 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
413
426
|
snapshot: RecoverySnapshot,
|
|
414
427
|
): Effect.fn.Return<RecoverySnapshot, LedgerError> {
|
|
415
428
|
const operation = "ledger load recovery snapshot";
|
|
429
|
+
|
|
416
430
|
const attachments = new Map(
|
|
417
431
|
snapshot.childAttachments.map((attachment) => [attachment.childSubmissionId, attachment]),
|
|
418
432
|
);
|
|
433
|
+
|
|
419
434
|
let enriched = false;
|
|
435
|
+
|
|
420
436
|
for (const reservation of snapshot.childReservations) {
|
|
421
437
|
const childSubmissionId = reservation.childSubmissionId;
|
|
438
|
+
|
|
422
439
|
if (childSubmissionId === undefined || attachments.has(childSubmissionId)) continue;
|
|
423
440
|
const target = yield* submissionTarget(operation, childSubmissionId);
|
|
441
|
+
|
|
424
442
|
// A local or opaque child identity was already answered authoritatively by the local
|
|
425
443
|
// facet; absence there means the child admission never committed.
|
|
426
444
|
if (target._tag !== "foreign") continue;
|
|
427
|
-
const child = yield* foreignLookupById(operation, target.
|
|
445
|
+
const child = yield* foreignLookupById(operation, target.threadId, childSubmissionId);
|
|
446
|
+
|
|
428
447
|
if (Option.isNone(child)) continue;
|
|
429
448
|
attachments.set(
|
|
430
449
|
childSubmissionId,
|
|
@@ -442,11 +461,14 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
442
461
|
if (!enriched) return snapshot;
|
|
443
462
|
// Rebuild in reservation (parent Tool Call) order, the order the local facet documents.
|
|
444
463
|
const ordered: Array<ChildAttachmentSnapshot> = [];
|
|
464
|
+
|
|
445
465
|
for (const reservation of snapshot.childReservations) {
|
|
446
466
|
if (reservation.childSubmissionId === undefined) continue;
|
|
447
467
|
const attachment = attachments.get(reservation.childSubmissionId);
|
|
468
|
+
|
|
448
469
|
if (attachment !== undefined) ordered.push(attachment);
|
|
449
470
|
}
|
|
471
|
+
|
|
450
472
|
return RecoverySnapshot.make({ ...snapshot, childAttachments: ordered });
|
|
451
473
|
});
|
|
452
474
|
|
|
@@ -454,14 +476,14 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
454
476
|
capabilities: local.capabilities,
|
|
455
477
|
|
|
456
478
|
admit: (request) =>
|
|
457
|
-
request.
|
|
479
|
+
options.ownsThread(request.threadId)
|
|
458
480
|
? local.admit(request)
|
|
459
481
|
: foreignLedgerCall(
|
|
460
482
|
"ledger admit",
|
|
461
|
-
request.
|
|
483
|
+
request.threadId,
|
|
462
484
|
LedgerAdmitCall.make({ request }),
|
|
463
|
-
|
|
464
|
-
|
|
485
|
+
LedgerAdmitResult,
|
|
486
|
+
Schema.Union([AdmissionConflict, AdmissionPolicyError]),
|
|
465
487
|
).pipe(Effect.map((reply) => reply.result)),
|
|
466
488
|
|
|
467
489
|
markReady: (request) =>
|
|
@@ -471,10 +493,10 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
471
493
|
? local.markReady(request)
|
|
472
494
|
: foreignLedgerCall(
|
|
473
495
|
"ledger mark ready",
|
|
474
|
-
target.
|
|
496
|
+
target.threadId,
|
|
475
497
|
LedgerMarkReadyCall.make({ request }),
|
|
476
|
-
|
|
477
|
-
|
|
498
|
+
LedgerMarkReadyResult,
|
|
499
|
+
NoAdditionalPortFailure,
|
|
478
500
|
).pipe(Effect.asVoid),
|
|
479
501
|
),
|
|
480
502
|
),
|
|
@@ -485,17 +507,17 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
485
507
|
Effect.flatMap((target) =>
|
|
486
508
|
target._tag === "local"
|
|
487
509
|
? local.lookup(request)
|
|
488
|
-
: foreignLookupById("ledger lookup", target.
|
|
510
|
+
: foreignLookupById("ledger lookup", target.threadId, request.submissionId),
|
|
489
511
|
),
|
|
490
512
|
)
|
|
491
|
-
: request.
|
|
513
|
+
: options.ownsThread(request.threadId)
|
|
492
514
|
? local.lookup(request)
|
|
493
515
|
: foreignLedgerCall(
|
|
494
516
|
"ledger lookup",
|
|
495
|
-
request.
|
|
517
|
+
request.threadId,
|
|
496
518
|
LedgerLookupCall.make({ request }),
|
|
497
|
-
|
|
498
|
-
|
|
519
|
+
LedgerLookupResult,
|
|
520
|
+
NoAdditionalPortFailure,
|
|
499
521
|
).pipe(
|
|
500
522
|
Effect.map((result) =>
|
|
501
523
|
result.submission === undefined ? Option.none() : Option.some(result.submission),
|
|
@@ -503,9 +525,9 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
503
525
|
),
|
|
504
526
|
|
|
505
527
|
resolveAdmission: (request) =>
|
|
506
|
-
request.
|
|
528
|
+
options.ownsThread(request.threadId)
|
|
507
529
|
? local.resolveAdmission(request)
|
|
508
|
-
: resolveForeignAdmission(request.
|
|
530
|
+
: resolveForeignAdmission(request.threadId, request),
|
|
509
531
|
|
|
510
532
|
requestAbort: (request) =>
|
|
511
533
|
submissionTarget("ledger request abort", request.submissionId).pipe(
|
|
@@ -514,10 +536,10 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
514
536
|
? local.requestAbort(request)
|
|
515
537
|
: foreignLedgerCall(
|
|
516
538
|
"ledger request abort",
|
|
517
|
-
target.
|
|
539
|
+
target.threadId,
|
|
518
540
|
LedgerRequestAbortCall.make({ request }),
|
|
519
|
-
|
|
520
|
-
|
|
541
|
+
LedgerRequestAbortResult,
|
|
542
|
+
AbortPortFailure,
|
|
521
543
|
).pipe(Effect.map((reply) => reply.intent)),
|
|
522
544
|
),
|
|
523
545
|
),
|
|
@@ -529,10 +551,10 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
529
551
|
? local.recordChildSettled(request)
|
|
530
552
|
: foreignLedgerCall(
|
|
531
553
|
"ledger record child settled",
|
|
532
|
-
target.
|
|
554
|
+
target.threadId,
|
|
533
555
|
LedgerRecordChildSettledCall.make({ request }),
|
|
534
|
-
|
|
535
|
-
|
|
556
|
+
LedgerRecordChildSettledResult,
|
|
557
|
+
NoAdditionalPortFailure,
|
|
536
558
|
).pipe(Effect.map((reply) => reply.outcome)),
|
|
537
559
|
),
|
|
538
560
|
),
|
|
@@ -540,23 +562,21 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
540
562
|
// Every operation below is lane-local by construction (plan §1.3): a foreign address is
|
|
541
563
|
// an out-of-contract call and fails fast typed instead of being quietly distributed.
|
|
542
564
|
claim: (request) =>
|
|
543
|
-
request.
|
|
565
|
+
options.ownsThread(request.threadId)
|
|
544
566
|
? local.claim(request)
|
|
545
|
-
: Effect.fail(
|
|
567
|
+
: Effect.fail(crossThreadLedgerError("ledger claim", request.threadId)),
|
|
546
568
|
|
|
547
569
|
claimJoining: (request) =>
|
|
548
|
-
request.
|
|
570
|
+
options.ownsThread(request.threadId)
|
|
549
571
|
? local.claimJoining(request)
|
|
550
|
-
: Effect.fail(
|
|
572
|
+
: Effect.fail(crossThreadLedgerError("ledger claim joining", request.threadId)),
|
|
551
573
|
|
|
552
574
|
renewOwnership: (request) =>
|
|
553
575
|
submissionTarget("ledger renew ownership", request.submissionId).pipe(
|
|
554
576
|
Effect.flatMap((target) =>
|
|
555
577
|
target._tag === "local"
|
|
556
578
|
? local.renewOwnership(request)
|
|
557
|
-
: Effect.fail(
|
|
558
|
-
crossConversationLedgerError("ledger renew ownership", target.conversationId),
|
|
559
|
-
),
|
|
579
|
+
: Effect.fail(crossThreadLedgerError("ledger renew ownership", target.threadId)),
|
|
560
580
|
),
|
|
561
581
|
),
|
|
562
582
|
|
|
@@ -565,9 +585,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
565
585
|
Effect.flatMap((target) =>
|
|
566
586
|
target._tag === "local"
|
|
567
587
|
? local.releaseOwnership(request)
|
|
568
|
-
: Effect.fail(
|
|
569
|
-
crossConversationLedgerError("ledger release ownership", target.conversationId),
|
|
570
|
-
),
|
|
588
|
+
: Effect.fail(crossThreadLedgerError("ledger release ownership", target.threadId)),
|
|
571
589
|
),
|
|
572
590
|
),
|
|
573
591
|
|
|
@@ -576,9 +594,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
576
594
|
Effect.flatMap((target) =>
|
|
577
595
|
target._tag === "local"
|
|
578
596
|
? local.markInputApplied(request)
|
|
579
|
-
: Effect.fail(
|
|
580
|
-
crossConversationLedgerError("ledger mark input applied", target.conversationId),
|
|
581
|
-
),
|
|
597
|
+
: Effect.fail(crossThreadLedgerError("ledger mark input applied", target.threadId)),
|
|
582
598
|
),
|
|
583
599
|
),
|
|
584
600
|
|
|
@@ -587,9 +603,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
587
603
|
Effect.flatMap((target) =>
|
|
588
604
|
target._tag === "local"
|
|
589
605
|
? local.reserveSettlement(request)
|
|
590
|
-
: Effect.fail(
|
|
591
|
-
crossConversationLedgerError("ledger reserve settlement", target.conversationId),
|
|
592
|
-
),
|
|
606
|
+
: Effect.fail(crossThreadLedgerError("ledger reserve settlement", target.threadId)),
|
|
593
607
|
),
|
|
594
608
|
),
|
|
595
609
|
|
|
@@ -598,9 +612,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
598
612
|
Effect.flatMap((target) =>
|
|
599
613
|
target._tag === "local"
|
|
600
614
|
? local.finalizeSettlement(request)
|
|
601
|
-
: Effect.fail(
|
|
602
|
-
crossConversationLedgerError("ledger finalize settlement", target.conversationId),
|
|
603
|
-
),
|
|
615
|
+
: Effect.fail(crossThreadLedgerError("ledger finalize settlement", target.threadId)),
|
|
604
616
|
),
|
|
605
617
|
),
|
|
606
618
|
|
|
@@ -609,9 +621,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
609
621
|
Effect.flatMap((target) =>
|
|
610
622
|
target._tag === "local"
|
|
611
623
|
? local.markJoined(request)
|
|
612
|
-
: Effect.fail(
|
|
613
|
-
crossConversationLedgerError("ledger mark joined", target.conversationId),
|
|
614
|
-
),
|
|
624
|
+
: Effect.fail(crossThreadLedgerError("ledger mark joined", target.threadId)),
|
|
615
625
|
),
|
|
616
626
|
),
|
|
617
627
|
|
|
@@ -620,9 +630,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
620
630
|
Effect.flatMap((target) =>
|
|
621
631
|
target._tag === "local"
|
|
622
632
|
? local.revertJoining(request)
|
|
623
|
-
: Effect.fail(
|
|
624
|
-
crossConversationLedgerError("ledger revert joining", target.conversationId),
|
|
625
|
-
),
|
|
633
|
+
: Effect.fail(crossThreadLedgerError("ledger revert joining", target.threadId)),
|
|
626
634
|
),
|
|
627
635
|
),
|
|
628
636
|
|
|
@@ -631,7 +639,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
631
639
|
Effect.flatMap((target) =>
|
|
632
640
|
target._tag === "local"
|
|
633
641
|
? local.suspend(request)
|
|
634
|
-
: Effect.fail(
|
|
642
|
+
: Effect.fail(crossThreadLedgerError("ledger suspend", target.threadId)),
|
|
635
643
|
),
|
|
636
644
|
),
|
|
637
645
|
|
|
@@ -641,10 +649,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
641
649
|
target._tag === "local"
|
|
642
650
|
? local.recordApprovalDecision(command)
|
|
643
651
|
: Effect.fail(
|
|
644
|
-
|
|
645
|
-
"ledger record approval decision",
|
|
646
|
-
target.conversationId,
|
|
647
|
-
),
|
|
652
|
+
crossThreadLedgerError("ledger record approval decision", target.threadId),
|
|
648
653
|
),
|
|
649
654
|
),
|
|
650
655
|
),
|
|
@@ -654,9 +659,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
654
659
|
Effect.flatMap((target) =>
|
|
655
660
|
target._tag === "local"
|
|
656
661
|
? local.markUnknown(request)
|
|
657
|
-
: Effect.fail(
|
|
658
|
-
crossConversationLedgerError("ledger mark unknown", target.conversationId),
|
|
659
|
-
),
|
|
662
|
+
: Effect.fail(crossThreadLedgerError("ledger mark unknown", target.threadId)),
|
|
660
663
|
),
|
|
661
664
|
),
|
|
662
665
|
|
|
@@ -666,10 +669,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
666
669
|
target._tag === "local"
|
|
667
670
|
? local.recordUnknownResolution(command)
|
|
668
671
|
: Effect.fail(
|
|
669
|
-
|
|
670
|
-
"ledger record unknown resolution",
|
|
671
|
-
target.conversationId,
|
|
672
|
-
),
|
|
672
|
+
crossThreadLedgerError("ledger record unknown resolution", target.threadId),
|
|
673
673
|
),
|
|
674
674
|
),
|
|
675
675
|
),
|
|
@@ -679,33 +679,35 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
679
679
|
Effect.flatMap((target) =>
|
|
680
680
|
target._tag === "local"
|
|
681
681
|
? local.reserveChildBudget(request)
|
|
682
|
-
: Effect.fail(
|
|
683
|
-
crossConversationLedgerError("ledger reserve child budget", target.conversationId),
|
|
684
|
-
),
|
|
682
|
+
: Effect.fail(crossThreadLedgerError("ledger reserve child budget", target.threadId)),
|
|
685
683
|
),
|
|
686
684
|
),
|
|
687
685
|
|
|
688
|
-
// Reservation identities carry no
|
|
686
|
+
// Reservation identities carry no Thread address; the reservation row lives in the
|
|
689
687
|
// parent's own Object and these transitions are parent-lane-local by construction, so
|
|
690
688
|
// they always execute on the local facet (which fails typed for an unknown row).
|
|
691
689
|
attachChildToReservation: local.attachChildToReservation,
|
|
692
690
|
beginChildBudgetRelease: local.beginChildBudgetRelease,
|
|
693
691
|
releaseChildBudget: local.releaseChildBudget,
|
|
694
692
|
|
|
695
|
-
// The local scan IS the whole worklist: one
|
|
693
|
+
// The local scan IS the whole worklist: one Thread per Object (durability §5).
|
|
696
694
|
scanNonterminal: local.scanNonterminal,
|
|
697
695
|
|
|
696
|
+
readAbortIntent: (request) =>
|
|
697
|
+
submissionTarget("ledger read abort intent", request.submissionId).pipe(
|
|
698
|
+
Effect.flatMap((target) =>
|
|
699
|
+
target._tag === "local"
|
|
700
|
+
? local.readAbortIntent(request)
|
|
701
|
+
: Effect.fail(crossThreadLedgerError("ledger read abort intent", target.threadId)),
|
|
702
|
+
),
|
|
703
|
+
),
|
|
704
|
+
|
|
698
705
|
loadRecoverySnapshot: (request) =>
|
|
699
706
|
submissionTarget("ledger load recovery snapshot", request.submissionId).pipe(
|
|
700
707
|
Effect.flatMap((target) =>
|
|
701
708
|
target._tag === "local"
|
|
702
709
|
? local.loadRecoverySnapshot(request).pipe(Effect.flatMap(enrichChildAttachments))
|
|
703
|
-
: Effect.fail(
|
|
704
|
-
crossConversationLedgerError(
|
|
705
|
-
"ledger load recovery snapshot",
|
|
706
|
-
target.conversationId,
|
|
707
|
-
),
|
|
708
|
-
),
|
|
710
|
+
: Effect.fail(crossThreadLedgerError("ledger load recovery snapshot", target.threadId)),
|
|
709
711
|
),
|
|
710
712
|
),
|
|
711
713
|
});
|
|
@@ -716,50 +718,51 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
716
718
|
const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices")(function* (
|
|
717
719
|
options: RoutedPortOptions,
|
|
718
720
|
) {
|
|
719
|
-
const local = yield*
|
|
720
|
-
const
|
|
721
|
+
const local = yield* ThreadStore;
|
|
722
|
+
const checkpoints = local.checkpoints;
|
|
723
|
+
const recoveryCheckpoints = local.recoveryCheckpoints;
|
|
724
|
+
const transport = yield* ThreadPortTransport;
|
|
721
725
|
const transportCall: TransportCall = makeTransportCall(transport);
|
|
722
726
|
|
|
723
727
|
const routeFailure =
|
|
724
728
|
(operation: string, target: string) =>
|
|
725
|
-
(error: PortTransportError | PortProtocolError):
|
|
726
|
-
|
|
729
|
+
(error: PortTransportError | PortProtocolError): ThreadStoreError =>
|
|
730
|
+
ThreadStoreError.make({
|
|
727
731
|
operation,
|
|
728
732
|
message: boundPortDiagnostic(
|
|
729
|
-
`Routed ${operation} to the
|
|
733
|
+
`Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`,
|
|
730
734
|
),
|
|
731
735
|
cause: error,
|
|
732
736
|
});
|
|
733
737
|
|
|
734
|
-
/** The store twin of `foreignLedgerCall` with `
|
|
735
|
-
const foreignStoreCall = <
|
|
738
|
+
/** The store twin of `foreignLedgerCall` with `ThreadStoreError` as the base error. */
|
|
739
|
+
const foreignStoreCall = <ResultSchema extends Schema.Top, FailureSchema extends Schema.Top>(
|
|
736
740
|
operation: string,
|
|
737
|
-
target:
|
|
741
|
+
target: ThreadId,
|
|
738
742
|
call: PortRequest,
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
): Effect.Effect<
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
transportCall(target, call).pipe(
|
|
743
|
+
resultSchema: ResultSchema,
|
|
744
|
+
failureSchema: FailureSchema,
|
|
745
|
+
): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | ThreadStoreError> => {
|
|
746
|
+
const isExpectedResult = Schema.is(resultSchema);
|
|
747
|
+
const isExpectedFailure = Schema.is(failureSchema);
|
|
748
|
+
|
|
749
|
+
return transportCall(target, call).pipe(
|
|
746
750
|
Effect.mapError(routeFailure(operation, target)),
|
|
747
751
|
Effect.flatMap(
|
|
748
752
|
(
|
|
749
753
|
response,
|
|
750
|
-
): Effect.Effect<
|
|
751
|
-
Extract<PortResult, { readonly _tag: Tag }>,
|
|
752
|
-
ExpectedFailure | ConversationStoreError
|
|
753
|
-
> => {
|
|
754
|
+
): Effect.Effect<ResultSchema["Type"], FailureSchema["Type"] | ThreadStoreError> => {
|
|
754
755
|
if (response._tag === "PortFailed") {
|
|
755
756
|
const failure = response.failure;
|
|
757
|
+
|
|
756
758
|
if (isExpectedFailure(failure)) return Effect.fail(failure);
|
|
757
|
-
if (failure._tag === "
|
|
759
|
+
if (failure._tag === "ThreadStoreError") return Effect.fail(failure);
|
|
760
|
+
|
|
758
761
|
return Effect.fail(
|
|
759
|
-
|
|
762
|
+
ThreadStoreError.make({
|
|
760
763
|
operation,
|
|
761
764
|
message: boundPortDiagnostic(
|
|
762
|
-
`The
|
|
765
|
+
`The Thread Object owning ${target} answered ${operation} with the ` +
|
|
763
766
|
`out-of-contract failure ${failure._tag}: ${failure.message}`,
|
|
764
767
|
),
|
|
765
768
|
cause: failure,
|
|
@@ -767,16 +770,18 @@ const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices
|
|
|
767
770
|
);
|
|
768
771
|
}
|
|
769
772
|
const result = response.result;
|
|
770
|
-
|
|
773
|
+
|
|
774
|
+
if (!isExpectedResult(result)) {
|
|
771
775
|
return Effect.fail(
|
|
772
|
-
|
|
776
|
+
ThreadStoreError.make({
|
|
773
777
|
operation,
|
|
774
778
|
message:
|
|
775
|
-
`The
|
|
776
|
-
`mismatched result ${result._tag}
|
|
779
|
+
`The Thread Object owning ${target} answered ${operation} with the ` +
|
|
780
|
+
`mismatched result ${result._tag}.`,
|
|
777
781
|
}),
|
|
778
782
|
);
|
|
779
783
|
}
|
|
784
|
+
|
|
780
785
|
return Effect.succeed(result);
|
|
781
786
|
},
|
|
782
787
|
),
|
|
@@ -784,118 +789,135 @@ const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices
|
|
|
784
789
|
attributes: { operation, target },
|
|
785
790
|
}),
|
|
786
791
|
);
|
|
792
|
+
};
|
|
787
793
|
|
|
788
|
-
const routed =
|
|
794
|
+
const routed = ThreadStore.of({
|
|
789
795
|
materialize: (request) =>
|
|
790
|
-
request.
|
|
796
|
+
options.ownsThread(request.threadId)
|
|
791
797
|
? local.materialize(request)
|
|
792
798
|
: foreignStoreCall(
|
|
793
|
-
"
|
|
794
|
-
request.
|
|
799
|
+
"thread materialize",
|
|
800
|
+
request.threadId,
|
|
795
801
|
StoreMaterializeCall.make({ request }),
|
|
796
|
-
|
|
797
|
-
|
|
802
|
+
StoreMaterializeResult,
|
|
803
|
+
FenceRejected,
|
|
798
804
|
).pipe(Effect.asVoid),
|
|
799
805
|
|
|
800
806
|
append: (request) =>
|
|
801
|
-
request.
|
|
807
|
+
options.ownsThread(request.threadId)
|
|
802
808
|
? local.append(request)
|
|
803
809
|
: foreignStoreCall(
|
|
804
|
-
"
|
|
805
|
-
request.
|
|
810
|
+
"thread append",
|
|
811
|
+
request.threadId,
|
|
806
812
|
StoreAppendCall.make({ request }),
|
|
807
|
-
|
|
808
|
-
|
|
813
|
+
StoreAppendResult,
|
|
814
|
+
AppendPortFailure,
|
|
809
815
|
).pipe(Effect.map((reply) => reply.result)),
|
|
810
816
|
|
|
811
817
|
read: (request) =>
|
|
812
|
-
request.
|
|
818
|
+
options.ownsThread(request.threadId)
|
|
813
819
|
? local.read(request)
|
|
814
820
|
: Stream.unwrap(
|
|
815
821
|
foreignStoreCall(
|
|
816
|
-
"
|
|
817
|
-
request.
|
|
822
|
+
"thread read",
|
|
823
|
+
request.threadId,
|
|
818
824
|
StoreReadPageCall.make({ request }),
|
|
819
|
-
|
|
820
|
-
|
|
825
|
+
StoreReadPageResult,
|
|
826
|
+
ThreadNotMaterialized,
|
|
821
827
|
).pipe(Effect.map((reply) => Stream.fromIterable(reply.records))),
|
|
822
828
|
),
|
|
823
829
|
|
|
824
830
|
inspectTail: (request) =>
|
|
825
|
-
request.
|
|
831
|
+
options.ownsThread(request.threadId)
|
|
826
832
|
? local.inspectTail(request)
|
|
827
833
|
: foreignStoreCall(
|
|
828
|
-
"
|
|
829
|
-
request.
|
|
834
|
+
"thread inspect tail",
|
|
835
|
+
request.threadId,
|
|
830
836
|
StoreInspectTailCall.make({ request }),
|
|
831
|
-
|
|
832
|
-
|
|
837
|
+
StoreInspectTailResult,
|
|
838
|
+
ThreadNotMaterialized,
|
|
833
839
|
).pipe(Effect.map((reply) => reply.tail)),
|
|
834
840
|
|
|
835
841
|
export: (request) =>
|
|
836
|
-
request.
|
|
842
|
+
options.ownsThread(request.threadId)
|
|
837
843
|
? local.export(request)
|
|
838
844
|
: foreignStoreCall(
|
|
839
|
-
"
|
|
840
|
-
request.
|
|
845
|
+
"thread export",
|
|
846
|
+
request.threadId,
|
|
841
847
|
StoreExportCall.make({ request }),
|
|
842
|
-
|
|
843
|
-
|
|
848
|
+
StoreExportResult,
|
|
849
|
+
ThreadNotMaterialized,
|
|
844
850
|
).pipe(Effect.map((reply) => reply.export)),
|
|
845
851
|
|
|
846
|
-
// Observation and checkpoints are lane-local
|
|
847
|
-
//
|
|
848
|
-
//
|
|
852
|
+
// Observation and checkpoints are lane-local: the closed route-capable store subset
|
|
853
|
+
// is materialize/append/read/inspectTail/export. Recovery cache misses can fall back
|
|
854
|
+
// to those canonical reads, including when the cache belongs to a foreign Object.
|
|
849
855
|
observe: (request) =>
|
|
850
|
-
request.
|
|
856
|
+
options.ownsThread(request.threadId)
|
|
851
857
|
? local.observe(request)
|
|
852
|
-
: Stream.unwrap(
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
858
|
+
: Stream.unwrap(Effect.fail(crossThreadStoreError("thread observe", request.threadId))),
|
|
859
|
+
|
|
860
|
+
...(recoveryCheckpoints === undefined
|
|
861
|
+
? {}
|
|
862
|
+
: {
|
|
863
|
+
recoveryCheckpoints: {
|
|
864
|
+
save: (request) =>
|
|
865
|
+
options.ownsThread(request.checkpoint.threadId)
|
|
866
|
+
? recoveryCheckpoints.save(request)
|
|
867
|
+
: Effect.fail(
|
|
868
|
+
crossThreadStoreError(
|
|
869
|
+
"thread save recovery checkpoint",
|
|
870
|
+
request.checkpoint.threadId,
|
|
871
|
+
),
|
|
872
|
+
),
|
|
873
|
+
load: (request) =>
|
|
874
|
+
options.ownsThread(request.threadId)
|
|
875
|
+
? recoveryCheckpoints.load(request)
|
|
876
|
+
: Effect.succeed(Option.none()),
|
|
877
|
+
},
|
|
878
|
+
}),
|
|
879
|
+
...(checkpoints === undefined
|
|
880
|
+
? {}
|
|
881
|
+
: {
|
|
882
|
+
checkpoints: {
|
|
883
|
+
save: (request) =>
|
|
884
|
+
options.ownsThread(request.checkpoint.threadId)
|
|
885
|
+
? checkpoints.save(request)
|
|
886
|
+
: Effect.fail(
|
|
887
|
+
crossThreadStoreError("thread save checkpoint", request.checkpoint.threadId),
|
|
888
|
+
),
|
|
889
|
+
load: (request) =>
|
|
890
|
+
options.ownsThread(request.threadId)
|
|
891
|
+
? checkpoints.load(request)
|
|
892
|
+
: Effect.fail(crossThreadStoreError("thread load checkpoint", request.threadId)),
|
|
893
|
+
},
|
|
894
|
+
}),
|
|
874
895
|
});
|
|
875
896
|
|
|
876
|
-
return Context.make(
|
|
897
|
+
return Context.make(ThreadStore, routed);
|
|
877
898
|
});
|
|
878
899
|
|
|
879
900
|
/**
|
|
880
901
|
* Routing decorator over the LOCAL `SubmissionLedger` facet (plan §1.3): a request addressing
|
|
881
|
-
* this Object's
|
|
882
|
-
*
|
|
902
|
+
* this Object's Thread executes locally; a route-capable request addressing another
|
|
903
|
+
* Thread is Schema-encoded onto the `ThreadPortTransport` and executed by the
|
|
883
904
|
* owning Object's local facet; any other foreign request fails fast typed. Provide the WP1
|
|
884
905
|
* local facet (`submissionLedgerLayer`/`ledgerLayer`) and a transport to close it.
|
|
885
906
|
*/
|
|
886
907
|
export const routedSubmissionLedgerLayer = (
|
|
887
908
|
options: RoutedPortOptions,
|
|
888
|
-
): Layer.Layer<SubmissionLedger, never, SubmissionLedger |
|
|
909
|
+
): Layer.Layer<SubmissionLedger, never, SubmissionLedger | ThreadPortTransport> =>
|
|
889
910
|
Layer.effectContext(makeRoutedLedgerServices(options));
|
|
890
911
|
|
|
891
912
|
/**
|
|
892
|
-
* Routing decorator over the LOCAL `
|
|
913
|
+
* Routing decorator over the LOCAL `ThreadStore` facet (plan §1.3): this-thread
|
|
893
914
|
* requests execute locally; foreign materialize/append/read/inspectTail/export travel the
|
|
894
|
-
* transport
|
|
915
|
+
* transport. Foreign recovery-cache loads return none for canonical replay; other foreign
|
|
916
|
+
* checkpoint operations and observation fail fast typed.
|
|
895
917
|
*/
|
|
896
|
-
export const
|
|
918
|
+
export const routedThreadStoreLayer = (
|
|
897
919
|
options: RoutedPortOptions,
|
|
898
|
-
): Layer.Layer<
|
|
920
|
+
): Layer.Layer<ThreadStore, never, ThreadStore | ThreadPortTransport> =>
|
|
899
921
|
Layer.effectContext(makeRoutedStoreServices(options));
|
|
900
922
|
|
|
901
923
|
// ---------------------------------------------------------------------------
|
|
@@ -915,16 +937,17 @@ const capture = <Failure extends PortFailure>(
|
|
|
915
937
|
* Execute one decoded port request against THIS Object's LOCAL facets — the owner-side half
|
|
916
938
|
* of the routed ports (plan §1.3). Callers must provide the WP1 local facets, never the
|
|
917
939
|
* routed decorators: the routing layer already established that this Object owns the
|
|
918
|
-
* addressed
|
|
940
|
+
* addressed Thread, and re-routing here could bounce a request between Objects.
|
|
919
941
|
* Failures never escape — every typed port failure becomes a `PortFailed` envelope that
|
|
920
942
|
* re-decodes on the caller side.
|
|
921
943
|
*/
|
|
922
944
|
export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(function* (
|
|
923
945
|
request: PortRequest,
|
|
924
|
-
): Effect.fn.Return<PortResponse, never, SubmissionLedger |
|
|
946
|
+
): Effect.fn.Return<PortResponse, never, SubmissionLedger | ThreadStore> {
|
|
925
947
|
switch (request._tag) {
|
|
926
948
|
case "LedgerAdmit": {
|
|
927
949
|
const ledger = yield* SubmissionLedger;
|
|
950
|
+
|
|
928
951
|
return yield* capture(
|
|
929
952
|
ledger
|
|
930
953
|
.admit(request.request)
|
|
@@ -933,12 +956,14 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
|
|
|
933
956
|
}
|
|
934
957
|
case "LedgerMarkReady": {
|
|
935
958
|
const ledger = yield* SubmissionLedger;
|
|
959
|
+
|
|
936
960
|
return yield* capture(
|
|
937
961
|
ledger.markReady(request.request).pipe(Effect.map(() => LedgerMarkReadyResult.make({}))),
|
|
938
962
|
);
|
|
939
963
|
}
|
|
940
964
|
case "LedgerLookup": {
|
|
941
965
|
const ledger = yield* SubmissionLedger;
|
|
966
|
+
|
|
942
967
|
return yield* capture(
|
|
943
968
|
ledger
|
|
944
969
|
.lookup(request.request)
|
|
@@ -953,6 +978,7 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
|
|
|
953
978
|
}
|
|
954
979
|
case "LedgerResolveAdmission": {
|
|
955
980
|
const ledger = yield* SubmissionLedger;
|
|
981
|
+
|
|
956
982
|
return yield* capture(
|
|
957
983
|
ledger
|
|
958
984
|
.resolveAdmission(request.request)
|
|
@@ -961,6 +987,7 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
|
|
|
961
987
|
}
|
|
962
988
|
case "LedgerRequestAbort": {
|
|
963
989
|
const ledger = yield* SubmissionLedger;
|
|
990
|
+
|
|
964
991
|
return yield* capture(
|
|
965
992
|
ledger
|
|
966
993
|
.requestAbort(request.request)
|
|
@@ -969,6 +996,7 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
|
|
|
969
996
|
}
|
|
970
997
|
case "LedgerRecordChildSettled": {
|
|
971
998
|
const ledger = yield* SubmissionLedger;
|
|
999
|
+
|
|
972
1000
|
return yield* capture(
|
|
973
1001
|
ledger
|
|
974
1002
|
.recordChildSettled(request.request)
|
|
@@ -976,13 +1004,15 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
|
|
|
976
1004
|
);
|
|
977
1005
|
}
|
|
978
1006
|
case "StoreMaterialize": {
|
|
979
|
-
const store = yield*
|
|
1007
|
+
const store = yield* ThreadStore;
|
|
1008
|
+
|
|
980
1009
|
return yield* capture(
|
|
981
1010
|
store.materialize(request.request).pipe(Effect.map(() => StoreMaterializeResult.make({}))),
|
|
982
1011
|
);
|
|
983
1012
|
}
|
|
984
1013
|
case "StoreAppend": {
|
|
985
|
-
const store = yield*
|
|
1014
|
+
const store = yield* ThreadStore;
|
|
1015
|
+
|
|
986
1016
|
return yield* capture(
|
|
987
1017
|
store
|
|
988
1018
|
.append(request.request)
|
|
@@ -990,7 +1020,8 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
|
|
|
990
1020
|
);
|
|
991
1021
|
}
|
|
992
1022
|
case "StoreReadPage": {
|
|
993
|
-
const store = yield*
|
|
1023
|
+
const store = yield* ThreadStore;
|
|
1024
|
+
|
|
994
1025
|
return yield* capture(
|
|
995
1026
|
store.read(request.request).pipe(
|
|
996
1027
|
Stream.runCollect,
|
|
@@ -999,7 +1030,8 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
|
|
|
999
1030
|
);
|
|
1000
1031
|
}
|
|
1001
1032
|
case "StoreInspectTail": {
|
|
1002
|
-
const store = yield*
|
|
1033
|
+
const store = yield* ThreadStore;
|
|
1034
|
+
|
|
1003
1035
|
return yield* capture(
|
|
1004
1036
|
store
|
|
1005
1037
|
.inspectTail(request.request)
|
|
@@ -1007,15 +1039,12 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
|
|
|
1007
1039
|
);
|
|
1008
1040
|
}
|
|
1009
1041
|
case "StoreExport": {
|
|
1010
|
-
const store = yield*
|
|
1042
|
+
const store = yield* ThreadStore;
|
|
1043
|
+
|
|
1011
1044
|
return yield* capture(
|
|
1012
1045
|
store
|
|
1013
1046
|
.export(request.request)
|
|
1014
|
-
.pipe(
|
|
1015
|
-
Effect.map((conversationExport) =>
|
|
1016
|
-
StoreExportResult.make({ export: conversationExport }),
|
|
1017
|
-
),
|
|
1018
|
-
),
|
|
1047
|
+
.pipe(Effect.map((threadExport) => StoreExportResult.make({ export: threadExport }))),
|
|
1019
1048
|
);
|
|
1020
1049
|
}
|
|
1021
1050
|
}
|
|
@@ -1039,9 +1068,7 @@ const encodedProtocolFailure = (message: string): unknown => ({
|
|
|
1039
1068
|
* transport never has to interpret exceptions as protocol answers.
|
|
1040
1069
|
*/
|
|
1041
1070
|
export const handleEncodedPortRequest = Effect.fn("DoPortRouting.handleEncodedPortRequest")(
|
|
1042
|
-
function* (
|
|
1043
|
-
encoded: unknown,
|
|
1044
|
-
): Effect.fn.Return<unknown, never, SubmissionLedger | ConversationStore> {
|
|
1071
|
+
function* (encoded: unknown): Effect.fn.Return<unknown, never, SubmissionLedger | ThreadStore> {
|
|
1045
1072
|
const response = yield* decodePortRequest(encoded).pipe(
|
|
1046
1073
|
Effect.flatMap(executePortRequest),
|
|
1047
1074
|
Effect.catch((error) =>
|
|
@@ -1056,6 +1083,7 @@ export const handleEncodedPortRequest = Effect.fn("DoPortRouting.handleEncodedPo
|
|
|
1056
1083
|
),
|
|
1057
1084
|
),
|
|
1058
1085
|
);
|
|
1086
|
+
|
|
1059
1087
|
return yield* encodePortResponse(response).pipe(
|
|
1060
1088
|
Effect.catch((error) =>
|
|
1061
1089
|
Effect.succeed(
|