@pikku/schedule 0.12.4 → 0.12.5

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/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## 0.12.5
2
+
3
+ ### Patch Changes
4
+
5
+ - dbff6ae: **A delayed RPC now actually runs.** `InMemorySchedulerService.scheduleRPC`
6
+ dispatched through `runScheduledTask`, which resolves names against the wired
7
+ _cron task_ registry — a registry no internally scheduled RPC is ever in. Every
8
+ such call died as `ScheduledTaskNotFoundError` and was swallowed into a log
9
+ line, and the payload captured alongside it was never forwarded. The one that
10
+ mattered was `pikkuWorkflowSleeper`: it is what wakes a run from
11
+ `workflow.sleep`, so an async workflow containing a sleep stopped at the first
12
+ one and stayed `running` forever. It now invokes the RPC with its data, matching
13
+ what the BullMQ and pg-boss schedulers already do.
14
+ - Updated dependencies [41c1a95]
15
+ - Updated dependencies [ce96383]
16
+ - Updated dependencies [7e60867]
17
+ - Updated dependencies [f8f1244]
18
+ - Updated dependencies [dcf20cb]
19
+ - Updated dependencies [6512384]
20
+ - Updated dependencies [e3b4c14]
21
+ - Updated dependencies [efd0ed1]
22
+ - Updated dependencies [cba98fb]
23
+ - Updated dependencies [ce96383]
24
+ - Updated dependencies [f8f1244]
25
+ - Updated dependencies [f8f1244]
26
+ - Updated dependencies [6e93a35]
27
+ - Updated dependencies [6dada45]
28
+ - @pikku/core@0.12.80
29
+
1
30
  ## 0.12.4
2
31
 
3
32
  ### Patch Changes
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 - present Yasser Fadl and Pikku contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,28 @@
1
- # @pikku/scheduler
1
+ # @pikku/schedule
2
2
 
3
- This package is part of @pikku/mono and is responsible for scheduling tasks in pikku.
3
+ In-memory scheduler service for Pikku cron wirings, built on `cron`.
4
+
5
+ Runs jobs in the current process, so it suits single-instance deployments and
6
+ local development. For multi-instance setups use a queue-backed scheduler such
7
+ as `@pikku/queue-bullmq` or `@pikku/queue-pg-boss`.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @pikku/schedule
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```typescript
18
+ import { InMemorySchedulerService } from '@pikku/schedule'
19
+
20
+ const scheduler = new InMemorySchedulerService(logger)
21
+
22
+ await scheduler.init()
23
+ scheduler.startAll()
24
+ ```
25
+
26
+ ## Docs
27
+
28
+ https://pikku.dev/docs
@@ -1,7 +1,8 @@
1
1
  import { CronJob } from 'cron';
2
2
  import { SchedulerService, parseDurationString } from '@pikku/core';
3
- import { pikkuState } from '@pikku/core/internal';
3
+ import { pikkuState, getSingletonServices } from '@pikku/core/ecosystem';
4
4
  import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler';
5
+ import { rpcService } from '@pikku/core/rpc';
5
6
  const getLogger = () => {
6
7
  return pikkuState(null, 'package', 'singletonServices').logger;
7
8
  };
