@yolk-sdk/harness 0.1.0-canary.77
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/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/coordinator.d.mts +62 -0
- package/dist/coordinator.d.mts.map +1 -0
- package/dist/coordinator.mjs +129 -0
- package/dist/coordinator.mjs.map +1 -0
- package/dist/driver/durable-object.d.mts +20 -0
- package/dist/driver/durable-object.d.mts.map +1 -0
- package/dist/driver/durable-object.mjs +12 -0
- package/dist/driver/durable-object.mjs.map +1 -0
- package/dist/driver/memory.d.mts +21 -0
- package/dist/driver/memory.d.mts.map +1 -0
- package/dist/driver/memory.mjs +15 -0
- package/dist/driver/memory.mjs.map +1 -0
- package/dist/driver.d.mts +67 -0
- package/dist/driver.d.mts.map +1 -0
- package/dist/driver.mjs +95 -0
- package/dist/driver.mjs.map +1 -0
- package/dist/inbox.d.mts +91 -0
- package/dist/inbox.d.mts.map +1 -0
- package/dist/inbox.mjs +224 -0
- package/dist/inbox.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/outcome.d.mts +61 -0
- package/dist/outcome.d.mts.map +1 -0
- package/dist/outcome.mjs +159 -0
- package/dist/outcome.mjs.map +1 -0
- package/dist/store.d.mts +25 -0
- package/dist/store.d.mts.map +1 -0
- package/dist/store.mjs +86 -0
- package/dist/store.mjs.map +1 -0
- package/package.json +90 -0
- package/src/coordinator.ts +254 -0
- package/src/driver/durable-object.ts +34 -0
- package/src/driver/memory.ts +38 -0
- package/src/driver.ts +250 -0
- package/src/inbox.ts +454 -0
- package/src/index.ts +2 -0
- package/src/outcome.ts +316 -0
- package/src/store.ts +137 -0
package/src/inbox.ts
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Ref, Semaphore } from 'effect'
|
|
2
|
+
import type { CapturedRun, Promotable } from './coordinator.ts'
|
|
3
|
+
|
|
4
|
+
export type InboxKind = 'input' | 'hitl'
|
|
5
|
+
|
|
6
|
+
export type InboxItem = {
|
|
7
|
+
readonly id: string
|
|
8
|
+
readonly runId: string
|
|
9
|
+
readonly delivery: Promotable | 'queue'
|
|
10
|
+
readonly kind: InboxKind
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ParkedResponse = {
|
|
14
|
+
readonly itemId: string
|
|
15
|
+
readonly requestId: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ParkedState = {
|
|
19
|
+
readonly generation: string
|
|
20
|
+
readonly requestIds: ReadonlyArray<string>
|
|
21
|
+
readonly responses: ReadonlyArray<ParkedResponse>
|
|
22
|
+
readonly ready: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type HitlAdmission = {
|
|
26
|
+
readonly itemId: string
|
|
27
|
+
readonly requestId: string
|
|
28
|
+
readonly generation: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type HitlDecision =
|
|
32
|
+
| { readonly _tag: 'Accepted' }
|
|
33
|
+
| { readonly _tag: 'Ready' }
|
|
34
|
+
| { readonly _tag: 'Duplicate' }
|
|
35
|
+
| { readonly _tag: 'Stale' }
|
|
36
|
+
| { readonly _tag: 'UnknownRequest' }
|
|
37
|
+
| { readonly _tag: 'NotParked' }
|
|
38
|
+
|
|
39
|
+
export type PauseDecision =
|
|
40
|
+
| { readonly _tag: 'Parked'; readonly generation: string }
|
|
41
|
+
| { readonly _tag: 'Stale' }
|
|
42
|
+
|
|
43
|
+
export type RecoveryAttempt =
|
|
44
|
+
| { readonly _tag: 'Skip' }
|
|
45
|
+
| { readonly _tag: 'Exhausted' }
|
|
46
|
+
| { readonly _tag: 'Resume' }
|
|
47
|
+
|
|
48
|
+
export type RecoveryAdmission =
|
|
49
|
+
| { readonly _tag: 'Skip' }
|
|
50
|
+
| { readonly _tag: 'Exhausted' }
|
|
51
|
+
| { readonly _tag: 'Resumed' }
|
|
52
|
+
|
|
53
|
+
export type DrainBegin =
|
|
54
|
+
| { readonly _tag: 'Skip' }
|
|
55
|
+
| {
|
|
56
|
+
readonly _tag: 'Run'
|
|
57
|
+
readonly drainToken: string
|
|
58
|
+
readonly readyResponses: ReadonlyArray<ParkedResponse>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type InboxShape = {
|
|
62
|
+
readonly enqueue: (item: InboxItem) => Effect.Effect<void>
|
|
63
|
+
readonly takePromotable: (
|
|
64
|
+
runId: string,
|
|
65
|
+
scope: Promotable,
|
|
66
|
+
drainToken: string
|
|
67
|
+
) => Effect.Effect<InboxItem | undefined>
|
|
68
|
+
readonly pending: (runId: string) => Effect.Effect<ReadonlyArray<InboxItem>>
|
|
69
|
+
readonly parked: (runId: string) => Effect.Effect<ParkedState | undefined>
|
|
70
|
+
readonly park: (
|
|
71
|
+
runId: string,
|
|
72
|
+
requestIds: ReadonlyArray<string>,
|
|
73
|
+
drainToken: string
|
|
74
|
+
) => Effect.Effect<PauseDecision>
|
|
75
|
+
readonly acceptHitl: (
|
|
76
|
+
runId: string,
|
|
77
|
+
admission: HitlAdmission,
|
|
78
|
+
onReady: Effect.Effect<void>
|
|
79
|
+
) => Effect.Effect<HitlDecision>
|
|
80
|
+
readonly clearPark: (runId: string, generation: string) => Effect.Effect<boolean>
|
|
81
|
+
readonly beginDrain: (runId: string, scope: Promotable) => Effect.Effect<DrainBegin>
|
|
82
|
+
readonly endDrain: (
|
|
83
|
+
runId: string,
|
|
84
|
+
drainToken: string,
|
|
85
|
+
acknowledged: boolean
|
|
86
|
+
) => Effect.Effect<void>
|
|
87
|
+
readonly invalidate: (
|
|
88
|
+
runId: string,
|
|
89
|
+
interrupt: Effect.Effect<boolean>,
|
|
90
|
+
releaseIdleClaim: Effect.Effect<void>
|
|
91
|
+
) => Effect.Effect<{ readonly hadPark: boolean; readonly interrupted: boolean }>
|
|
92
|
+
readonly enqueueAndWake: (item: InboxItem, wake: Effect.Effect<void>) => Effect.Effect<void>
|
|
93
|
+
readonly wakeIfUnblocked: (
|
|
94
|
+
runId: string,
|
|
95
|
+
scope: Promotable,
|
|
96
|
+
wake: Effect.Effect<void>
|
|
97
|
+
) => Effect.Effect<boolean>
|
|
98
|
+
readonly startIfUnblocked: <E, R>(
|
|
99
|
+
runId: string,
|
|
100
|
+
start: Effect.Effect<CapturedRun<E>, E, R>
|
|
101
|
+
) => Effect.Effect<CapturedRun<E> | undefined, E, R>
|
|
102
|
+
readonly admitRecovery: <E, R>(
|
|
103
|
+
runId: string,
|
|
104
|
+
attempt: Effect.Effect<RecoveryAttempt, E, R>,
|
|
105
|
+
wake: Effect.Effect<void>
|
|
106
|
+
) => Effect.Effect<RecoveryAdmission, E, R>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class Inbox extends Context.Service<Inbox, InboxShape>()('@yolk-sdk/harness/Inbox') {}
|
|
110
|
+
|
|
111
|
+
const isPromotableAt = (item: InboxItem, scope: Promotable) => {
|
|
112
|
+
if (scope === 'steer') return item.delivery === 'steer'
|
|
113
|
+
return item.delivery === 'steer' || item.delivery === 'input' || item.delivery === 'queue'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type Park = {
|
|
117
|
+
readonly generation: string
|
|
118
|
+
readonly requestIds: ReadonlyArray<string>
|
|
119
|
+
readonly responses: ReadonlyArray<ParkedResponse>
|
|
120
|
+
readonly readyWoken: boolean
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type RunControl = {
|
|
124
|
+
readonly pending: Promotable | undefined
|
|
125
|
+
readonly liveToken: string | undefined
|
|
126
|
+
readonly park: Park | undefined
|
|
127
|
+
readonly leasedGeneration: string | undefined
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const emptyControl: RunControl = {
|
|
131
|
+
pending: undefined,
|
|
132
|
+
liveToken: undefined,
|
|
133
|
+
park: undefined,
|
|
134
|
+
leasedGeneration: undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const widerPending = (current: Promotable | undefined, next: Promotable): Promotable =>
|
|
138
|
+
current === 'input' || next === 'input' ? 'input' : 'steer'
|
|
139
|
+
|
|
140
|
+
const remainingPending = (pending: Promotable, scope: Promotable): Promotable | undefined =>
|
|
141
|
+
scope === 'steer' && pending === 'input' ? 'input' : undefined
|
|
142
|
+
|
|
143
|
+
const itemScope = (item: InboxItem): Promotable => (item.delivery === 'steer' ? 'steer' : 'input')
|
|
144
|
+
|
|
145
|
+
const parkComplete = (park: Park) =>
|
|
146
|
+
park.requestIds.every(requestId =>
|
|
147
|
+
park.responses.some(response => response.requestId === requestId)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const parkBlocked = (park: Park | undefined) => park !== undefined && !parkComplete(park)
|
|
151
|
+
|
|
152
|
+
const parkedState = (park: Park): ParkedState => ({
|
|
153
|
+
generation: park.generation,
|
|
154
|
+
requestIds: park.requestIds,
|
|
155
|
+
responses: park.responses,
|
|
156
|
+
ready: parkComplete(park)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const isPrunable = (control: RunControl) =>
|
|
160
|
+
control.pending === undefined &&
|
|
161
|
+
control.liveToken === undefined &&
|
|
162
|
+
control.park === undefined &&
|
|
163
|
+
control.leasedGeneration === undefined
|
|
164
|
+
|
|
165
|
+
export const makeInMemoryInboxLayer = (): Layer.Layer<Inbox> =>
|
|
166
|
+
Layer.effect(
|
|
167
|
+
Inbox,
|
|
168
|
+
Effect.gen(function* () {
|
|
169
|
+
const items = yield* Ref.make<ReadonlyArray<InboxItem>>([])
|
|
170
|
+
const controls = yield* Ref.make<ReadonlyMap<string, RunControl>>(new Map())
|
|
171
|
+
const gate = yield* Semaphore.make(1)
|
|
172
|
+
let generationSeq = 0
|
|
173
|
+
let drainSeq = 0
|
|
174
|
+
|
|
175
|
+
const withGate = <A, E, R>(body: Effect.Effect<A, E, R>) => gate.withPermits(1)(body)
|
|
176
|
+
|
|
177
|
+
const getControl = (runId: string) =>
|
|
178
|
+
Ref.get(controls).pipe(Effect.map(current => current.get(runId) ?? emptyControl))
|
|
179
|
+
|
|
180
|
+
const writeControl = (runId: string, next: RunControl) =>
|
|
181
|
+
Ref.update(controls, current => {
|
|
182
|
+
const copy = new Map(current)
|
|
183
|
+
if (isPrunable(next)) copy.delete(runId)
|
|
184
|
+
else copy.set(runId, next)
|
|
185
|
+
return copy
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const dropQueued = (runId: string) =>
|
|
189
|
+
Ref.update(items, current => current.filter(item => item.runId !== runId))
|
|
190
|
+
|
|
191
|
+
const acceptHitlPure = (control: RunControl, admission: HitlAdmission): HitlDecision => {
|
|
192
|
+
const park = control.park
|
|
193
|
+
if (park === undefined) return { _tag: 'NotParked' }
|
|
194
|
+
if (park.generation !== admission.generation) return { _tag: 'Stale' }
|
|
195
|
+
if (!park.requestIds.includes(admission.requestId)) return { _tag: 'UnknownRequest' }
|
|
196
|
+
if (
|
|
197
|
+
park.responses.some(
|
|
198
|
+
response =>
|
|
199
|
+
response.itemId === admission.itemId || response.requestId === admission.requestId
|
|
200
|
+
)
|
|
201
|
+
) {
|
|
202
|
+
return { _tag: 'Duplicate' }
|
|
203
|
+
}
|
|
204
|
+
if (park.readyWoken) return { _tag: 'Duplicate' }
|
|
205
|
+
const responses = [
|
|
206
|
+
...park.responses,
|
|
207
|
+
{ itemId: admission.itemId, requestId: admission.requestId }
|
|
208
|
+
]
|
|
209
|
+
return parkComplete({ ...park, responses }) ? { _tag: 'Ready' } : { _tag: 'Accepted' }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return Inbox.of({
|
|
213
|
+
enqueue: item => Ref.update(items, current => [...current, item]),
|
|
214
|
+
takePromotable: (runId, scope, drainToken) =>
|
|
215
|
+
withGate(
|
|
216
|
+
Effect.uninterruptible(
|
|
217
|
+
Effect.gen(function* () {
|
|
218
|
+
const control = yield* getControl(runId)
|
|
219
|
+
if (control.liveToken === undefined || control.liveToken !== drainToken) {
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
|
222
|
+
return yield* Ref.modify(items, current => {
|
|
223
|
+
const index = current.findIndex(
|
|
224
|
+
item => item.runId === runId && isPromotableAt(item, scope)
|
|
225
|
+
)
|
|
226
|
+
if (index < 0) return [undefined, current] as const
|
|
227
|
+
const taken = current[index]
|
|
228
|
+
return [taken, current.filter((_, itemIndex) => itemIndex !== index)] as const
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
)
|
|
232
|
+
),
|
|
233
|
+
pending: runId =>
|
|
234
|
+
Ref.get(items).pipe(Effect.map(current => current.filter(item => item.runId === runId))),
|
|
235
|
+
parked: runId =>
|
|
236
|
+
getControl(runId).pipe(
|
|
237
|
+
Effect.map(control =>
|
|
238
|
+
control.park === undefined ? undefined : parkedState(control.park)
|
|
239
|
+
)
|
|
240
|
+
),
|
|
241
|
+
park: (runId, requestIds, drainToken) =>
|
|
242
|
+
withGate(
|
|
243
|
+
Effect.uninterruptible(
|
|
244
|
+
Effect.gen(function* () {
|
|
245
|
+
const control = yield* getControl(runId)
|
|
246
|
+
if (control.liveToken !== drainToken) return { _tag: 'Stale' } as const
|
|
247
|
+
generationSeq += 1
|
|
248
|
+
const generation = String(generationSeq)
|
|
249
|
+
yield* writeControl(runId, {
|
|
250
|
+
pending: control.pending,
|
|
251
|
+
liveToken: control.liveToken,
|
|
252
|
+
leasedGeneration: control.leasedGeneration,
|
|
253
|
+
park: {
|
|
254
|
+
generation,
|
|
255
|
+
requestIds: [...requestIds],
|
|
256
|
+
responses: [],
|
|
257
|
+
readyWoken: false
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
return { _tag: 'Parked', generation } as const
|
|
261
|
+
})
|
|
262
|
+
)
|
|
263
|
+
),
|
|
264
|
+
acceptHitl: (runId, admission, onReady) =>
|
|
265
|
+
withGate(
|
|
266
|
+
Effect.uninterruptible(
|
|
267
|
+
Effect.gen(function* () {
|
|
268
|
+
const control = yield* getControl(runId)
|
|
269
|
+
const decision = acceptHitlPure(control, admission)
|
|
270
|
+
if (decision._tag !== 'Accepted' && decision._tag !== 'Ready') {
|
|
271
|
+
return decision
|
|
272
|
+
}
|
|
273
|
+
const park = control.park
|
|
274
|
+
if (park === undefined) return { _tag: 'NotParked' } as const
|
|
275
|
+
const nextPark: Park = {
|
|
276
|
+
...park,
|
|
277
|
+
responses: [
|
|
278
|
+
...park.responses,
|
|
279
|
+
{ itemId: admission.itemId, requestId: admission.requestId }
|
|
280
|
+
],
|
|
281
|
+
readyWoken: decision._tag === 'Ready'
|
|
282
|
+
}
|
|
283
|
+
yield* writeControl(runId, {
|
|
284
|
+
pending:
|
|
285
|
+
decision._tag === 'Ready'
|
|
286
|
+
? widerPending(control.pending, 'input')
|
|
287
|
+
: control.pending,
|
|
288
|
+
liveToken: control.liveToken,
|
|
289
|
+
leasedGeneration: control.leasedGeneration,
|
|
290
|
+
park: nextPark
|
|
291
|
+
})
|
|
292
|
+
if (decision._tag === 'Ready') yield* onReady
|
|
293
|
+
return decision
|
|
294
|
+
})
|
|
295
|
+
)
|
|
296
|
+
),
|
|
297
|
+
clearPark: (runId, generation) =>
|
|
298
|
+
withGate(
|
|
299
|
+
Effect.uninterruptible(
|
|
300
|
+
Effect.gen(function* () {
|
|
301
|
+
const control = yield* getControl(runId)
|
|
302
|
+
if (control.park === undefined || control.park.generation !== generation) {
|
|
303
|
+
return false
|
|
304
|
+
}
|
|
305
|
+
yield* writeControl(runId, {
|
|
306
|
+
pending: control.pending,
|
|
307
|
+
liveToken: control.liveToken,
|
|
308
|
+
leasedGeneration: control.leasedGeneration,
|
|
309
|
+
park: undefined
|
|
310
|
+
})
|
|
311
|
+
return true
|
|
312
|
+
})
|
|
313
|
+
)
|
|
314
|
+
),
|
|
315
|
+
beginDrain: (runId, scope) =>
|
|
316
|
+
withGate(
|
|
317
|
+
Effect.uninterruptible(
|
|
318
|
+
Effect.gen(function* () {
|
|
319
|
+
const control = yield* getControl(runId)
|
|
320
|
+
const pending = control.pending
|
|
321
|
+
if (parkBlocked(control.park) || pending === undefined) {
|
|
322
|
+
return { _tag: 'Skip' } as const
|
|
323
|
+
}
|
|
324
|
+
drainSeq += 1
|
|
325
|
+
const drainToken = `d${drainSeq}`
|
|
326
|
+
const park = control.park
|
|
327
|
+
const ready = park !== undefined && parkComplete(park) ? park : undefined
|
|
328
|
+
yield* writeControl(runId, {
|
|
329
|
+
pending: remainingPending(pending, scope),
|
|
330
|
+
liveToken: drainToken,
|
|
331
|
+
park,
|
|
332
|
+
leasedGeneration: ready?.generation
|
|
333
|
+
})
|
|
334
|
+
return {
|
|
335
|
+
_tag: 'Run',
|
|
336
|
+
drainToken,
|
|
337
|
+
readyResponses: ready === undefined ? [] : ready.responses
|
|
338
|
+
} as const
|
|
339
|
+
})
|
|
340
|
+
)
|
|
341
|
+
),
|
|
342
|
+
endDrain: (runId, drainToken, acknowledged) =>
|
|
343
|
+
withGate(
|
|
344
|
+
Effect.uninterruptible(
|
|
345
|
+
Effect.gen(function* () {
|
|
346
|
+
const control = yield* getControl(runId)
|
|
347
|
+
if (control.liveToken !== drainToken) return
|
|
348
|
+
const leased = control.leasedGeneration
|
|
349
|
+
const park = control.park
|
|
350
|
+
const clearPark =
|
|
351
|
+
acknowledged &&
|
|
352
|
+
leased !== undefined &&
|
|
353
|
+
park !== undefined &&
|
|
354
|
+
park.generation === leased
|
|
355
|
+
yield* writeControl(runId, {
|
|
356
|
+
pending: control.pending,
|
|
357
|
+
liveToken: undefined,
|
|
358
|
+
leasedGeneration: undefined,
|
|
359
|
+
park: clearPark ? undefined : park
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
)
|
|
363
|
+
),
|
|
364
|
+
invalidate: (runId, interrupt, releaseIdleClaim) =>
|
|
365
|
+
withGate(
|
|
366
|
+
Effect.uninterruptible(
|
|
367
|
+
Effect.gen(function* () {
|
|
368
|
+
const control = yield* getControl(runId)
|
|
369
|
+
const hadPark = control.park !== undefined
|
|
370
|
+
yield* dropQueued(runId)
|
|
371
|
+
yield* writeControl(runId, emptyControl)
|
|
372
|
+
const interrupted = yield* interrupt
|
|
373
|
+
if (!interrupted) yield* releaseIdleClaim
|
|
374
|
+
return { hadPark, interrupted }
|
|
375
|
+
})
|
|
376
|
+
)
|
|
377
|
+
),
|
|
378
|
+
enqueueAndWake: (item, wake) =>
|
|
379
|
+
withGate(
|
|
380
|
+
Effect.uninterruptible(
|
|
381
|
+
Effect.gen(function* () {
|
|
382
|
+
yield* Ref.update(items, current => [...current, item])
|
|
383
|
+
const control = yield* getControl(item.runId)
|
|
384
|
+
if (parkBlocked(control.park)) return
|
|
385
|
+
yield* writeControl(item.runId, {
|
|
386
|
+
pending: widerPending(control.pending, itemScope(item)),
|
|
387
|
+
liveToken: control.liveToken,
|
|
388
|
+
leasedGeneration: control.leasedGeneration,
|
|
389
|
+
park: control.park
|
|
390
|
+
})
|
|
391
|
+
yield* wake
|
|
392
|
+
})
|
|
393
|
+
)
|
|
394
|
+
),
|
|
395
|
+
wakeIfUnblocked: (runId, scope, wake) =>
|
|
396
|
+
withGate(
|
|
397
|
+
Effect.uninterruptible(
|
|
398
|
+
Effect.gen(function* () {
|
|
399
|
+
const control = yield* getControl(runId)
|
|
400
|
+
if (parkBlocked(control.park)) return false
|
|
401
|
+
yield* writeControl(runId, {
|
|
402
|
+
pending: widerPending(control.pending, scope),
|
|
403
|
+
liveToken: control.liveToken,
|
|
404
|
+
leasedGeneration: control.leasedGeneration,
|
|
405
|
+
park: control.park
|
|
406
|
+
})
|
|
407
|
+
yield* wake
|
|
408
|
+
return true
|
|
409
|
+
})
|
|
410
|
+
)
|
|
411
|
+
),
|
|
412
|
+
startIfUnblocked: (runId, start) =>
|
|
413
|
+
withGate(
|
|
414
|
+
Effect.uninterruptible(
|
|
415
|
+
Effect.gen(function* () {
|
|
416
|
+
const control = yield* getControl(runId)
|
|
417
|
+
if (parkBlocked(control.park)) return undefined
|
|
418
|
+
const ticket = yield* start
|
|
419
|
+
if (ticket._tag === 'Started') {
|
|
420
|
+
yield* writeControl(runId, {
|
|
421
|
+
pending: widerPending(control.pending, 'input'),
|
|
422
|
+
liveToken: control.liveToken,
|
|
423
|
+
leasedGeneration: control.leasedGeneration,
|
|
424
|
+
park: control.park
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
return ticket
|
|
428
|
+
})
|
|
429
|
+
)
|
|
430
|
+
),
|
|
431
|
+
admitRecovery: (runId, attempt, wake) =>
|
|
432
|
+
Effect.uninterruptibleMask(restore =>
|
|
433
|
+
restore(gate.take(1)).pipe(
|
|
434
|
+
Effect.flatMap(() =>
|
|
435
|
+
Effect.gen(function* () {
|
|
436
|
+
const control = yield* getControl(runId)
|
|
437
|
+
if (parkBlocked(control.park)) return { _tag: 'Skip' } as const
|
|
438
|
+
const decision = yield* attempt
|
|
439
|
+
if (decision._tag !== 'Resume') return decision
|
|
440
|
+
yield* writeControl(runId, {
|
|
441
|
+
pending: widerPending(control.pending, 'input'),
|
|
442
|
+
liveToken: control.liveToken,
|
|
443
|
+
leasedGeneration: control.leasedGeneration,
|
|
444
|
+
park: control.park
|
|
445
|
+
})
|
|
446
|
+
yield* wake
|
|
447
|
+
return { _tag: 'Resumed' } as const
|
|
448
|
+
}).pipe(Effect.ensuring(gate.release(1)))
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
)
|
package/src/index.ts
ADDED