@@ -74,7 +74,7 @@ interface ReplayMessage {
7474 readonly lastSentChunk : string | undefined
7575 readonly discard : boolean
7676 readonly deliverAt ?: number | undefined
77- readonly replyTo ?: string | undefined
77+ readonly replyTos ?: ReadonlyArray < string > | undefined
7878}
7979
8080interface InvokeResult {
@@ -89,8 +89,8 @@ interface InvokeOutcome {
8989}
9090
9191interface DeliveryOptions {
92- readonly deliverAt : number
93- readonly primaryKey : string | null
92+ readonly deliverAt ? : number | undefined
93+ readonly primaryKey ? : string | null | undefined
9494 readonly replyTo ?: string | undefined
9595}
9696
@@ -121,7 +121,6 @@ export class ClusterEntity extends DurableObject<unknown> {
121121 #serial: Promise < void > = Promise . resolve ( )
122122 readonly #sessions = new Map < string , ReplySession > ( )
123123 readonly #workerWaiters = new Map < string , Array < WorkerWaiter > > ( )
124- readonly #workerWaiterTargets = new Map < string , string > ( )
125124
126125 constructor ( ctx : DurableObjectState , env : unknown ) {
127126 super ( ctx , env )
@@ -176,7 +175,7 @@ export class ClusterEntity extends DurableObject<unknown> {
176175 ? undefined
177176 : {
178177 scheduled : true ,
179- ...( row . replyTo === undefined ? undefined : { replyTo : row . replyTo } )
178+ ...( row . replyTos === undefined ? undefined : { replyTos : row . replyTos } )
180179 }
181180 ) . pipe (
182181 Effect . catchCause ( ( cause ) => this . #completeReplayFailure( registration , row , cause ) )
@@ -223,7 +222,7 @@ export class ClusterEntity extends DurableObject<unknown> {
223222 if ( persisted . processed ) {
224223 return { result : { requestId : persisted . originalId , replies : [ ] } }
225224 }
226- if ( delivery !== undefined ) {
225+ if ( delivery ?. deliverAt !== undefined ) {
227226 yield * this . #armEarliestAlarm( )
228227 return this . #delayedOutcome(
229228 persisted . originalId ,
@@ -234,6 +233,15 @@ export class ClusterEntity extends DurableObject<unknown> {
234233 }
235234 const original = loadMessage ( storage . sql , persisted . originalId )
236235 if ( original === undefined ) return yield * Effect . die ( "Duplicate mailbox row disappeared" )
236+ if ( original . deliverAt !== undefined && original . deliverAt > Date . now ( ) ) {
237+ yield * this . #armEarliestAlarm( )
238+ return this . #delayedOutcome(
239+ persisted . originalId ,
240+ discard ,
241+ delivery ?. replyTo ,
242+ String ( envelope . requestId )
243+ )
244+ }
237245 const replies = yield * this . #runStored(
238246 registration ,
239247 runtime ,
@@ -243,7 +251,7 @@ export class ClusterEntity extends DurableObject<unknown> {
243251 )
244252 return { result : { requestId : persisted . originalId , replies } }
245253 }
246- if ( delivery !== undefined ) {
254+ if ( delivery ?. deliverAt !== undefined ) {
247255 yield * this . #armEarliestAlarm( )
248256 return this . #delayedOutcome( String ( envelope . requestId ) , discard , delivery . replyTo )
249257 }
@@ -264,7 +272,6 @@ export class ClusterEntity extends DurableObject<unknown> {
264272 const waiters = this . #workerWaiters. get ( requestId ) ?? [ ]
265273 waiters . push ( { clientRequestId, resolve, reject } )
266274 this . #workerWaiters. set ( requestId , waiters )
267- this . #workerWaiterTargets. set ( clientRequestId , requestId )
268275 } )
269276 return { result, deferred }
270277 }
@@ -287,22 +294,20 @@ export class ClusterEntity extends DurableObject<unknown> {
287294 }
288295
289296 /** @internal Interrupts an in-memory handler execution. Persisted rows remain replayable. */
290- interrupt ( requestId : string ) : Promise < void > {
291- const storageRequestId = this . #workerWaiterTargets. get ( requestId ) ?? requestId
297+ interrupt ( storageRequestId : string , clientRequestId = storageRequestId ) : Promise < void > {
292298 const waiters = this . #workerWaiters. get ( storageRequestId )
293299 if ( waiters !== undefined ) {
294300 const remaining = waiters . filter ( ( waiter ) => {
295- if ( waiter . clientRequestId !== requestId ) return true
301+ if ( waiter . clientRequestId !== clientRequestId ) return true
296302 waiter . reject ( new Error ( "Delayed entity request interrupted" ) )
297303 return false
298304 } )
299- this . #workerWaiterTargets. delete ( requestId )
300305 if ( remaining . length === 0 ) this . #workerWaiters. delete ( storageRequestId )
301306 else this . #workerWaiters. set ( storageRequestId , remaining )
302307 }
303- const session = this . #sessions. get ( requestId )
308+ const session = this . #sessions. get ( storageRequestId )
304309 if ( session === undefined ) return Promise . resolve ( )
305- this . #sessions. delete ( requestId )
310+ this . #sessions. delete ( storageRequestId )
306311 session . ack ?. resolve ( )
307312 session . ack = undefined
308313 session . done = true
@@ -338,7 +343,7 @@ export class ClusterEntity extends DurableObject<unknown> {
338343 envelopeText : string ,
339344 lastSentChunk : string | undefined ,
340345 discard : boolean ,
341- options ?: { readonly scheduled ?: boolean ; readonly replyTo ?: string | undefined }
346+ options ?: { readonly scheduled ?: boolean ; readonly replyTos ?: ReadonlyArray < string > | undefined }
342347 ) {
343348 return Effect . flatMap (
344349 decodeRequest ( registration , envelopeText ) ,
@@ -358,7 +363,7 @@ export class ClusterEntity extends DurableObject<unknown> {
358363 ( row ) =>
359364 this . #runStored( registration , runtime , row . envelope , row . lastSentChunk , row . discard , {
360365 scheduled : true ,
361- ...( row . replyTo === undefined ? undefined : { replyTo : row . replyTo } )
366+ ...( row . replyTos === undefined ? undefined : { replyTos : row . replyTos } )
362367 } ) . pipe (
363368 Effect . catchCause ( ( cause ) => this . #completeReplayFailure( registration , row , cause ) )
364369 ) ,
@@ -403,7 +408,7 @@ export class ClusterEntity extends DurableObject<unknown> {
403408 ) ,
404409 ( reply ) =>
405410 Effect . sync ( ( ) => storage . transactionSync ( ( ) => saveReply ( storage . sql , reply ) ) ) . pipe (
406- Effect . andThen ( this . #deliverScheduledReply( encoded . requestId as string , reply , row . replyTo ) )
411+ Effect . andThen ( this . #deliverScheduledReply( encoded . requestId as string , reply , row . replyTos ) )
407412 )
408413 ) . pipe (
409414 Effect . catchCause ( ( ) =>
@@ -422,7 +427,7 @@ export class ClusterEntity extends DurableObject<unknown> {
422427 lastSentChunkText : string | undefined ,
423428 discard : boolean ,
424429 persisted : boolean ,
425- options ?: { readonly scheduled ?: boolean ; readonly replyTo ?: string | undefined }
430+ options ?: { readonly scheduled ?: boolean ; readonly replyTos ?: ReadonlyArray < string > | undefined }
426431 ) : Effect . Effect < ReadonlyArray < string > > {
427432 const rpc = registration . entity . protocol . requests . get ( envelope . tag ) as Rpc . AnyWithProps
428433 const storage = this . #state. storage
@@ -457,7 +462,7 @@ export class ClusterEntity extends DurableObject<unknown> {
457462 yield * Effect . promise ( ( ) => this . #offerReply( session , encoded ) )
458463 }
459464 if ( scheduled && reply . _tag === "WithExit" ) {
460- yield * this . #deliverScheduledReply( requestId , encoded , options ?. replyTo )
465+ yield * this . #deliverScheduledReply( requestId , encoded , options ?. replyTos )
461466 }
462467 } )
463468 )
@@ -477,25 +482,48 @@ export class ClusterEntity extends DurableObject<unknown> {
477482 } )
478483 }
479484
480- #deliverScheduledReply( requestId : string , reply : string , replyTo : string | undefined ) : Effect . Effect < void > {
485+ #deliverScheduledReply(
486+ requestId : string ,
487+ reply : string ,
488+ replyTos : ReadonlyArray < string > | undefined
489+ ) : Effect . Effect < void > {
481490 const waiters = this . #workerWaiters. get ( requestId )
482491 if ( waiters !== undefined ) {
483492 this . #workerWaiters. delete ( requestId )
484493 for ( const waiter of waiters ) {
485- this . #workerWaiterTargets. delete ( waiter . clientRequestId )
486494 waiter . resolve ( { requestId, replies : [ reply ] } )
487495 }
488496 }
489- if ( replyTo === undefined ) return Effect . void
497+ if ( replyTos === undefined ) return Effect . void
490498 const namespace = ( this . #state. exports as Record < string , unknown > ) . ClusterEntity as
491499 | {
492500 readonly getByName : (
493501 name : string
494502 ) => { readonly deliverReply : ( requestId : string , reply : string ) => Promise < boolean > }
495503 }
496504 | undefined
497- if ( namespace === undefined ) return Effect . void
498- return Effect . promise ( ( ) => namespace . getByName ( replyTo ) . deliverReply ( requestId , reply ) ) . pipe ( Effect . ignore )
505+ if ( namespace === undefined ) {
506+ return Effect . logError (
507+ "Scheduled entity reply delivery failed" ,
508+ new Error ( "CloudflareCluster: ClusterEntity export is unavailable for scheduled reply delivery" )
509+ )
510+ }
511+ return Effect . forEach (
512+ replyTos ,
513+ ( replyTo ) =>
514+ Effect . promise ( ( ) => namespace . getByName ( replyTo ) . deliverReply ( requestId , reply ) ) . pipe (
515+ Effect . flatMap ( ( delivered ) =>
516+ delivered
517+ ? Effect . void
518+ : Effect . logError (
519+ "Scheduled entity reply delivery failed" ,
520+ new Error ( `Scheduled entity reply target is unavailable: ${ replyTo } ` )
521+ )
522+ ) ,
523+ Effect . catchCause ( ( cause ) => Effect . logError ( "Scheduled entity reply delivery failed" , cause ) )
524+ ) ,
525+ { discard : true }
526+ )
499527 }
500528
501529 /** @internal Completes an in-memory delayed ask owned by this entity object. */
0 commit comments