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