tardie 0.22.1 → 0.22.2-rc.258

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tardie",
3
- "version": "0.22.1",
3
+ "version": "0.22.2-rc.258",
4
4
  "license": "MIT",
5
5
  "author": "Clavia, Inc.",
6
6
  "description": "A durable agent harness. State is a pure function of the log.",
@@ -16,7 +16,7 @@
16
16
  "access": "public"
17
17
  },
18
18
  "tardigrade": {
19
- "sourceTree": "7ad7583338cc8d69829a810313b4f1d5dd7ec1a3"
19
+ "sourceTree": "a86f7a006f44eb41fcc97c3ebae698b8fc0db60a"
20
20
  },
21
21
  "files": [
22
22
  "src",
package/src/bun/host.ts CHANGED
@@ -19,7 +19,7 @@ import type { ThreadAllocation } from "tardie/core/actor/allocation"
19
19
  import { formatThreadAddress, parseThreadAddress, type ThreadAddress, type ProviderEndpoint } from "tardie/core/transport/endpoint"
20
20
  import type { Link } from "tardie/core/transport/link"
21
21
  import { actorEventKeyOf, actorThreadsOf, type ActorThreadRecord, type ThreadRegistered, type ThreadRequested } from "tardie/core/actor"
22
- import { alarmFired, earliestDeadlineOf } from "tardie/core/interaction/timeout"
22
+ import { alarmFired, deadlineCancellationEventsAt, earliestDeadlineOf } from "tardie/core/interaction/timeout"
23
23
  import { methodIngressKeyOf } from "tardie/core/interaction/invocation"
24
24
  import { type ActorMethods } from "tardie/core/actor/method"
25
25
  import {
@@ -595,7 +595,12 @@ export const createBunHost = async <R = never>(options: BunHostOptions<R>): Prom
595
595
  const active = await runtimeOf(thread)
596
596
  if (active.alarm?.deadlineAt !== deadlineAt) return
597
597
  delete active.alarm
598
- await appendTo(thread, [alarmFired({ scheduledFor: deadlineAt, at })])
598
+ // synchronizeAlarm commits each alarm with its crossed deadline cancellations (host.test.ts, "an alarm commits its deadline cancellation atomically").
599
+ const log = await active.runtime.runPromise(active.store.read)
600
+ await appendTo(thread, [
601
+ alarmFired({ scheduledFor: deadlineAt, at }),
602
+ ...(methods === undefined ? [] : deadlineCancellationEventsAt(log, methods, at))
603
+ ])
599
604
  driver.mark(thread)
600
605
  await drive()
601
606
  })
@@ -12,7 +12,7 @@ import { ThreadAllocator, reserveRootThread } from "tardie/core/actor/allocation
12
12
  import { initializingThreadAllocator } from "tardie/host/allocation"
13
13
  import { formatThreadAddress, type ThreadAddress, type ProviderEndpoint } from "tardie/core/transport/endpoint"
14
14
  import type { Link } from "tardie/core/transport/link"
15
- import { alarmFired, earliestDeadlineOf } from "tardie/core/interaction/timeout"
15
+ import { alarmFired, deadlineCancellationEventsAt, earliestDeadlineOf } from "tardie/core/interaction/timeout"
16
16
  import { methodIngressKeyOf } from "tardie/core/interaction/invocation"
17
17
  import { type ActorMethods } from "tardie/core/actor/method"
