@empyria/restate 0.4.28 → 0.4.30
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 +3 -0
- package/lib/Cron.js +32 -0
- package/package.json +11 -11
- package/test/Cron.test.js +45 -0
package/README.md
CHANGED
|
@@ -81,6 +81,9 @@ endpoint, then create jobs with either:
|
|
|
81
81
|
starts with the application.** The job has the stable `id` you give it, so calling it on every start never adds a second
|
|
82
82
|
job: the same request changes nothing, and a changed request (another schedule, target or payload) replaces the pending
|
|
83
83
|
run under the same ID. Call it once the endpoint has registered, e.g. from `setupRestate`'s `registrationCallbackFn`.
|
|
84
|
+
- `getCronJob({restateURL, id})`: the job's request and next run, or `null` when it does not exist;
|
|
85
|
+
- `cancelCronJob({restateURL, id})`: cancels the pending run, clears the job and **reads it back**: it resolves only when
|
|
86
|
+
the job is really gone, and throws otherwise. Safe to repeat, and for a job that never existed.
|
|
84
87
|
|
|
85
88
|
Cron expressions are `croner` patterns (five fields, or six with seconds for tests). The target handler is sent the
|
|
86
89
|
`payload` as its input, or no input when there is none.
|
package/lib/Cron.js
CHANGED
|
@@ -156,6 +156,8 @@ const scheduleNextExecution = async (ctx, request) => {
|
|
|
156
156
|
const jobState = {
|
|
157
157
|
request,
|
|
158
158
|
next_execution_time: next.toString(),
|
|
159
|
+
// The same instant in ms since 1970, for callers that store or compare it.
|
|
160
|
+
next_execution_at: next.getTime(),
|
|
159
161
|
next_execution_id: await handle.invocationId,
|
|
160
162
|
}
|
|
161
163
|
ctx.set(JOB_STATE_NAME, jobState)
|
|
@@ -188,4 +190,34 @@ export async function ensureCronJob({ restateURL, id, ...request }) {
|
|
|
188
190
|
})
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
const checkJobId = (fn, id) => {
|
|
194
|
+
if (typeof id !== 'string' || !/^[A-Za-z0-9._:-]+$/.test(id)) {
|
|
195
|
+
throw new TypeError(`${fn} needs an id made of letters, digits, ".", "_", ":" and "-"`)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* What a recurring job is doing now, from outside Restate: its request and next run, or `null` when no such job
|
|
201
|
+
* exists (never created, or cancelled).
|
|
202
|
+
* @param {{restateURL: string, id: string}} params
|
|
203
|
+
*/
|
|
204
|
+
export async function getCronJob({ restateURL, id }) {
|
|
205
|
+
checkJobId('getCronJob', id)
|
|
206
|
+
return (await sendMessage({ restateURL, name: 'CronJob', message: 'getInfo', key: id })) ?? null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Cancels a recurring job and checks that it is gone: the pending run is cancelled and the job's state cleared, then
|
|
211
|
+
* the job is read back. Resolves only when the job really does not exist any more, so a caller may act on it (for
|
|
212
|
+
* example move a card) without a second look. Safe to repeat, and safe for a job that never existed.
|
|
213
|
+
* @param {{restateURL: string, id: string}} params
|
|
214
|
+
* @returns {Promise<void>}
|
|
215
|
+
*/
|
|
216
|
+
export async function cancelCronJob({ restateURL, id }) {
|
|
217
|
+
checkJobId('cancelCronJob', id)
|
|
218
|
+
await sendMessage({ restateURL, name: 'CronJob', message: 'cancel', key: id })
|
|
219
|
+
const left = await getCronJob({ restateURL, id })
|
|
220
|
+
if (left) throw new Error(`Cron job '${id}' is still scheduled after it was cancelled`)
|
|
221
|
+
}
|
|
222
|
+
|
|
191
223
|
export { JOB_STATE_NAME }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empyria/restate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.30",
|
|
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,22 +26,22 @@
|
|
|
26
26
|
"test:watch": "bun test --watch"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@empyria/classification": "0.1.
|
|
30
|
-
"@empyria/common": "0.3.
|
|
29
|
+
"@empyria/classification": "0.1.14",
|
|
30
|
+
"@empyria/common": "0.3.17",
|
|
31
31
|
"@restatedev/pubsub": "0.3.2",
|
|
32
32
|
"@restatedev/pubsub-client": "0.3.2",
|
|
33
|
-
"@restatedev/restate-sdk": "1.
|
|
34
|
-
"@restatedev/restate-sdk-clients": "1.
|
|
35
|
-
"@restatedev/restate-sdk-core": "1.
|
|
36
|
-
"croner": "
|
|
33
|
+
"@restatedev/restate-sdk": "1.18.0",
|
|
34
|
+
"@restatedev/restate-sdk-clients": "1.18.0",
|
|
35
|
+
"@restatedev/restate-sdk-core": "1.18.0",
|
|
36
|
+
"croner": "11.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@ai-sdk/openai-compatible": "3.0.
|
|
39
|
+
"@ai-sdk/openai-compatible": "3.0.80",
|
|
40
40
|
"@ai-sdk/provider": "4.0.34",
|
|
41
41
|
"@restatedev/restate-server": "1.8.1",
|
|
42
|
-
"ai": "7.0.
|
|
43
|
-
"oxfmt": "0.
|
|
44
|
-
"oxlint": "1.
|
|
42
|
+
"ai": "7.0.156",
|
|
43
|
+
"oxfmt": "0.75.0",
|
|
44
|
+
"oxlint": "1.90.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@ai-sdk/openai-compatible": "^3.0.60",
|
package/test/Cron.test.js
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
cronJobInitiator,
|
|
4
4
|
cronJob,
|
|
5
5
|
ensureCronJob,
|
|
6
|
+
getCronJob,
|
|
7
|
+
cancelCronJob,
|
|
6
8
|
setJobStateName,
|
|
7
9
|
JOB_STATE_NAME,
|
|
8
10
|
} from '../lib/Cron.js'
|
|
@@ -300,3 +302,46 @@ describe('retry policy', () => {
|
|
|
300
302
|
expect(cronJob.options?.retryPolicy ?? cronJob.retryPolicy).toBeDefined()
|
|
301
303
|
})
|
|
302
304
|
})
|
|
305
|
+
|
|
306
|
+
describe('getCronJob and cancelCronJob', () => {
|
|
307
|
+
const realFetch = globalThis.fetch
|
|
308
|
+
afterEach(() => {
|
|
309
|
+
globalThis.fetch = realFetch
|
|
310
|
+
})
|
|
311
|
+
const respond = (script) => {
|
|
312
|
+
const calls = []
|
|
313
|
+
globalThis.fetch = async (url) => {
|
|
314
|
+
calls.push(url)
|
|
315
|
+
const body = script.shift()
|
|
316
|
+
return new Response(body === undefined ? '' : JSON.stringify(body), { status: 200 })
|
|
317
|
+
}
|
|
318
|
+
return calls
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
test('getCronJob answers null when the job does not exist', async () => {
|
|
322
|
+
respond([undefined])
|
|
323
|
+
expect(await getCronJob({ restateURL: 'http://r', id: 'a:1' })).toBeNull()
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('cancelCronJob cancels, then reads the job back before it resolves', async () => {
|
|
327
|
+
const calls = respond([undefined, undefined])
|
|
328
|
+
await cancelCronJob({ restateURL: 'http://r', id: 'a:1' })
|
|
329
|
+
expect(calls).toEqual(['http://r/CronJob/a:1/cancel', 'http://r/CronJob/a:1/getInfo'])
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test('cancelCronJob fails when the job is still there afterwards', async () => {
|
|
333
|
+
respond([undefined, { request: {} }])
|
|
334
|
+
expect(cancelCronJob({ restateURL: 'http://r', id: 'a:1' })).rejects.toThrow(
|
|
335
|
+
'still scheduled',
|
|
336
|
+
)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('cancelCronJob fails when the cancel call fails', async () => {
|
|
340
|
+
globalThis.fetch = async () => new Response('', { status: 503, statusText: 'down' })
|
|
341
|
+
expect(cancelCronJob({ restateURL: 'http://r', id: 'a:1' })).rejects.toThrow('503')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('cancelCronJob refuses an id that cannot be a job id', () => {
|
|
345
|
+
expect(cancelCronJob({ restateURL: 'http://r', id: 'a b' })).rejects.toThrow(TypeError)
|
|
346
|
+
})
|
|
347
|
+
})
|