@effect-agent/platform-cloudflare 0.1.0-beta.79 → 0.1.0-beta.80
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/Alarm.d.mts +17 -5
- package/dist/Alarm.mjs +48 -12
- package/dist/Alarm.mjs.map +1 -1
- package/dist/CloudflareBindings.d.mts +14 -7
- package/dist/CloudflareBindings.mjs +10 -7
- package/dist/CloudflareBindings.mjs.map +1 -1
- package/dist/CloudflareScheduling.mjs +2 -2
- package/dist/CloudflareSubscriptions.mjs +1 -1
- package/dist/CloudflareThreadClient.d.mts +41 -41
- package/dist/CloudflareThreadClient.mjs +3 -3
- package/dist/CloudflareThreadClient.mjs.map +1 -1
- package/dist/{ThreadObject-MuU9xAyn.d.mts → ThreadObject-3uDpKfB3.d.mts} +82 -44
- package/dist/{ThreadObject-KJf_5Isr.mjs → ThreadObject-DGkXv41C.mjs} +213 -68
- package/dist/ThreadObject-DGkXv41C.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +2 -2
- package/dist/ThreadObject.mjs +2 -2
- package/dist/WakeScheduler.d.mts +2 -2
- package/dist/WakeScheduler.mjs +6 -6
- package/dist/WakeScheduler.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{prepared-admission-G7N4DDqS.mjs → prepared-admission-DSi55W-7.mjs} +2 -2
- package/dist/{prepared-admission-G7N4DDqS.mjs.map → prepared-admission-DSi55W-7.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +91 -14
- package/src/CloudflareBindings.ts +15 -6
- package/src/CloudflareThreadClient.ts +3 -3
- package/src/ThreadObject.ts +284 -59
- package/src/WakeScheduler.ts +6 -6
- package/src/internal/layers.ts +150 -22
- package/src/internal/message-delivery.ts +37 -21
- package/src/internal/transport.ts +2 -2
- package/dist/ThreadObject-KJf_5Isr.mjs.map +0 -1
package/src/internal/layers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ThreadId } from "@effect-agent/core/Identifiers";
|
|
1
|
+
import { ThreadId, type SubmissionId } from "@effect-agent/core/Identifiers";
|
|
2
2
|
import {
|
|
3
3
|
type RunContextPreparation,
|
|
4
4
|
type RunCostEstimator,
|
|
@@ -52,16 +52,31 @@ import {
|
|
|
52
52
|
operationAuthorizerLayer,
|
|
53
53
|
type OperationAuthorizerService,
|
|
54
54
|
} from "@effect-agent/thread/OperationAuthorizer";
|
|
55
|
+
import { type PreparedInputAdmission } from "@effect-agent/thread/PreparedInputAdmission";
|
|
55
56
|
import { ProducerId } from "@effect-agent/thread/Records";
|
|
56
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
LedgerError,
|
|
59
|
+
SubmissionLedger,
|
|
60
|
+
SubmissionLookupById,
|
|
61
|
+
type SubmissionSnapshot,
|
|
62
|
+
} from "@effect-agent/thread/SubmissionLedger";
|
|
57
63
|
import { ThreadProjectionMaintenance } from "@effect-agent/thread/ThreadProjectionMaintenance";
|
|
58
64
|
import { ThreadStoreError, ThreadStore } from "@effect-agent/thread/ThreadStore";
|
|
59
65
|
import { ToolReconciler } from "@effect-agent/thread/ToolReconciler";
|
|
60
66
|
import { type WakeScheduler } from "@effect-agent/thread/WakeScheduler";
|
|
61
67
|
import { BrowserCrypto } from "@effect/platform-browser";
|
|
62
68
|
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
63
|
-
import
|
|
64
|
-
|
|
69
|
+
import {
|
|
70
|
+
type Crypto,
|
|
71
|
+
Cause,
|
|
72
|
+
Context,
|
|
73
|
+
Duration,
|
|
74
|
+
Effect,
|
|
75
|
+
Layer,
|
|
76
|
+
Schema,
|
|
77
|
+
Semaphore,
|
|
78
|
+
type Option,
|
|
79
|
+
} from "effect";
|
|
65
80
|
import { SqlClient } from "effect/unstable/sql/SqlClient";
|
|
66
81
|
|
|
67
82
|
import {
|
|
@@ -75,6 +90,7 @@ import {
|
|
|
75
90
|
} from "../Alarm.ts";
|
|
76
91
|
import {
|
|
77
92
|
ThreadObjectIdentity,
|
|
93
|
+
ThreadObjectPlacement,
|
|
78
94
|
DurableObjectContext,
|
|
79
95
|
type ThreadObjectNamespace,
|
|
80
96
|
} from "../CloudflareBindings.ts";
|
|
@@ -162,6 +178,7 @@ export interface CloudflareDurableRuntimeOptions {
|
|
|
162
178
|
export type CloudflareBootstrapServices =
|
|
163
179
|
| CloudflareDurableRuntimeConfig
|
|
164
180
|
| ThreadObjectIdentity
|
|
181
|
+
| ThreadObjectPlacement
|
|
165
182
|
| DurableRuntimeConfig
|
|
166
183
|
| Crypto.Crypto
|
|
167
184
|
| DoStorageFailpoint
|
|
@@ -208,6 +225,10 @@ export class ThreadObjectPorts extends Context.Service<
|
|
|
208
225
|
ThreadObjectPorts,
|
|
209
226
|
{
|
|
210
227
|
readonly handle: (request: PortRequest) => Effect.Effect<PortResponse>;
|
|
228
|
+
/** Local-only identity check before a bound RPC can read or mutate a Submission. */
|
|
229
|
+
readonly lookupSubmission: (
|
|
230
|
+
submissionId: SubmissionId,
|
|
231
|
+
) => Effect.Effect<Option.Option<SubmissionSnapshot>, LedgerError>;
|
|
211
232
|
}
|
|
212
233
|
>()("@effect-agent/platform-cloudflare/ThreadObjectPorts") {}
|
|
213
234
|
|
|
@@ -284,27 +305,21 @@ const threadIdFromState = (
|
|
|
284
305
|
* The native class factory builds this Layer inside its constructor gate. Custom Effect hosts
|
|
285
306
|
* can provide it around the complete application Layer with the native context already supplied.
|
|
286
307
|
*/
|
|
287
|
-
|
|
308
|
+
const runtimeConfigLayer = (
|
|
288
309
|
options: CloudflareDurableRuntimeOptions,
|
|
289
|
-
|
|
310
|
+
producerId: ProducerId,
|
|
311
|
+
): Layer.Layer<
|
|
312
|
+
Exclude<CloudflareBootstrapServices, ThreadObjectIdentity | ThreadObjectPlacement>,
|
|
313
|
+
CloudflarePlatformConfigError,
|
|
314
|
+
DurableObjectContext
|
|
315
|
+
> =>
|
|
290
316
|
Layer.unwrap(
|
|
291
317
|
Effect.gen(function* () {
|
|
292
318
|
const { ctx } = yield* DurableObjectContext;
|
|
293
319
|
const config = yield* configFromOptions(options);
|
|
294
|
-
const threadId = yield* threadIdFromState(ctx);
|
|
295
|
-
|
|
296
|
-
const producerId = yield* decodeProducerId(`${config.producerPrefix}:${threadId}`).pipe(
|
|
297
|
-
Effect.mapError((error) =>
|
|
298
|
-
CloudflarePlatformConfigError.make({
|
|
299
|
-
message: `The minted producer identity is invalid: ${error.message}`,
|
|
300
|
-
cause: error,
|
|
301
|
-
}),
|
|
302
|
-
),
|
|
303
|
-
);
|
|
304
320
|
|
|
305
321
|
return Layer.mergeAll(
|
|
306
322
|
Layer.succeed(CloudflareDurableRuntimeConfig, config),
|
|
307
|
-
Layer.succeed(ThreadObjectIdentity, { threadId, producerId }),
|
|
308
323
|
DurableRuntimeConfig.layer({
|
|
309
324
|
deploymentId: config.deploymentId,
|
|
310
325
|
producerId,
|
|
@@ -338,6 +353,59 @@ export const layerConfig = (
|
|
|
338
353
|
}),
|
|
339
354
|
);
|
|
340
355
|
|
|
356
|
+
const producerIdentity = (prefix: string, owner: string) =>
|
|
357
|
+
decodeProducerId(`${prefix}:${owner}`).pipe(
|
|
358
|
+
Effect.mapError((error) =>
|
|
359
|
+
CloudflarePlatformConfigError.make({
|
|
360
|
+
message: `The minted producer identity is invalid: ${error.message}`,
|
|
361
|
+
cause: error,
|
|
362
|
+
}),
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
/** Native one-Thread configuration; retains its existing producer and logical identities. */
|
|
367
|
+
export const layerConfig = (
|
|
368
|
+
options: CloudflareDurableRuntimeOptions,
|
|
369
|
+
): Layer.Layer<CloudflareBootstrapServices, CloudflarePlatformConfigError, DurableObjectContext> =>
|
|
370
|
+
Layer.unwrap(
|
|
371
|
+
Effect.gen(function* () {
|
|
372
|
+
const { ctx } = yield* DurableObjectContext;
|
|
373
|
+
const threadId = yield* threadIdFromState(ctx);
|
|
374
|
+
const producerId = yield* producerIdentity(options.producerPrefix, threadId);
|
|
375
|
+
|
|
376
|
+
return Layer.mergeAll(
|
|
377
|
+
runtimeConfigLayer(options, producerId),
|
|
378
|
+
Layer.succeed(ThreadObjectIdentity, { threadId, producerId }),
|
|
379
|
+
Layer.succeed(ThreadObjectPlacement, { ownsThread: (target) => target === threadId }),
|
|
380
|
+
);
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Configuration for an application Object owning several logical Threads. The producer is
|
|
386
|
+
* the stable physical Object. No logical identity is installed globally: handleRpc binds and
|
|
387
|
+
* validates it per request using the placement guard and this actual runtime producer.
|
|
388
|
+
*/
|
|
389
|
+
export const layerHostConfig = (
|
|
390
|
+
options: CloudflareDurableRuntimeOptions,
|
|
391
|
+
ownsThread: (threadId: ThreadId) => boolean,
|
|
392
|
+
): Layer.Layer<
|
|
393
|
+
Exclude<CloudflareBootstrapServices, ThreadObjectIdentity>,
|
|
394
|
+
CloudflarePlatformConfigError,
|
|
395
|
+
DurableObjectContext
|
|
396
|
+
> =>
|
|
397
|
+
Layer.unwrap(
|
|
398
|
+
Effect.gen(function* () {
|
|
399
|
+
const { ctx } = yield* DurableObjectContext;
|
|
400
|
+
const producerId = yield* producerIdentity(options.producerPrefix, ctx.id.toString());
|
|
401
|
+
|
|
402
|
+
return Layer.merge(
|
|
403
|
+
runtimeConfigLayer(options, producerId),
|
|
404
|
+
Layer.succeed(ThreadObjectPlacement, { ownsThread }),
|
|
405
|
+
);
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
|
|
341
409
|
export interface ThreadPublicationOptions<E = never, R = never, P = never> {
|
|
342
410
|
/**
|
|
343
411
|
* Optional host outbox consumer, built once per incarnation with RAW LOCAL ThreadStore and
|
|
@@ -450,12 +518,68 @@ const boundLayer = <E = never, R = never>(
|
|
|
450
518
|
| ThreadObjectNamespace
|
|
451
519
|
| CloudflareBootstrapServices
|
|
452
520
|
| Exclude<R, ThreadStore | SubmissionLedger | SqlClient>
|
|
521
|
+
> =>
|
|
522
|
+
Layer.unwrap(
|
|
523
|
+
Effect.map(DurableObjectContext, ({ ctx }) =>
|
|
524
|
+
sharedLayer(DurableAgentRuntime.layerWithBindings(bindings), options).pipe(
|
|
525
|
+
Layer.provideMerge(SqliteClient.layer({ storage: ctx.storage })),
|
|
526
|
+
),
|
|
527
|
+
),
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Build the application runtime over the existing owner SqlClient and native ports, then
|
|
532
|
+
* assemble its one maintenance coordinator. The application may acquire its Bindings from
|
|
533
|
+
* those ports and expose extra services, including ThreadHostMaintenance. It must not acquire
|
|
534
|
+
* another runtime stack or require ThreadMaintenance while constructing this Layer.
|
|
535
|
+
* Supply layerHostConfig and deterministic placement; dispatch addressed ingress with handleRpc.
|
|
536
|
+
*/
|
|
537
|
+
export function layerInHost<A, E, R, P = never, PE = never, PR = never>(
|
|
538
|
+
application: Layer.Layer<DurableAgentRuntime | A, E, R>,
|
|
539
|
+
options: Omit<ThreadPublicationOptions<PE, PR>, "projection"> & {
|
|
540
|
+
readonly projection: Layer.Layer<ThreadProjectionMaintenance | P, PE, PR>;
|
|
541
|
+
},
|
|
542
|
+
): Layer.Layer<
|
|
543
|
+
CloudflareDurableRuntimeServices | A | P,
|
|
544
|
+
Layer.Error<ReturnType<typeof sharedLayer<A, E, R, PE, PR>>>,
|
|
545
|
+
Layer.Services<ReturnType<typeof sharedLayer<A, E, Exclude<R, P>, PE, PR>>>
|
|
546
|
+
>;
|
|
547
|
+
|
|
548
|
+
export function layerInHost<A, E, R, PE = never, PR = never>(
|
|
549
|
+
application: Layer.Layer<DurableAgentRuntime | A, E, R>,
|
|
550
|
+
options?: ThreadPublicationOptions<PE, PR>,
|
|
551
|
+
): ReturnType<typeof sharedLayer<A, E, R, PE, PR>>;
|
|
552
|
+
|
|
553
|
+
export function layerInHost<A, E, R, PE = never, PR = never>(
|
|
554
|
+
application: Layer.Layer<DurableAgentRuntime | A, E, R>,
|
|
555
|
+
options: ThreadPublicationOptions<PE, PR> = {},
|
|
556
|
+
) {
|
|
557
|
+
return sharedLayer(application, options);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
type HostRuntimeServices = Exclude<
|
|
561
|
+
CloudflareDurableRuntimeServices,
|
|
562
|
+
DurableAgentRuntime | ThreadMaintenance
|
|
563
|
+
>;
|
|
564
|
+
|
|
565
|
+
const sharedLayer = <A, E, R, PE = never, PR = never>(
|
|
566
|
+
application: Layer.Layer<DurableAgentRuntime | A, E, R>,
|
|
567
|
+
options: ThreadPublicationOptions<PE, PR> = {},
|
|
568
|
+
): Layer.Layer<
|
|
569
|
+
CloudflareDurableRuntimeServices | A,
|
|
570
|
+
DoStorageInitializationError | MessageDeliveryError | E | PE,
|
|
571
|
+
| DurableObjectContext
|
|
572
|
+
| ThreadObjectNamespace
|
|
573
|
+
| Exclude<CloudflareBootstrapServices, ThreadObjectIdentity>
|
|
574
|
+
| SqlClient
|
|
575
|
+
| Exclude<R, HostRuntimeServices | PreparedInputAdmission>
|
|
576
|
+
| Exclude<PR, ThreadStore | SubmissionLedger | SqlClient>
|
|
453
577
|
> =>
|
|
454
578
|
Layer.unwrap(
|
|
455
579
|
Effect.gen(function* () {
|
|
456
580
|
const { ctx } = yield* DurableObjectContext;
|
|
457
581
|
const config = yield* CloudflareDurableRuntimeConfig;
|
|
458
|
-
const {
|
|
582
|
+
const { ownsThread } = yield* ThreadObjectPlacement;
|
|
459
583
|
|
|
460
584
|
const storageOptions: DoStorageOptions = {
|
|
461
585
|
storage: ctx.storage,
|
|
@@ -467,7 +591,7 @@ const boundLayer = <E = never, R = never>(
|
|
|
467
591
|
|
|
468
592
|
const infrastructure = Layer.mergeAll(
|
|
469
593
|
storageConfigLayer(storageOptions),
|
|
470
|
-
|
|
594
|
+
Layer.effect(SqlClient)(SqlClient),
|
|
471
595
|
);
|
|
472
596
|
|
|
473
597
|
// The same local ports serve routed decorators and owner-side RPC execution.
|
|
@@ -585,19 +709,22 @@ const boundLayer = <E = never, R = never>(
|
|
|
585
709
|
const portsEndpointLayer = Layer.effect(ThreadObjectPorts)(
|
|
586
710
|
Effect.gen(function* () {
|
|
587
711
|
const local = yield* Effect.context<SubmissionLedger | ThreadStore>();
|
|
712
|
+
const ledger = Context.get(local, SubmissionLedger);
|
|
588
713
|
|
|
589
714
|
return ThreadObjectPorts.of({
|
|
590
715
|
handle: (request) => executePortRequest(request).pipe(Effect.provide(local)),
|
|
716
|
+
lookupSubmission: (submissionId) =>
|
|
717
|
+
ledger.lookup(SubmissionLookupById.make({ submissionId })),
|
|
591
718
|
});
|
|
592
719
|
}),
|
|
593
720
|
).pipe(Layer.provide(localPorts));
|
|
594
721
|
|
|
595
722
|
const routedPorts = Layer.mergeAll(
|
|
596
|
-
routedSubmissionLedgerLayer({
|
|
597
|
-
routedThreadStoreLayer({
|
|
723
|
+
routedSubmissionLedgerLayer({ ownsThread }),
|
|
724
|
+
routedThreadStoreLayer({ ownsThread }),
|
|
598
725
|
).pipe(Layer.provide(localPorts), Layer.provide(threadPortTransportLayer));
|
|
599
726
|
|
|
600
|
-
const runtimeStack =
|
|
727
|
+
const runtimeStack = application.pipe(
|
|
601
728
|
Layer.provide(
|
|
602
729
|
cloudflarePreparedInputAdmissionLayer.pipe(Layer.provide(CloudflareThreadClient.layer)),
|
|
603
730
|
),
|
|
@@ -605,6 +732,7 @@ const boundLayer = <E = never, R = never>(
|
|
|
605
732
|
Layer.provideMerge(routedPorts),
|
|
606
733
|
Layer.provideMerge(wakes),
|
|
607
734
|
Layer.provideMerge(base),
|
|
735
|
+
Layer.provideMerge(portsEndpointLayer),
|
|
608
736
|
);
|
|
609
737
|
|
|
610
738
|
return Layer.mergeAll(
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { type ThreadId } from "@effect-agent/core/Identifiers";
|
|
1
2
|
import {
|
|
2
3
|
MessageDeliveryDriver,
|
|
3
4
|
MessageDeliveryError,
|
|
4
5
|
MessageDeliveryStore,
|
|
5
6
|
} from "@effect-agent/thread/MessageDelivery";
|
|
6
7
|
import { WakeScheduler } from "@effect-agent/thread/WakeScheduler";
|
|
7
|
-
import { Clock, Context, Deferred, Effect, Layer, Option, Ref, Semaphore } from "effect";
|
|
8
|
+
import { Clock, Context, Deferred, Effect, Layer, Option, Ref, Semaphore, Stream } from "effect";
|
|
8
9
|
|
|
9
10
|
import { DurableAlarmError, ThreadMessageDelivery, ThreadMutationGate } from "../Alarm.ts";
|
|
10
|
-
import {
|
|
11
|
+
import { ThreadObjectPlacement } from "../CloudflareBindings.ts";
|
|
11
12
|
import { CloudflareDurableRuntimeConfig } from "../CloudflareConfig.ts";
|
|
12
13
|
|
|
13
14
|
/** Every externally requested write prearms the owning Object's maintenance generation. */
|
|
@@ -17,14 +18,17 @@ export const guardedMessageDeliveryStoreLayer = Layer.effect(
|
|
|
17
18
|
const store = yield* MessageDeliveryStore;
|
|
18
19
|
const mutations = yield* ThreadMutationGate;
|
|
19
20
|
const wakes = yield* WakeScheduler;
|
|
20
|
-
const {
|
|
21
|
+
const { ownsThread } = yield* ThreadObjectPlacement;
|
|
21
22
|
// Reconstructed as unknown on every incarnation. The gate prevents a racing read from
|
|
22
23
|
// caching an empty deadline across a write; SQL remains the recovery authority.
|
|
23
24
|
const deadline = yield* Ref.make<number | null | undefined>(undefined);
|
|
24
25
|
const cacheGate = yield* Semaphore.make(1);
|
|
25
26
|
|
|
26
|
-
const local = <A, E>(
|
|
27
|
-
owner
|
|
27
|
+
const local = <A, E>(
|
|
28
|
+
owner: ThreadId | undefined,
|
|
29
|
+
body: Effect.Effect<A, E>,
|
|
30
|
+
): Effect.Effect<A, E | MessageDeliveryError> =>
|
|
31
|
+
owner === undefined || ownsThread(owner)
|
|
28
32
|
? body
|
|
29
33
|
: Effect.fail(
|
|
30
34
|
MessageDeliveryError.make({ reason: "validation", operation: "message owner" }),
|
|
@@ -44,7 +48,7 @@ export const guardedMessageDeliveryStoreLayer = Layer.effect(
|
|
|
44
48
|
const cached = yield* Ref.get(deadline);
|
|
45
49
|
|
|
46
50
|
if (cached !== undefined) return cached;
|
|
47
|
-
const current = yield* store.nextDeadline(
|
|
51
|
+
const current = yield* store.nextDeadline();
|
|
48
52
|
|
|
49
53
|
yield* Ref.set(deadline, current);
|
|
50
54
|
|
|
@@ -56,14 +60,27 @@ export const guardedMessageDeliveryStoreLayer = Layer.effect(
|
|
|
56
60
|
insert: (record) =>
|
|
57
61
|
local(
|
|
58
62
|
record.key.ownerThreadId,
|
|
59
|
-
mutate(store.insert(record)).pipe(
|
|
63
|
+
mutate(store.insert(record)).pipe(
|
|
64
|
+
Effect.tap(() => wakes.notify(record.key.ownerThreadId)),
|
|
65
|
+
),
|
|
60
66
|
),
|
|
61
67
|
get: (key) => local(key.ownerThreadId, store.get(key)),
|
|
62
68
|
list: (request) => local(request.ownerThreadId, store.list(request)),
|
|
63
69
|
change: (key, change) => local(key.ownerThreadId, mutate(store.change(key, change))),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
// Only the trusted physical-owner pump omits an owner. All returned keys still
|
|
71
|
+
// pass placement validation before the driver may dispatch any of the wave.
|
|
72
|
+
due: (nowMillis, limit, owner) =>
|
|
73
|
+
local(owner, store.due(nowMillis, limit, owner)).pipe(
|
|
74
|
+
Effect.flatMap((keys) =>
|
|
75
|
+
keys.every((key) => ownsThread(key.ownerThreadId))
|
|
76
|
+
? Effect.succeed(keys)
|
|
77
|
+
: Effect.fail(
|
|
78
|
+
MessageDeliveryError.make({ reason: "validation", operation: "message owner" }),
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
nextDeadline: (owner) =>
|
|
83
|
+
local(owner, owner === undefined ? nextDeadline : store.nextDeadline(owner)),
|
|
67
84
|
});
|
|
68
85
|
}),
|
|
69
86
|
);
|
|
@@ -75,7 +92,6 @@ export const threadMessageDeliveryLayer = Layer.effectContext(
|
|
|
75
92
|
const store = yield* MessageDeliveryStore;
|
|
76
93
|
const wakes = yield* WakeScheduler;
|
|
77
94
|
const config = yield* CloudflareDurableRuntimeConfig;
|
|
78
|
-
const { threadId } = yield* ThreadObjectIdentity;
|
|
79
95
|
|
|
80
96
|
const failure = (operation: string) => () =>
|
|
81
97
|
DurableAlarmError.make({
|
|
@@ -84,10 +100,10 @@ export const threadMessageDeliveryLayer = Layer.effectContext(
|
|
|
84
100
|
});
|
|
85
101
|
|
|
86
102
|
const drain = Effect.gen(function* () {
|
|
87
|
-
const deadline = yield* store.nextDeadline(
|
|
103
|
+
const deadline = yield* store.nextDeadline();
|
|
88
104
|
|
|
89
105
|
if (deadline !== null && deadline <= (yield* Clock.currentTimeMillis)) {
|
|
90
|
-
yield* driver.runDue(
|
|
106
|
+
yield* driver.runDue();
|
|
91
107
|
}
|
|
92
108
|
}).pipe(Effect.mapError(failure("drain message delivery")));
|
|
93
109
|
|
|
@@ -96,16 +112,16 @@ export const threadMessageDeliveryLayer = Layer.effectContext(
|
|
|
96
112
|
drainUntil: Effect.fn("ThreadMessageDelivery.drainUntil")(function* (
|
|
97
113
|
finished: Deferred.Deferred<void>,
|
|
98
114
|
) {
|
|
115
|
+
// One scoped subscription covers every logical lane in this physical owner.
|
|
116
|
+
// Acquire it before reading durable deadlines; hints remain droppable.
|
|
117
|
+
const notified = (yield* Stream.toPull(wakes.wakes)).pipe(Effect.catch(() => Effect.never));
|
|
118
|
+
|
|
99
119
|
// Always finish one wave; source completion prevents starting subsequent waves.
|
|
100
120
|
yield* Effect.gen(function* () {
|
|
101
|
-
// Subscribe before the durable read so an insertion during a wave is retained
|
|
102
|
-
// as a hint for the next one. The scan interval covers dropped notifications.
|
|
103
|
-
const notified = yield* wakes.subscribe(threadId);
|
|
104
|
-
|
|
105
121
|
yield* drain;
|
|
106
122
|
|
|
107
123
|
const deadline = yield* store
|
|
108
|
-
.nextDeadline(
|
|
124
|
+
.nextDeadline()
|
|
109
125
|
.pipe(Effect.mapError(failure("read message deadline")));
|
|
110
126
|
|
|
111
127
|
// The index includes unfinished waves, lease expiry, retry and settlement polls.
|
|
@@ -122,10 +138,10 @@ export const threadMessageDeliveryLayer = Layer.effectContext(
|
|
|
122
138
|
Deferred.await(finished),
|
|
123
139
|
Effect.raceFirst(notified, Effect.sleep(delay)),
|
|
124
140
|
);
|
|
125
|
-
}).pipe(Effect.
|
|
126
|
-
}),
|
|
141
|
+
}).pipe(Effect.repeat({ until: () => Deferred.isDone(finished) }));
|
|
142
|
+
}, Effect.scoped),
|
|
127
143
|
pendingDeadline: store
|
|
128
|
-
.nextDeadline(
|
|
144
|
+
.nextDeadline()
|
|
129
145
|
.pipe(Effect.map(Option.fromNullishOr), Effect.mapError(failure("read message deadline"))),
|
|
130
146
|
});
|
|
131
147
|
}),
|
|
@@ -25,12 +25,12 @@ export const threadPortTransportLayer: Layer.Layer<
|
|
|
25
25
|
ThreadObjectNamespace
|
|
26
26
|
> = Layer.effect(ThreadPortTransport)(
|
|
27
27
|
Effect.gen(function* () {
|
|
28
|
-
const {
|
|
28
|
+
const { get } = yield* ThreadObjectNamespace;
|
|
29
29
|
|
|
30
30
|
return ThreadPortTransport.of({
|
|
31
31
|
call: (threadId, request) =>
|
|
32
32
|
Effect.tryPromise({
|
|
33
|
-
try: () =>
|
|
33
|
+
try: () => get(threadId).portCall(request),
|
|
34
34
|
catch: (cause) => portTransportFailure(threadId, cause),
|
|
35
35
|
}).pipe(
|
|
36
36
|
Effect.withSpan("CloudflarePortTransport.call", {
|