@@ -24,7 +25,23 @@ export class InMemorySchedulerService extends SchedulerService {
24
25
  const timer = setTimeout(async () => {
25
26
  this.delayedTasks.delete(taskId);
26
27
  try {
27
- await runScheduledTask({ name: rpcName, session });
28
+ // An RPC, not a cron task. `runScheduledTask` looks the name up in the
29
+ // scheduler's task registry, which only ever holds wired cron tasks —
30
+ // so every internally-scheduled RPC died here as
31
+ // ScheduledTaskNotFoundError. The one that matters most is
32
+ // `pikkuWorkflowSleeper`: it is registered by the workflow service as a
33
+ // function, and it is what wakes a run from `workflow.sleep`. With the
34
+ // wake-up lost to a swallowed log line, an async workflow run stopped
35
+ // at its first sleep and stayed `running` forever.
36
+ //
37
+ // `data` has to be forwarded for the same reason. The sleeper is called
38
+ // with `{ runId, stepId }` and cannot identify the sleeping step
39
+ // without it — it was captured in `delayedTasks` but never passed on.
40
+ // This matches what the BullMQ and pg-boss schedulers already do:
41
+ // enqueue `{ rpcName, data, session }` and invoke the RPC with it.
42
+ const services = getSingletonServices();
43
+ const rpc = rpcService.getContextRPCService(services, {}, false);
44
+ await rpc.invoke(rpcName, data);
28
45
  }
29
46
  catch (err) {
30
47
  getLogger().error(`Failed to execute delayed RPC '${rpcName}': ${err}`);
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@pikku/schedule",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
+ "description": "In-memory scheduler service for Pikku cron wirings",
4
5
  "author": "yasser.fadl@gmail.com",
5
6
  "license": "MIT",
6
7
  "module": "dist/index.js",
@@ -18,7 +19,7 @@
18
19
  "prepublishOnly": "yarn build"
19
20
  },
20
21
  "peerDependencies": {
21
- "@pikku/core": "^0.12.58"
22
+ "@pikku/core": "^0.12.80"
22
23
  },
23
24
  "devDependencies": {
24
25
  "@types/node": "^24.11.0",
@@ -1,9 +1,19 @@
1
1
  import { describe, test } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
- import { pikkuState } from '@pikku/core/internal'
3
+ import { pikkuState } from '@pikku/core/ecosystem'
4
+ import { addFunction } from '@pikku/core/function'
4
5
 
5
6
  import { InMemorySchedulerService } from './in-memory-scheduler-service.js'
6
7
 
8
+ const waitFor = async (predicate: () => boolean, withinMs = 2000) => {
9
+ const deadline = Date.now() + withinMs
10
+ while (Date.now() < deadline) {
11
+ if (predicate()) return true
12
+ await new Promise((resolve) => setTimeout(resolve, 10))
13
+ }
14
+ return false
15
+ }
16
+
7
17
  describe('InMemorySchedulerService scheduleRPC', () => {
8
18
  test('schedules and executes delayed RPC', async () => {
9
19
  const logger = {
@@ -31,4 +41,54 @@ describe('InMemorySchedulerService scheduleRPC', () => {
31
41
  await scheduler.close()
32
42
  }
33
43
  })
44
+
45
+ /**
46
+ * The delay actually has to invoke the RPC, with its data.
47
+ *
48
+ * The test above only proved the task was registered, which is why the
49
+ * scheduler could dispatch through `runScheduledTask` — a lookup against the
50
+ * *cron task* registry that no scheduled RPC is ever in — and still pass.
51
+ * Every internally scheduled RPC failed with ScheduledTaskNotFoundError,
52
+ * `pikkuWorkflowSleeper` among them, so an async workflow never woke from
53
+ * `workflow.sleep`.
54
+ */
55
+ test('invokes the named RPC with its data once the delay elapses', async () => {
56
+ const errors: string[] = []
57
+ const logger = {
58
+ info: () => {},
59
+ warn: () => {},
60
+ debug: () => {},
61
+ error: (message: string) => errors.push(message),
62
+ }
63
+ pikkuState(null, 'package', 'singletonServices', { logger } as any)
64
+
65
+ const calls: unknown[] = []
66
+ addFunction(
67
+ 'delayedTestRpc',
68
+ {
69
+ func: async (_services: any, data: unknown) => calls.push(data),
70
+ } as any,
71
+ null
72
+ )
73
+ pikkuState(null, 'function', 'meta')['delayedTestRpc'] = {
74
+ name: 'delayedTestRpc',
75
+ sessionless: true,
76
+ permissions: [],
77
+ } as any
78
+
79
+ const scheduler = new InMemorySchedulerService()
80
+ try {
81
+ await scheduler.scheduleRPC(20, 'delayedTestRpc', {
82
+ runId: 'run-1',
83
+ stepId: 'step-1',
84
+ })
85
+ assert.ok(
86
+ await waitFor(() => calls.length > 0),
87
+ `RPC was never invoked; scheduler errors: ${JSON.stringify(errors)}`
88
+ )
89
+ assert.deepEqual(calls[0], { runId: 'run-1', stepId: 'step-1' })
90
+ } finally {
91
+ await scheduler.close()
92
+ }
93
+ })
34
94
  })
@@ -5,8 +5,9 @@ import type {
5
5
  ScheduledTaskSummary,
6
6
  } from '@pikku/core'
7
7
  import { SchedulerService, parseDurationString } from '@pikku/core'
8
- import { pikkuState } from '@pikku/core/internal'
8
+ import { pikkuState, getSingletonServices } from '@pikku/core/ecosystem'
9
9
  import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler'
10
+ import { rpcService } from '@pikku/core/rpc'
10
11
 
11
12
  interface DelayedTask {
12
13
  taskId: string
@@ -49,7 +50,27 @@ export class InMemorySchedulerService extends SchedulerService {
49
50
  const timer = setTimeout(async () => {
50
51
  this.delayedTasks.delete(taskId)
51
52
  try {
52
- await runScheduledTask({ name: rpcName, session })
53
+ // An RPC, not a cron task. `runScheduledTask` looks the name up in the
54
+ // scheduler's task registry, which only ever holds wired cron tasks —
55
+ // so every internally-scheduled RPC died here as
56
+ // ScheduledTaskNotFoundError. The one that matters most is
57
+ // `pikkuWorkflowSleeper`: it is registered by the workflow service as a
58
+ // function, and it is what wakes a run from `workflow.sleep`. With the
59
+ // wake-up lost to a swallowed log line, an async workflow run stopped
60
+ // at its first sleep and stayed `running` forever.
61
+ //
62
+ // `data` has to be forwarded for the same reason. The sleeper is called
63
+ // with `{ runId, stepId }` and cannot identify the sleeping step
64
+ // without it — it was captured in `delayedTasks` but never passed on.
65
+ // This matches what the BullMQ and pg-boss schedulers already do:
66
+ // enqueue `{ rpcName, data, session }` and invoke the RPC with it.
67
+ const services = getSingletonServices()
68
+ const rpc = rpcService.getContextRPCService(
69
+ services as any,
70
+ {},
71
+ false
72
+ ) as { invoke: (name: string, data: unknown) => Promise<unknown> }
73
+ await rpc.invoke(rpcName, data)
53
74
  } catch (err: unknown) {
54
75
  getLogger().error(`Failed to execute delayed RPC '${rpcName}': ${err}`)
55
76
  }