18
18
  import {
@@ -230,12 +230,16 @@ export async function createCloudflareThreadHost<R = never>(options: CloudflareT
230
230
  const nextMethodDeadline = async (): Promise<number | undefined> => {
231
231
  return earliestDeadlineOf(await Effect.runPromise(events.read), methods)
232
232
  }
233
+ // recordAlarm commits each alarm with its crossed deadline cancellations (test/actor.workers.ts, "an alarm commits its deadline cancellation atomically").
233
234
  const recordAlarm = async (at: number): Promise<void> => {
234
- const deadline = earliestDeadlineOf(await Effect.runPromise(events.read), methods)
235
+ const log = await Effect.runPromise(events.read)
236
+ const deadline = earliestDeadlineOf(log, methods)
235
237
  if (deadline !== undefined && deadline <= at) {
236
- const result = await Effect.runPromise(events.append([alarmFired({ scheduledFor: deadline, at })]))
238
+ const result = await Effect.runPromise(store.append([
239
+ alarmFired({ scheduledFor: deadline, at }),
240
+ ...(methods === undefined ? [] : deadlineCancellationEventsAt(log, methods, at))
241
+ ]))
237
242
  if (result.appended > 0) driver.mark(options.thread)
238
- await Effect.runPromise(syncCommit(result))
239
243
  } else {
240
244
  await options.storage.sync()
241
245
  }
@@ -10,7 +10,7 @@ import { type ActorInvocationContext, invocationKey, sameInvocation, type Invoca
10
10
 
11
11
  import { recordedDispatchOf, terminalInvocationRefOf, terminalStorageKey, type RecordedDispatch } from "./records-compat"
12
12
 
13
- import { cancellationStateOf, initialMethodStates, reduceMethodStates } from "./state"
13
+ import { cancellationStateOf, initialMethodStates, reduceMethodStates, type ActorMethodView } from "./state"
14
14
  import { type ActorMethods } from "../actor/method"
15
15
 
16
16
  export interface AlarmFiredFields {
@@ -142,6 +142,42 @@ const deadlineCancellationTransition = (invocation: InvocationRef, deadlineAt: n
142
142
  })]
143
143
  })
144
144
 
145
+ // deadlineCancellationsAt selects crossed deadlines for running cancellable invocations (timeout.test.ts, "cancellation selection projects one cancellation from an eligible invocation").
146
+ const deadlineCancellationsAt = (
147
+ log: ReadonlyArray<Event>,
148
+ methods: ActorMethods,
149
+ at: number
150
+ ): ReadonlyArray<InvocationDeadline> => {
151
+ const views = new Map<string, ActorMethodView<unknown>>()
152
+ return invocationDeadlinesOf(log).flatMap(({ invocation, deadlineAt }) => {
153
+ if (deadlineAt > at) return []
154
+ const method = methods[invocation.method]
155
+ if (method === undefined || method.cancellation === undefined || invocationSettled(log, invocation)) return []
156
+ let view = views.get(invocation.method)
157
+ if (view === undefined) {
158
+ view = replayProjection(method.projection, log)
159
+ views.set(invocation.method, view)
160
+ }
161
+ return cancellationStateOf(method, view, invocation) !== "running" ? [] : [{ invocation, deadlineAt }]
162
+ })
163
+ }
164
+
165
+ // deadlineCancellationEventsAt constructs durable requests for selected deadline cancellations (test/actor.workers.ts, "an alarm commits its deadline cancellation atomically").
166
+ export const deadlineCancellationEventsAt = (
167
+ log: ReadonlyArray<Event>,
168
+ methods: ActorMethods,
169
+ at: number
170
+ ): ReadonlyArray<Event> =>
171
+ deadlineCancellationsAt(log, methods, at).map(({ invocation, deadlineAt }) =>
172
+ cancellationRequested({
173
+ request: `deadline/${invocation.method}/${invocation.id}/${invocation.epoch}/${deadlineAt}`,
174
+ invocation,
175
+ cause: "deadline",
176
+ deadlineAt,
177
+ at
178
+ })
179
+ )
180
+
145
181
  // methodTimeoutDerivation turns alarm facts into method terminals without reading a clock.
146
182
  export const methodTimeoutDerivation: CompleteTransitionDerivation = (log) => {
147
183
  const terminal = terminalCalls(log)
@@ -153,18 +189,15 @@ export const methodTimeoutDerivation: CompleteTransitionDerivation = (log) => {
153
189
  })
154
190
  }
155
191
 
156
- // methodDeadlineCancellationDerivation turns an accepted invocation deadline into the same durable cancellation used by callers.
192
+ // methodDeadlineCancellationDerivation repairs missing cancellations from recorded alarms (timeout.test.ts, "legacy recovery derives an alarm's missing cancellation exactly once").
157
193
  export const methodDeadlineCancellationDerivation = (methods: ActorMethods): CompleteTransitionDerivation => (log) => {
158
- const alarms = alarmsOf(log)
159
- return invocationDeadlinesOf(log).flatMap(({ invocation, deadlineAt }) => {
160
- const cancellation = methods[invocation.method]?.cancellation
161
- if (cancellation === undefined || invocationSettled(log, invocation)) return []
162
- const alarm = alarmFor(alarms, deadlineAt)
163
- const method = methods[invocation.method]
164
- if (alarm === undefined || method === undefined ||
165
- cancellationStateOf(method, replayProjection(method.projection, log), invocation) !== "running") return []
166
- return [deadlineCancellationTransition(invocation, deadlineAt)]
167
- })
194
+ const latestAlarmAt = alarmsOf(log).reduce<number | undefined>(
195
+ (latest, alarm) => latest === undefined ? alarm.at : Math.max(latest, alarm.at),
196
+ undefined
197
+ )
198
+ if (latestAlarmAt === undefined) return []
199
+ return deadlineCancellationsAt(log, methods, latestAlarmAt)
200
+ .map(({ invocation, deadlineAt }) => deadlineCancellationTransition(invocation, deadlineAt))
168
201
  }
169
202
 
170
203
  /** @deprecated Use methodTimeoutDerivation. This compatibility name describes a complete-history transition derivation. */