@empyria/restate 0.4.14 → 0.4.16

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/README.md CHANGED
@@ -118,10 +118,6 @@ endpoint, then create jobs with either:
118
118
  starts with the application.** The job has the stable `id` you give it, so calling it on every start never adds a second
119
119
  job: the same request changes nothing, and a changed request (another schedule, target or payload) replaces the pending
120
120
  run under the same ID. Call it once the endpoint has registered, e.g. from `setupRestate`'s `registrationCallbackFn`.
121
- It also repairs a job whose chain broke: each run schedules the next, so a pending run that was killed, cancelled or
122
- lost with a deleted deployment would stop the job for good. When the same request finds its pending run overdue by
123
- more than `OVERDUE_GRACE_MS` (60 s), `ensure` schedules a fresh one; a replaced run that still arrives does nothing,
124
- because a run only works when it is the job's current one.
125
121
 
126
122
  Cron expressions are `croner` patterns (five fields, or six with seconds for tests). The target handler is sent the
127
123
  `payload` as its input, or no input when there is none.
package/lib/Cron.js CHANGED
@@ -27,15 +27,6 @@ export const RETRY_FOREVER = {
27
27
  exponentiationFactor: 2,
28
28
  }
29
29
 
30
- /**
31
- * How late a job's pending run may be before `ensure` treats it as lost and schedules a fresh one. A run is normally
32
- * late only by the time the services were down; a run that is late by more than this, when the application starts and
33
- * calls `ensure`, has most likely been killed, cancelled or lost with a deleted deployment, and nothing else would ever
34
- * schedule the next one. Replacing a run that is merely slow is harmless: `execute` ignores a run that is no longer
35
- * the job's current one.
36
- */
37
- export const OVERDUE_GRACE_MS = 60_000
38
-
39
30
  export const cronJobInitiator = restate.service({
40
31
  name: 'CronJobInitiator',
41
32
  handlers: {
@@ -65,8 +56,7 @@ export const cronJob = restate.object({
65
56
  * code that starts with the application and cannot know whether the job was created before.
66
57
  *
67
58
  * - no job yet: schedules it, as `initiate` does;
68
- * - the same request already scheduled: changes nothing and returns the current state, unless its pending run
69
- * is overdue by more than {@link OVERDUE_GRACE_MS} (killed, cancelled or lost): then it schedules a fresh one;
59
+ * - the same request already scheduled: changes nothing and returns the current state;
70
60
  * - a different request (another schedule, target or payload): cancels the pending run and
71
61
  * schedules the new request under the same job ID.
72
62
  */
@@ -74,11 +64,7 @@ export const cronJob = restate.object({
74
64
  const wanted = normalizeRequest(request)
75
65
  const jobState = await ctx.get(JOB_STATE_NAME)
76
66
 
77
- if (jobState && sameRequest(jobState.request, wanted)) {
78
- const dueAt = jobState.next_execution_at ?? Date.parse(jobState.next_execution_time)
79
- const now = await ctx.date.now()
80
- if (!(Number.isFinite(dueAt) && now > dueAt + OVERDUE_GRACE_MS)) return jobState
81
- }
67
+ if (jobState && sameRequest(jobState.request, wanted)) return jobState
82
68
  if (jobState) ctx.cancel(jobState.next_execution_id)
83
69
 
84
70
  return await scheduleNextExecution(ctx, wanted)
@@ -88,15 +74,6 @@ export const cronJob = restate.object({
88
74
  if (!jobState) {
89
75
  throw new TerminalError('Job not found.')
90
76
  }
91
- // Only the job's current run works and schedules the next one. A run that `ensure` replaced (or that a
92
- // changed schedule superseded) and that still arrives does nothing, so a job never runs as two chains.
93
- const self = ctx.request?.().id
94
- if (self && jobState.next_execution_id && self !== jobState.next_execution_id) {
95
- ctx.console?.log?.(
96
- `CronJob ${ctx.key}: run ${self} is not the current one (${jobState.next_execution_id}), skipped`,
97
- )
98
- return
99
- }
100
77
 
101
78
  // execute the task
102
79
  const { service, method, key, payload } = jobState.request
@@ -179,8 +156,6 @@ const scheduleNextExecution = async (ctx, request) => {
179
156
  const jobState = {
180
157
  request,
181
158
  next_execution_time: next.toString(),
182
- // The same instant as a number, so `ensure` can tell an overdue run without parsing a date string.
183
- next_execution_at: next.getTime(),
184
159
  next_execution_id: await handle.invocationId,
185
160
  }
186
161
  ctx.set(JOB_STATE_NAME, jobState)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empyria/restate",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "description": "Restate.dev helpers for the Empyria nanoservice framework",
5
5
  "license": "MIT",
6
6
  "author": "Imre Fazekas <imre.fazekas@icloud.com>",
@@ -26,8 +26,8 @@
26
26
  "test:watch": "bun test --watch"
27
27
  },
28
28
  "dependencies": {
29
- "@empyria/classification": "0.1.9",
30
- "@empyria/common": "0.3.10",
29
+ "@empyria/classification": "0.1.10",
30
+ "@empyria/common": "0.3.12",
31
31
  "@restatedev/pubsub": "0.3.2",
32
32
  "@restatedev/pubsub-client": "0.3.2",
33
33
  "@restatedev/restate-sdk": "1.17.2",
@@ -36,12 +36,12 @@
36
36
  "croner": "10.0.1"
37
37
  },
38
38
  "devDependencies": {
39
- "@ai-sdk/openai-compatible": "3.0.62",
40
- "@ai-sdk/provider": "4.0.21",
39
+ "@ai-sdk/openai-compatible": "3.0.63",
40
+ "@ai-sdk/provider": "4.0.22",
41
41
  "@restatedev/restate-server": "1.7.13",
42
- "ai": "7.0.127",
43
- "oxfmt": "0.71.0",
44
- "oxlint": "1.86.0"
42
+ "ai": "7.0.128",
43
+ "oxfmt": "0.72.0",
44
+ "oxlint": "1.87.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@ai-sdk/openai-compatible": "^3.0.60",
package/test/Cron.test.js CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  ensureCronJob,
6
6
  setJobStateName,
7
7
  JOB_STATE_NAME,
8
- OVERDUE_GRACE_MS,
9
8
  } from '../lib/Cron.js'
10
9
  import { TerminalError } from '@restatedev/restate-sdk'
11
10
 
@@ -35,7 +34,7 @@ describe('cronJobInitiator.create', () => {
35
34
 
36
35
  // Every cronJob handler that schedules a next run needs the same "date/objectSendClient/set"
37
36
  // wiring; this factory keeps the mocked context consistent across tests.
38
- function makeSchedulingCtx({ existing, now = Date.now(), invocationId } = {}) {
37
+ function makeSchedulingCtx({ existing } = {}) {
39
38
  const sets = []
40
39
  const cancelled = []
41
40
  return {
@@ -43,8 +42,7 @@ function makeSchedulingCtx({ existing, now = Date.now(), invocationId } = {}) {
43
42
  cancelled,
44
43
  cancel: (invocationId) => cancelled.push(invocationId),
45
44
  get: async () => existing,
46
- date: { now: async () => now },
47
- ...(invocationId ? { request: () => ({ id: invocationId }) } : {}),
45
+ date: { now: async () => Date.now() },
48
46
  key: 'job-1',
49
47
  objectSendClient: () => ({
50
48
  execute: () => ({ invocationId: Promise.resolve('exec-inv-1') }),
@@ -111,37 +109,6 @@ describe('cronJob.execute', () => {
111
109
  expect(ctx.sets.length).toBe(1)
112
110
  })
113
111
 
114
- test("a run that is not the job's current one does nothing and schedules nothing", async () => {
115
- const sent = []
116
- const ctx = makeSchedulingCtx({
117
- invocationId: 'inv-replaced',
118
- existing: {
119
- request: { service: 'Svc', method: 'do', cronExpression: '* * * * *' },
120
- next_execution_id: 'inv-current',
121
- },
122
- })
123
- ctx.genericSend = (opts) => sent.push(opts)
124
- await cronJob.object.execute(ctx)
125
- expect(sent).toEqual([])
126
- expect(ctx.sets).toEqual([])
127
- })
128
-
129
- test('the current run works and schedules the next one', async () => {
130
- const sent = []
131
- const ctx = makeSchedulingCtx({
132
- invocationId: 'inv-current',
133
- existing: {
134
- request: { service: 'Svc', method: 'do', cronExpression: '* * * * *' },
135
- next_execution_id: 'inv-current',
136
- },
137
- })
138
- ctx.genericSend = (opts) => sent.push(opts)
139
- await cronJob.object.execute(ctx)
140
- expect(sent.length).toBe(1)
141
- expect(ctx.sets.length).toBe(1)
142
- expect(ctx.sets[0][1].next_execution_at).toBeNumber()
143
- })
144
-
145
112
  test('throws when there is no job state to execute', async () => {
146
113
  const ctx = makeSchedulingCtx({ existing: undefined })
147
114
  await expect(cronJob.object.execute(ctx)).rejects.toThrow(TerminalError)
@@ -212,42 +179,6 @@ describe('cronJob.ensure', () => {
212
179
  expect(ctx.cancelled).toEqual([])
213
180
  })
214
181
 
215
- test('the same request with a pending run that is long overdue (lost) schedules a fresh run', async () => {
216
- const now = Date.parse('2026-10-05T08:00:00Z')
217
- const existing = {
218
- request,
219
- next_execution_time: new Date(now - OVERDUE_GRACE_MS - 1000).toString(),
220
- next_execution_at: now - OVERDUE_GRACE_MS - 1000,
221
- next_execution_id: 'inv-killed',
222
- }
223
- const ctx = makeSchedulingCtx({ existing, now })
224
- const state = await cronJob.object.ensure(ctx, request)
225
- expect(state.next_execution_id).toBe('exec-inv-1')
226
- expect(state.next_execution_at).toBeGreaterThan(now)
227
- expect(ctx.cancelled).toEqual(['inv-killed'])
228
- })
229
-
230
- test('a state written before next_execution_at existed is judged by its date string', async () => {
231
- const now = Date.parse('2026-10-05T08:00:00Z')
232
- const existing = {
233
- request,
234
- next_execution_time: new Date(now - 10 * 60_000).toString(),
235
- next_execution_id: 'inv-killed',
236
- }
237
- const ctx = makeSchedulingCtx({ existing, now })
238
- expect((await cronJob.object.ensure(ctx, request)).next_execution_id).toBe('exec-inv-1')
239
- })
240
-
241
- test('a pending run that is due soon, or only a little late, is left alone', async () => {
242
- const now = Date.parse('2026-10-05T08:00:00Z')
243
- for (const dueAt of [now + 30_000, now - OVERDUE_GRACE_MS + 1000]) {
244
- const existing = { request, next_execution_at: dueAt, next_execution_id: 'inv-pending' }
245
- const ctx = makeSchedulingCtx({ existing, now })
246
- expect(await cronJob.object.ensure(ctx, request)).toBe(existing)
247
- expect(ctx.cancelled).toEqual([])
248
- }
249
- })
250
-
251
182
  test('unknown fields and field order do not make a request "different"', async () => {
252
183
  const existing = { request, next_execution_time: 'T', next_execution_id: 'inv-old' }
253
184
  const ctx = makeSchedulingCtx({ existing })