@effect-agent/storage-cloudflare 0.1.0-beta.23 → 0.1.0-beta.25
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/index.mjs +117 -85
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/do-conversation-store.ts +20 -15
- package/src/do-journal.ts +3 -4
- package/src/do-ledger.ts +60 -29
- package/src/do-storage-failpoint.ts +1 -1
- package/src/routing.ts +74 -78
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbortCommand, AbortIntent, AdmissionAdmitted, AdmissionConflict, AdmissionIndeterminate, AdmissionNotAdmitted, AdmissionRequest, AdmissionResolution, AdmissionResult, AppendConflict, AppendResult, ApprovalConflict, ApprovalDecision, ApprovalDecisionCommand, ApprovalDecisionIntent, AttachChildToReservationRequest, BeginChildBudgetReleaseRequest, CanonicalBatch, CanonicalRecord, CanonicalRecordEnvelope, CanonicalSequence, CheckpointRejected, ChildAttachmentSnapshot, ChildBudgetReservationRequest, ChildBudgetReservationSnapshot, ChildReservationConflict, ChildReservationStatus, ChildSettledNotification, ChildSettledOutcome, Claim, ClaimJoiningRequest, ClaimRequest, ConversationCheckpoint, ConversationExport, ConversationExportRequest, ConversationMaterialization, ConversationNotMaterialized, ConversationObservation, ConversationRead, ConversationStore, ConversationStoreError, ConversationTail, ConversationTailRequest, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, Digest, EMPTY_TAIL_DIGEST, FenceRejected, FencedAppendRequest, InputAppliedMarker, JoinSnapshot, JoinedToHost, JoiningClaim, LedgerCapabilities, LedgerError, LoadCheckpointRequest, MarkInputAppliedRequest, MarkJoinedRequest, MarkReadyRequest, MarkUnknownRequest, ObservationOffset, OwnershipLost, OwnershipRenewal, OwnershipSnapshot, ParentLinkage, PersistedJson, ProducerEpoch, QueueSequence, RecordEnvelope, RecoverySnapshot, RecoverySnapshotRequest, ReleaseChildBudgetRequest, ReleaseOwnershipRequest, RenewOwnershipRequest, ReservedChildBudget, ReservedSettlement, RevertJoiningRequest, SaveCheckpointRequest, Settlement, SettlementConflict, SettlementFinalization, SettlementOutcome, SettlementReservation, SettlementReservationSnapshot, SubmissionLedger, SubmissionLookup, SubmissionLookupById, SubmissionLookupByKey, SubmissionSnapshot, SubmissionState, SuspendRequest, SuspensionReason, SuspensionSnapshot, UnknownResolution, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, digestCanonicalBatch, settlementFailureFromRecord, submissionAbortRecordId } from "@effect-agent/session";
|
|
2
|
-
import { Clock, Context, Crypto, DateTime, Duration, Effect, Layer, Option, Ref, Schema, Stream } from "effect";
|
|
2
|
+
import { Clock, Context, Crypto, DateTime, Duration, Effect, Layer, Option, Predicate, Ref, Schema, Stream } from "effect";
|
|
3
3
|
import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-do";
|
|
4
4
|
import * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
5
5
|
import { BrowserCrypto } from "@effect/platform-browser";
|
|
@@ -394,6 +394,7 @@ const MAX_RECORDS_PER_CONVERSATION = 65536;
|
|
|
394
394
|
const MAX_IDENTIFIER_LENGTH$1 = 1024;
|
|
395
395
|
/** Durable Object SQL storage allows at most 100 bound parameters per statement. */
|
|
396
396
|
const MAX_BOUND_PARAMETERS = 100;
|
|
397
|
+
const isSqlError = Schema.is(SqlError);
|
|
397
398
|
const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
|
|
398
399
|
const chunked = (values, size) => {
|
|
399
400
|
const chunks = [];
|
|
@@ -587,7 +588,7 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
587
588
|
* nested transactions, so new journal operations must not wrap this helper inside another
|
|
588
589
|
* transaction.
|
|
589
590
|
*/
|
|
590
|
-
const withWriteTransaction = (operation) => (effect) => sql.withTransaction(effect).pipe(Effect.mapError((error) => error
|
|
591
|
+
const withWriteTransaction = (operation) => (effect) => sql.withTransaction(effect).pipe(Effect.mapError((error) => isSqlError(error) ? storageError(operation)(error) : error), Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }));
|
|
591
592
|
const materialize = Effect.fn("DoJournal.materialize")(function* (conversationId, createdAt, emptyTailDigest, producerEpoch) {
|
|
592
593
|
if (conversationId.length > MAX_IDENTIFIER_LENGTH$1) return yield* DoStorageError.make({
|
|
593
594
|
operation: "materialize conversation",
|
|
@@ -1119,6 +1120,9 @@ const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
|
|
|
1119
1120
|
const DO_OFFSET_PREFIX = "effect-agent-do@1:";
|
|
1120
1121
|
const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
|
|
1121
1122
|
const isDigest = Schema.is(Digest);
|
|
1123
|
+
const isDoFenceRejected = Schema.is(DoFenceRejected);
|
|
1124
|
+
const isDoAppendConflict = Schema.is(DoAppendConflict);
|
|
1125
|
+
const isDoCheckpointConflict = Schema.is(DoCheckpointConflict);
|
|
1122
1126
|
const storeError = (operation, error) => ConversationStoreError.make({
|
|
1123
1127
|
cause: error,
|
|
1124
1128
|
operation,
|
|
@@ -1347,8 +1351,8 @@ const makeServices$1 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1347
1351
|
}).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
|
|
1348
1352
|
yield* hitFailpoint("append:before");
|
|
1349
1353
|
const result = yield* journal.append(rawRequest).pipe(Effect.mapError((error) => {
|
|
1350
|
-
if (error
|
|
1351
|
-
if (error
|
|
1354
|
+
if (isDoFenceRejected(error)) return mapFence(validated.conversationId, error);
|
|
1355
|
+
if (isDoAppendConflict(error)) return error.actualTailSequence !== void 0 && isDigest(error.actualTailDigest) ? AppendConflict.make({
|
|
1352
1356
|
conversationId: validated.conversationId,
|
|
1353
1357
|
batchId: validated.batch.batchId,
|
|
1354
1358
|
reason: error.reason,
|
|
@@ -1449,7 +1453,7 @@ const makeServices$1 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1449
1453
|
checkpointJson
|
|
1450
1454
|
});
|
|
1451
1455
|
yield* hitFailpoint("save-checkpoint:before");
|
|
1452
|
-
yield* journal.saveCheckpoint(raw).pipe(Effect.mapError((error) => error
|
|
1456
|
+
yield* journal.saveCheckpoint(raw).pipe(Effect.mapError((error) => isDoCheckpointConflict(error) ? CheckpointRejected.make({
|
|
1453
1457
|
conversationId: validated.checkpoint.conversationId,
|
|
1454
1458
|
reason: "digest-mismatch"
|
|
1455
1459
|
}) : storeError("save checkpoint", error)));
|
|
@@ -1511,10 +1515,7 @@ const storageFailpointLayer = (options) => options.failpoint === void 0 ? DoStor
|
|
|
1511
1515
|
* port; point both at the SAME `ctx.storage` so claims fence the same producer epochs
|
|
1512
1516
|
* (ADR-0011 D7's "same file" rule, transposed to one object's private database).
|
|
1513
1517
|
*/
|
|
1514
|
-
const layer = (options) => {
|
|
1515
|
-
const sqlLayer = SqliteClient.layer({ storage: options.storage });
|
|
1516
|
-
return conversationStoreLayer.pipe(Layer.provide(Layer.mergeAll(storageConfigLayer(options), storageFailpointLayer(options), sqlLayer, BrowserCrypto.layer)));
|
|
1517
|
-
};
|
|
1518
|
+
const layer = (options) => Layer.unwrap(Effect.map(DoStorageConfig, (config) => conversationStoreLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(DoStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ storage: options.storage }), BrowserCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
|
|
1518
1519
|
/** Create an adapter-owned resumable observation offset for a known canonical sequence. */
|
|
1519
1520
|
const observationOffsetAt = makeOffset;
|
|
1520
1521
|
//#endregion
|
|
@@ -1528,6 +1529,11 @@ const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
|
|
|
1528
1529
|
const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
|
|
1529
1530
|
const SCAN_PAGE_SIZE = 256;
|
|
1530
1531
|
const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
|
|
1532
|
+
const RESUME_IMMEDIATELY = "resume-immediately";
|
|
1533
|
+
const SUSPENDED = "suspended";
|
|
1534
|
+
const NOT_WAITING = "not-waiting";
|
|
1535
|
+
const STILL_WAITING = "still-waiting";
|
|
1536
|
+
const WOKEN = "woken";
|
|
1531
1537
|
const MAX_IDENTIFIER_LENGTH = 1024;
|
|
1532
1538
|
var SubmissionRow = class extends Schema.Class("SubmissionRow")({
|
|
1533
1539
|
submission_id: BoundedIdentifier,
|
|
@@ -1687,6 +1693,9 @@ const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResoluti
|
|
|
1687
1693
|
const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
|
|
1688
1694
|
const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(ChildBudgetReservationSnapshot);
|
|
1689
1695
|
const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
|
|
1696
|
+
const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
|
|
1697
|
+
const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
|
|
1698
|
+
const isDoStorageError = Schema.is(DoStorageError);
|
|
1690
1699
|
/** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
|
|
1691
1700
|
const internalFailure = (operation) => (error) => LedgerError.make({
|
|
1692
1701
|
operation,
|
|
@@ -1720,7 +1729,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1720
1729
|
* ownership-token and epoch checks are atomic with their writes (DUR-006). Transaction
|
|
1721
1730
|
* failures surface as LedgerError carrying the typed `DoStorageError` as cause.
|
|
1722
1731
|
*/
|
|
1723
|
-
const inWriteTransaction = (operation, effect) => journal.withWriteTransaction(operation)(effect).pipe(Effect.mapError((error) => error
|
|
1732
|
+
const inWriteTransaction = (operation, effect) => journal.withWriteTransaction(operation)(effect).pipe(Effect.mapError((error) => isDoStorageError(error) ? internalFailure(operation)(error) : error));
|
|
1724
1733
|
const mintUuid = (operation) => crypto.randomUUIDv7.pipe(Effect.mapError((error) => internalFailure(operation)(error)));
|
|
1725
1734
|
const currentInstant = Effect.map(Clock.currentTimeMillis, (millis) => ({
|
|
1726
1735
|
millis,
|
|
@@ -2607,7 +2616,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2607
2616
|
if (validated.reason._tag === "ApprovalPending") {
|
|
2608
2617
|
const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
2609
2618
|
const decided = new Set(decisions.map((row) => row.tool_call_id));
|
|
2610
|
-
if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return
|
|
2619
|
+
if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return RESUME_IMMEDIATELY;
|
|
2611
2620
|
} else {
|
|
2612
2621
|
const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
|
|
2613
2622
|
const markerChildren = new Set(markers.map((row) => row.child_submission_id));
|
|
@@ -2616,7 +2625,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2616
2625
|
allSettled = false;
|
|
2617
2626
|
break;
|
|
2618
2627
|
}
|
|
2619
|
-
if (allSettled) return
|
|
2628
|
+
if (allSettled) return RESUME_IMMEDIATELY;
|
|
2620
2629
|
}
|
|
2621
2630
|
const now = yield* currentInstant;
|
|
2622
2631
|
yield* sql`
|
|
@@ -2631,7 +2640,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2631
2640
|
DELETE FROM effect_agent_submission_ownership
|
|
2632
2641
|
WHERE submission_id = ${validated.submissionId}
|
|
2633
2642
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2634
|
-
return
|
|
2643
|
+
return SUSPENDED;
|
|
2635
2644
|
}));
|
|
2636
2645
|
yield* hitFailpoint("ledger:suspend:after", operation);
|
|
2637
2646
|
return outcome;
|
|
@@ -2759,12 +2768,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2759
2768
|
});
|
|
2760
2769
|
}
|
|
2761
2770
|
const existing = (yield* readUnknownResolutions(operation, validated.submissionId)).find((row) => row.tool_call_id === validated.toolCallId);
|
|
2762
|
-
|
|
2771
|
+
const existingIntent = existing === void 0 ? void 0 : yield* unknownResolutionIntentFromRow(operation, existing);
|
|
2772
|
+
if (existingIntent !== void 0 && !equivalentUnknownResolution(existingIntent.resolution, validated.resolution)) return yield* UnknownResolutionConflict.make({
|
|
2763
2773
|
submissionId: validated.submissionId,
|
|
2764
2774
|
toolCallId: validated.toolCallId
|
|
2765
2775
|
});
|
|
2766
2776
|
let resolved;
|
|
2767
|
-
if (
|
|
2777
|
+
if (existingIntent !== void 0) resolved = existingIntent;
|
|
2768
2778
|
else {
|
|
2769
2779
|
const now = yield* currentInstant;
|
|
2770
2780
|
yield* sql`
|
|
@@ -2839,13 +2849,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2839
2849
|
)
|
|
2840
2850
|
ON CONFLICT (parent_submission_id, child_submission_id) DO NOTHING
|
|
2841
2851
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2842
|
-
if (parent.state !== "suspended" || parent.suspended_reason_json === null) return
|
|
2852
|
+
if (parent.state !== "suspended" || parent.suspended_reason_json === null) return NOT_WAITING;
|
|
2843
2853
|
const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(parent.suspended_reason_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submissions", parent.submission_id, error.message)));
|
|
2844
|
-
if (reason._tag !== "WaitingForChild") return
|
|
2845
|
-
if (!reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)) return
|
|
2854
|
+
if (reason._tag !== "WaitingForChild") return NOT_WAITING;
|
|
2855
|
+
if (!reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)) return NOT_WAITING;
|
|
2846
2856
|
const markers = yield* readChildSettlementMarkers(operation, validated.parentSubmissionId);
|
|
2847
2857
|
const markerChildren = new Set(markers.map((row) => row.child_submission_id));
|
|
2848
|
-
for (const entry of reason.children) if (!(yield* childProvablySettled(operation, markerChildren, entry.childSubmissionId))) return
|
|
2858
|
+
for (const entry of reason.children) if (!(yield* childProvablySettled(operation, markerChildren, entry.childSubmissionId))) return STILL_WAITING;
|
|
2849
2859
|
yield* sql`
|
|
2850
2860
|
UPDATE effect_agent_submissions
|
|
2851
2861
|
SET
|
|
@@ -2854,7 +2864,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2854
2864
|
suspended_at = NULL
|
|
2855
2865
|
WHERE submission_id = ${validated.parentSubmissionId}
|
|
2856
2866
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2857
|
-
return
|
|
2867
|
+
return WOKEN;
|
|
2858
2868
|
}));
|
|
2859
2869
|
yield* hitFailpoint("ledger:child-settled:after", operation);
|
|
2860
2870
|
return outcome;
|
|
@@ -2867,13 +2877,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2867
2877
|
const reserved = yield* inWriteTransaction(operation, Effect.gen(function* () {
|
|
2868
2878
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
2869
2879
|
if (Option.isSome(existing)) {
|
|
2870
|
-
|
|
2880
|
+
const existingSnapshot = yield* childReservationSnapshotFromRow(operation, existing.value);
|
|
2881
|
+
if (!(existing.value.parent_submission_id === validated.parentSubmissionId && existing.value.parent_tool_call_id === validated.parentToolCallId && existing.value.allocation_digest === validated.allocationDigest && equivalentPersistedJson(existingSnapshot.allocation, validated.allocation))) return yield* ChildReservationConflict.make({
|
|
2871
2882
|
reservationId: validated.reservationId,
|
|
2872
2883
|
status: existing.value.status,
|
|
2873
2884
|
message: "A reservation with this identity exists with a different parent Tool Call or allocation."
|
|
2874
2885
|
});
|
|
2875
2886
|
return ReservedChildBudget.make({
|
|
2876
|
-
reservation:
|
|
2887
|
+
reservation: existingSnapshot,
|
|
2877
2888
|
replayed: true
|
|
2878
2889
|
});
|
|
2879
2890
|
}
|
|
@@ -2964,7 +2975,8 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2964
2975
|
message: `Unknown child reservation ${validated.reservationId}.`
|
|
2965
2976
|
});
|
|
2966
2977
|
if (existing.value.status !== "reserved") {
|
|
2967
|
-
|
|
2978
|
+
const existingSnapshot = yield* childReservationSnapshotFromRow(operation, existing.value);
|
|
2979
|
+
if (existingSnapshot.accounting !== void 0 && equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)) return existingSnapshot;
|
|
2968
2980
|
return yield* ChildReservationConflict.make({
|
|
2969
2981
|
reservationId: validated.reservationId,
|
|
2970
2982
|
status: existing.value.status,
|
|
@@ -3201,7 +3213,7 @@ const submissionLedgerLayer = Layer.effectContext(makeServices());
|
|
|
3201
3213
|
* A composition-root convenience Layer for the durable Submission Ledger. Point it at the
|
|
3202
3214
|
* same `ctx.storage` as the ConversationStore so claims fence the same producer epochs.
|
|
3203
3215
|
*/
|
|
3204
|
-
const ledgerLayer = (options) => submissionLedgerLayer.pipe(Layer.provide(Layer.mergeAll(
|
|
3216
|
+
const ledgerLayer = (options) => Layer.unwrap(Effect.map(DoStorageConfig, (config) => submissionLedgerLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(DoStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ storage: options.storage }), BrowserCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
|
|
3205
3217
|
//#endregion
|
|
3206
3218
|
//#region src/port-protocol.ts
|
|
3207
3219
|
/**
|
|
@@ -3357,20 +3369,32 @@ var PortTransportError = class extends Schema.TaggedError()("PortTransportError"
|
|
|
3357
3369
|
retryable: Schema.optionalKey(Schema.Boolean),
|
|
3358
3370
|
cause: Schema.optionalKey(Schema.Defect())
|
|
3359
3371
|
}) {};
|
|
3372
|
+
const safeTransportDiagnostic = (cause) => {
|
|
3373
|
+
try {
|
|
3374
|
+
const message = cause instanceof Error ? cause.message : cause;
|
|
3375
|
+
return boundPortDiagnostic(typeof message === "string" ? message : String(message));
|
|
3376
|
+
} catch {
|
|
3377
|
+
return "[unavailable transport diagnostic]";
|
|
3378
|
+
}
|
|
3379
|
+
};
|
|
3380
|
+
const transportRetryableSignal = (cause) => {
|
|
3381
|
+
if (!Predicate.isObjectKeyword(cause)) return void 0;
|
|
3382
|
+
try {
|
|
3383
|
+
const signal = Reflect.get(cause, "retryable");
|
|
3384
|
+
return typeof signal === "boolean" ? signal : void 0;
|
|
3385
|
+
} catch {
|
|
3386
|
+
return;
|
|
3387
|
+
}
|
|
3388
|
+
};
|
|
3360
3389
|
/**
|
|
3361
3390
|
* Build a `PortTransportError` from an arbitrary thrown transport cause, preserving the
|
|
3362
3391
|
* platform stub's own `retryable` signal when present.
|
|
3363
3392
|
*/
|
|
3364
3393
|
const portTransportFailure = (target, cause) => {
|
|
3365
|
-
const
|
|
3366
|
-
let retryable;
|
|
3367
|
-
if (typeof cause === "object" && cause !== null && "retryable" in cause) {
|
|
3368
|
-
const signal = cause.retryable;
|
|
3369
|
-
if (typeof signal === "boolean") retryable = signal;
|
|
3370
|
-
}
|
|
3394
|
+
const retryable = transportRetryableSignal(cause);
|
|
3371
3395
|
return PortTransportError.make({
|
|
3372
3396
|
target,
|
|
3373
|
-
message:
|
|
3397
|
+
message: safeTransportDiagnostic(cause),
|
|
3374
3398
|
...retryable === void 0 ? {} : { retryable },
|
|
3375
3399
|
cause
|
|
3376
3400
|
});
|
|
@@ -3409,13 +3433,13 @@ const routableSubmissionTarget = (localConversationId) => Effect.fn("DoPortRouti
|
|
|
3409
3433
|
conversationId
|
|
3410
3434
|
})), Effect.orElseSucceed(() => LOCAL));
|
|
3411
3435
|
});
|
|
3412
|
-
const
|
|
3413
|
-
const
|
|
3414
|
-
const
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3436
|
+
const NoAdditionalPortFailure = Schema.Never;
|
|
3437
|
+
const AbortPortFailure = Schema.Union([SettlementConflict, JoinedToHost]);
|
|
3438
|
+
const AppendPortFailure = Schema.Union([
|
|
3439
|
+
ConversationNotMaterialized,
|
|
3440
|
+
AppendConflict,
|
|
3441
|
+
FenceRejected
|
|
3442
|
+
]);
|
|
3419
3443
|
/**
|
|
3420
3444
|
* The fail-fast refusal for any foreign operation OUTSIDE the closed route-capable subset
|
|
3421
3445
|
* (plan §1.3): honesty over accidental distribution.
|
|
@@ -3448,27 +3472,31 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
3448
3472
|
* any failure outside the operation's declared surface — including protocol anomalies — is
|
|
3449
3473
|
* folded into a `LedgerError` naming the anomaly instead of being erased or re-thrown raw.
|
|
3450
3474
|
*/
|
|
3451
|
-
const foreignLedgerCall = (operation, target, call,
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
if (
|
|
3456
|
-
|
|
3475
|
+
const foreignLedgerCall = (operation, target, call, resultSchema, failureSchema) => {
|
|
3476
|
+
const isExpectedResult = Schema.is(resultSchema);
|
|
3477
|
+
const isExpectedFailure = Schema.is(failureSchema);
|
|
3478
|
+
return transportCall(target, call).pipe(Effect.mapError(routeFailure(operation, target)), Effect.flatMap((response) => {
|
|
3479
|
+
if (response._tag === "PortFailed") {
|
|
3480
|
+
const failure = response.failure;
|
|
3481
|
+
if (isExpectedFailure(failure)) return Effect.fail(failure);
|
|
3482
|
+
if (failure._tag === "LedgerError") return Effect.fail(failure);
|
|
3483
|
+
return Effect.fail(LedgerError.make({
|
|
3484
|
+
operation,
|
|
3485
|
+
message: boundPortDiagnostic(`The Conversation Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
|
|
3486
|
+
cause: failure
|
|
3487
|
+
}));
|
|
3488
|
+
}
|
|
3489
|
+
const result = response.result;
|
|
3490
|
+
if (!isExpectedResult(result)) return Effect.fail(LedgerError.make({
|
|
3457
3491
|
operation,
|
|
3458
|
-
message:
|
|
3459
|
-
cause: failure
|
|
3492
|
+
message: `The Conversation Object owning ${target} answered ${operation} with the mismatched result ${result._tag}.`
|
|
3460
3493
|
}));
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
if (!isResultTag(resultTag)(result)) return Effect.fail(LedgerError.make({
|
|
3494
|
+
return Effect.succeed(result);
|
|
3495
|
+
}), Effect.withSpan("DoPortRouting.foreignLedgerCall", { attributes: {
|
|
3464
3496
|
operation,
|
|
3465
|
-
|
|
3466
|
-
}));
|
|
3467
|
-
|
|
3468
|
-
}), Effect.withSpan("DoPortRouting.foreignLedgerCall", { attributes: {
|
|
3469
|
-
operation,
|
|
3470
|
-
target
|
|
3471
|
-
} }));
|
|
3497
|
+
target
|
|
3498
|
+
} }));
|
|
3499
|
+
};
|
|
3472
3500
|
/**
|
|
3473
3501
|
* Routed `resolveAdmission` — where the S2 tri-state becomes real (plan §1.3): when the
|
|
3474
3502
|
* owning Object cannot be reached, or its answer cannot be understood, the routed adapter
|
|
@@ -3488,7 +3516,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
3488
3516
|
PortTransportError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Conversation Object owning ${target} is unreachable: ${error.message}`) })),
|
|
3489
3517
|
PortProtocolError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The answer of the Conversation Object owning ${target} could not be understood: ${error.message}`) }))
|
|
3490
3518
|
}), Effect.withSpan("DoPortRouting.resolveForeignAdmission", { attributes: { target } }));
|
|
3491
|
-
const foreignLookupById = (operation, target, submissionId) => foreignLedgerCall(operation, target, LedgerLookupCall.make({ request: SubmissionLookupById.make({ submissionId }) }),
|
|
3519
|
+
const foreignLookupById = (operation, target, submissionId) => foreignLedgerCall(operation, target, LedgerLookupCall.make({ request: SubmissionLookupById.make({ submissionId }) }), LedgerLookupResult, NoAdditionalPortFailure).pipe(Effect.map((result) => result.submission === void 0 ? Option.none() : Option.some(result.submission)));
|
|
3492
3520
|
/**
|
|
3493
3521
|
* Enrich a LOCAL parent's recovery snapshot with the lane state of attached children whose
|
|
3494
3522
|
* rows live in other Durable Objects (plan §1.3): markers first (the local facet already
|
|
@@ -3529,12 +3557,12 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
3529
3557
|
});
|
|
3530
3558
|
const routed = SubmissionLedger.of({
|
|
3531
3559
|
capabilities: local.capabilities,
|
|
3532
|
-
admit: (request) => request.conversationId === options.localConversationId ? local.admit(request) : foreignLedgerCall("ledger admit", request.conversationId, LedgerAdmitCall.make({ request }),
|
|
3533
|
-
markReady: (request) => submissionTarget("ledger mark ready", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markReady(request) : foreignLedgerCall("ledger mark ready", target.conversationId, LedgerMarkReadyCall.make({ request }),
|
|
3534
|
-
lookup: (request) => request._tag === "SubmissionLookupById" ? submissionTarget("ledger lookup", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.lookup(request) : foreignLookupById("ledger lookup", target.conversationId, request.submissionId))) : request.conversationId === options.localConversationId ? local.lookup(request) : foreignLedgerCall("ledger lookup", request.conversationId, LedgerLookupCall.make({ request }),
|
|
3560
|
+
admit: (request) => request.conversationId === options.localConversationId ? local.admit(request) : foreignLedgerCall("ledger admit", request.conversationId, LedgerAdmitCall.make({ request }), LedgerAdmitResult, AdmissionConflict).pipe(Effect.map((reply) => reply.result)),
|
|
3561
|
+
markReady: (request) => submissionTarget("ledger mark ready", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markReady(request) : foreignLedgerCall("ledger mark ready", target.conversationId, LedgerMarkReadyCall.make({ request }), LedgerMarkReadyResult, NoAdditionalPortFailure).pipe(Effect.asVoid))),
|
|
3562
|
+
lookup: (request) => request._tag === "SubmissionLookupById" ? submissionTarget("ledger lookup", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.lookup(request) : foreignLookupById("ledger lookup", target.conversationId, request.submissionId))) : request.conversationId === options.localConversationId ? local.lookup(request) : foreignLedgerCall("ledger lookup", request.conversationId, LedgerLookupCall.make({ request }), LedgerLookupResult, NoAdditionalPortFailure).pipe(Effect.map((result) => result.submission === void 0 ? Option.none() : Option.some(result.submission))),
|
|
3535
3563
|
resolveAdmission: (request) => request.conversationId === options.localConversationId ? local.resolveAdmission(request) : resolveForeignAdmission(request.conversationId, request),
|
|
3536
|
-
requestAbort: (request) => submissionTarget("ledger request abort", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.requestAbort(request) : foreignLedgerCall("ledger request abort", target.conversationId, LedgerRequestAbortCall.make({ request }),
|
|
3537
|
-
recordChildSettled: (request) => submissionTarget("ledger record child settled", request.parentSubmissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordChildSettled(request) : foreignLedgerCall("ledger record child settled", target.conversationId, LedgerRecordChildSettledCall.make({ request }),
|
|
3564
|
+
requestAbort: (request) => submissionTarget("ledger request abort", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.requestAbort(request) : foreignLedgerCall("ledger request abort", target.conversationId, LedgerRequestAbortCall.make({ request }), LedgerRequestAbortResult, AbortPortFailure).pipe(Effect.map((reply) => reply.intent)))),
|
|
3565
|
+
recordChildSettled: (request) => submissionTarget("ledger record child settled", request.parentSubmissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordChildSettled(request) : foreignLedgerCall("ledger record child settled", target.conversationId, LedgerRecordChildSettledCall.make({ request }), LedgerRecordChildSettledResult, NoAdditionalPortFailure).pipe(Effect.map((reply) => reply.outcome)))),
|
|
3538
3566
|
claim: (request) => request.conversationId === options.localConversationId ? local.claim(request) : Effect.fail(crossConversationLedgerError("ledger claim", request.conversationId)),
|
|
3539
3567
|
claimJoining: (request) => request.conversationId === options.localConversationId ? local.claimJoining(request) : Effect.fail(crossConversationLedgerError("ledger claim joining", request.conversationId)),
|
|
3540
3568
|
renewOwnership: (request) => submissionTarget("ledger renew ownership", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.renewOwnership(request) : Effect.fail(crossConversationLedgerError("ledger renew ownership", target.conversationId)))),
|
|
@@ -3567,33 +3595,37 @@ const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices
|
|
|
3567
3595
|
cause: error
|
|
3568
3596
|
});
|
|
3569
3597
|
/** The store twin of `foreignLedgerCall` with `ConversationStoreError` as the base error. */
|
|
3570
|
-
const foreignStoreCall = (operation, target, call,
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
if (
|
|
3575
|
-
|
|
3598
|
+
const foreignStoreCall = (operation, target, call, resultSchema, failureSchema) => {
|
|
3599
|
+
const isExpectedResult = Schema.is(resultSchema);
|
|
3600
|
+
const isExpectedFailure = Schema.is(failureSchema);
|
|
3601
|
+
return transportCall(target, call).pipe(Effect.mapError(routeFailure(operation, target)), Effect.flatMap((response) => {
|
|
3602
|
+
if (response._tag === "PortFailed") {
|
|
3603
|
+
const failure = response.failure;
|
|
3604
|
+
if (isExpectedFailure(failure)) return Effect.fail(failure);
|
|
3605
|
+
if (failure._tag === "ConversationStoreError") return Effect.fail(failure);
|
|
3606
|
+
return Effect.fail(ConversationStoreError.make({
|
|
3607
|
+
operation,
|
|
3608
|
+
message: boundPortDiagnostic(`The Conversation Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
|
|
3609
|
+
cause: failure
|
|
3610
|
+
}));
|
|
3611
|
+
}
|
|
3612
|
+
const result = response.result;
|
|
3613
|
+
if (!isExpectedResult(result)) return Effect.fail(ConversationStoreError.make({
|
|
3576
3614
|
operation,
|
|
3577
|
-
message:
|
|
3578
|
-
cause: failure
|
|
3615
|
+
message: `The Conversation Object owning ${target} answered ${operation} with the mismatched result ${result._tag}.`
|
|
3579
3616
|
}));
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
if (!isResultTag(resultTag)(result)) return Effect.fail(ConversationStoreError.make({
|
|
3617
|
+
return Effect.succeed(result);
|
|
3618
|
+
}), Effect.withSpan("DoPortRouting.foreignStoreCall", { attributes: {
|
|
3583
3619
|
operation,
|
|
3584
|
-
|
|
3585
|
-
}));
|
|
3586
|
-
|
|
3587
|
-
}), Effect.withSpan("DoPortRouting.foreignStoreCall", { attributes: {
|
|
3588
|
-
operation,
|
|
3589
|
-
target
|
|
3590
|
-
} }));
|
|
3620
|
+
target
|
|
3621
|
+
} }));
|
|
3622
|
+
};
|
|
3591
3623
|
const routed = ConversationStore.of({
|
|
3592
|
-
materialize: (request) => request.conversationId === options.localConversationId ? local.materialize(request) : foreignStoreCall("conversation materialize", request.conversationId, StoreMaterializeCall.make({ request }),
|
|
3593
|
-
append: (request) => request.conversationId === options.localConversationId ? local.append(request) : foreignStoreCall("conversation append", request.conversationId, StoreAppendCall.make({ request }),
|
|
3594
|
-
read: (request) => request.conversationId === options.localConversationId ? local.read(request) : Stream.unwrap(foreignStoreCall("conversation read", request.conversationId, StoreReadPageCall.make({ request }),
|
|
3595
|
-
inspectTail: (request) => request.conversationId === options.localConversationId ? local.inspectTail(request) : foreignStoreCall("conversation inspect tail", request.conversationId, StoreInspectTailCall.make({ request }),
|
|
3596
|
-
export: (request) => request.conversationId === options.localConversationId ? local.export(request) : foreignStoreCall("conversation export", request.conversationId, StoreExportCall.make({ request }),
|
|
3624
|
+
materialize: (request) => request.conversationId === options.localConversationId ? local.materialize(request) : foreignStoreCall("conversation materialize", request.conversationId, StoreMaterializeCall.make({ request }), StoreMaterializeResult, FenceRejected).pipe(Effect.asVoid),
|
|
3625
|
+
append: (request) => request.conversationId === options.localConversationId ? local.append(request) : foreignStoreCall("conversation append", request.conversationId, StoreAppendCall.make({ request }), StoreAppendResult, AppendPortFailure).pipe(Effect.map((reply) => reply.result)),
|
|
3626
|
+
read: (request) => request.conversationId === options.localConversationId ? local.read(request) : Stream.unwrap(foreignStoreCall("conversation read", request.conversationId, StoreReadPageCall.make({ request }), StoreReadPageResult, ConversationNotMaterialized).pipe(Effect.map((reply) => Stream.fromIterable(reply.records)))),
|
|
3627
|
+
inspectTail: (request) => request.conversationId === options.localConversationId ? local.inspectTail(request) : foreignStoreCall("conversation inspect tail", request.conversationId, StoreInspectTailCall.make({ request }), StoreInspectTailResult, ConversationNotMaterialized).pipe(Effect.map((reply) => reply.tail)),
|
|
3628
|
+
export: (request) => request.conversationId === options.localConversationId ? local.export(request) : foreignStoreCall("conversation export", request.conversationId, StoreExportCall.make({ request }), StoreExportResult, ConversationNotMaterialized).pipe(Effect.map((reply) => reply.export)),
|
|
3597
3629
|
observe: (request) => request.conversationId === options.localConversationId ? local.observe(request) : Stream.unwrap(Effect.fail(crossConversationStoreError("conversation observe", request.conversationId))),
|
|
3598
3630
|
saveCheckpoint: (request) => request.checkpoint.conversationId === options.localConversationId ? local.saveCheckpoint(request) : Effect.fail(crossConversationStoreError("conversation save checkpoint", request.checkpoint.conversationId)),
|
|
3599
3631
|
loadCheckpoint: (request) => request.conversationId === options.localConversationId ? local.loadCheckpoint(request) : Effect.fail(crossConversationStoreError("conversation load checkpoint", request.conversationId))
|