@owlmeans/queue 0.1.18-rc.40 → 0.1.18-rc.41

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.
Files changed (89) hide show
  1. package/README.md +17 -16
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/queue/SKILL.md +26 -24
  4. package/agent-meta/skills/scheduled-jobs/SKILL.md +1 -1
  5. package/build/bridge/types.d.ts +25 -0
  6. package/build/bridge/types.d.ts.map +1 -0
  7. package/build/bridge/types.js +2 -0
  8. package/build/bridge/types.js.map +1 -0
  9. package/build/bridge.d.ts +5 -28
  10. package/build/bridge.d.ts.map +1 -1
  11. package/build/bridge.js +33 -104
  12. package/build/bridge.js.map +1 -1
  13. package/build/config.d.ts +6 -22
  14. package/build/config.d.ts.map +1 -1
  15. package/build/config.js +10 -92
  16. package/build/config.js.map +1 -1
  17. package/build/entrypoint-jobs/types.d.ts +29 -0
  18. package/build/entrypoint-jobs/types.d.ts.map +1 -0
  19. package/build/entrypoint-jobs/types.js +2 -0
  20. package/build/entrypoint-jobs/types.js.map +1 -0
  21. package/build/entrypoint-jobs.d.ts +5 -0
  22. package/build/entrypoint-jobs.d.ts.map +1 -0
  23. package/build/entrypoint-jobs.js +84 -0
  24. package/build/entrypoint-jobs.js.map +1 -0
  25. package/build/index.d.ts +8 -0
  26. package/build/index.d.ts.map +1 -1
  27. package/build/index.js +3 -0
  28. package/build/index.js.map +1 -1
  29. package/build/protocol/types.d.ts +12 -0
  30. package/build/protocol/types.d.ts.map +1 -0
  31. package/build/protocol/types.js +2 -0
  32. package/build/protocol/types.js.map +1 -0
  33. package/build/protocol.d.ts +6 -3
  34. package/build/protocol.d.ts.map +1 -1
  35. package/build/protocol.js +57 -50
  36. package/build/protocol.js.map +1 -1
  37. package/build/queue-config/types.d.ts +29 -0
  38. package/build/queue-config/types.d.ts.map +1 -0
  39. package/build/queue-config/types.js +2 -0
  40. package/build/queue-config/types.js.map +1 -0
  41. package/build/queue-config.d.ts +9 -0
  42. package/build/queue-config.d.ts.map +1 -0
  43. package/build/queue-config.js +83 -0
  44. package/build/queue-config.js.map +1 -0
  45. package/build/queue-route/types.d.ts +14 -0
  46. package/build/queue-route/types.d.ts.map +1 -0
  47. package/build/queue-route/types.js +2 -0
  48. package/build/queue-route/types.js.map +1 -0
  49. package/build/queue-route.d.ts +4 -0
  50. package/build/queue-route.d.ts.map +1 -0
  51. package/build/queue-route.js +24 -0
  52. package/build/queue-route.js.map +1 -0
  53. package/build/route.d.ts +2 -2
  54. package/build/route.d.ts.map +1 -1
  55. package/build/route.js +5 -20
  56. package/build/route.js.map +1 -1
  57. package/build/transport.d.ts.map +1 -1
  58. package/build/transport.js +2 -2
  59. package/build/transport.js.map +1 -1
  60. package/build/types.d.ts +3 -0
  61. package/build/types.d.ts.map +1 -1
  62. package/build/types.local.d.ts +2 -1
  63. package/build/types.local.d.ts.map +1 -1
  64. package/build/worker.d.ts +0 -17
  65. package/build/worker.d.ts.map +1 -1
  66. package/build/worker.js +1 -36
  67. package/build/worker.js.map +1 -1
  68. package/package.json +8 -8
  69. package/src/bridge/types.ts +25 -0
  70. package/src/bridge.ts +38 -118
  71. package/src/config.ts +14 -105
  72. package/src/entrypoint-jobs/types.ts +29 -0
  73. package/src/entrypoint-jobs.ts +105 -0
  74. package/src/index.ts +8 -0
  75. package/src/protocol/types.ts +16 -0
  76. package/src/protocol.ts +74 -53
  77. package/src/queue-config/types.ts +29 -0
  78. package/src/queue-config.ts +102 -0
  79. package/src/queue-route/types.ts +14 -0
  80. package/src/queue-route.ts +33 -0
  81. package/src/route.ts +6 -23
  82. package/src/transport.ts +3 -3
  83. package/src/types.ts +3 -0
  84. package/src/worker.ts +2 -48
  85. package/tests/bridge.spec.ts +8 -8
  86. package/tests/config.spec.ts +11 -10
  87. package/tests/protocol.spec.ts +6 -6
  88. package/tests/schedule.spec.ts +13 -14
  89. package/src/types.local.ts +0 -3
@@ -0,0 +1,102 @@
1
+ import { memoHelper } from '@owlmeans/context'
2
+ import type { Config, JobOptions, QueueDeclaration, ScheduleDeclaration } from './types.js'
3
+ import type { QueueConfigHelper } from './queue-config/types.js'
4
+ import { ScheduleMisdeclared, UnknownJobName, UnknownQueue } from './errors.js'
5
+
6
+ /**
7
+ * The queue configuration of one `cfg`. It reads `cfg.queue` on every call, so declarations made
8
+ * after it was built are seen.
9
+ */
10
+ export const makeQueueConfigHelper = (cfg: Config): QueueConfigHelper => {
11
+ const queueOf = (name: string): QueueDeclaration => {
12
+ const queue = cfg.queue?.queues?.find(declared => declared.name === name)
13
+
14
+ if (queue == null) {
15
+ throw new UnknownQueue(name)
16
+ }
17
+
18
+ return queue
19
+ }
20
+
21
+ const queueOfJob = (job: string): QueueDeclaration | undefined =>
22
+ cfg.queue?.queues?.find(declared => declared.jobs.includes(job))
23
+
24
+ const isListening = (name: string): boolean =>
25
+ cfg.queue?.listen?.includes(name) === true
26
+
27
+ /** A date option the broker can read — a `Date`, epoch milliseconds or a parsable string. */
28
+ const isInstant = (value: Date | number | string): boolean =>
29
+ Number.isFinite((value instanceof Date ? value : new Date(value)).getTime())
30
+
31
+ const assertSchedule = (schedule: ScheduleDeclaration): void => {
32
+ const { id } = schedule
33
+ if (typeof id !== 'string' || id.trim() === '') {
34
+ throw new ScheduleMisdeclared('(empty):id')
35
+ }
36
+
37
+ const queue = queueOf(schedule.queue)
38
+ if (!queue.jobs.includes(schedule.name)) {
39
+ throw new UnknownJobName(`${schedule.queue}:${schedule.name}`)
40
+ }
41
+
42
+ const every = schedule.every != null
43
+ const pattern = schedule.pattern != null
44
+ if (every === pattern) {
45
+ throw new ScheduleMisdeclared(`${id}:every-or-pattern`)
46
+ }
47
+ if (every && !(Number.isSafeInteger(schedule.every) && (schedule.every as number) > 0)) {
48
+ throw new ScheduleMisdeclared(`${id}:every`)
49
+ }
50
+ if (pattern && (typeof schedule.pattern !== 'string' || schedule.pattern.trim() === '')) {
51
+ throw new ScheduleMisdeclared(`${id}:pattern`)
52
+ }
53
+ // Both only mean something to a cron pattern: an interval has no wall clock to read in a time
54
+ // zone, and its first run is immediate already.
55
+ if (!pattern && schedule.tz != null) {
56
+ throw new ScheduleMisdeclared(`${id}:tz`)
57
+ }
58
+ if (!pattern && schedule.immediately === true) {
59
+ throw new ScheduleMisdeclared(`${id}:immediately`)
60
+ }
61
+ if (schedule.immediately === true && schedule.startDate != null) {
62
+ throw new ScheduleMisdeclared(`${id}:immediately-start-date`)
63
+ }
64
+ if (schedule.limit != null && !(Number.isSafeInteger(schedule.limit) && schedule.limit > 0)) {
65
+ throw new ScheduleMisdeclared(`${id}:limit`)
66
+ }
67
+ if (schedule.startDate != null && !isInstant(schedule.startDate)) {
68
+ throw new ScheduleMisdeclared(`${id}:start-date`)
69
+ }
70
+ if (schedule.endDate != null && !isInstant(schedule.endDate)) {
71
+ throw new ScheduleMisdeclared(`${id}:end-date`)
72
+ }
73
+
74
+ // The type already omits both; a declaration assembled at runtime still reaches this check. The
75
+ // broker names every run itself and places it on the schedule, so either would be overridden.
76
+ const opts = schedule.opts as JobOptions | undefined
77
+ if (opts?.id != null) {
78
+ throw new ScheduleMisdeclared(`${id}:opts-id`)
79
+ }
80
+ if (opts?.delay != null) {
81
+ throw new ScheduleMisdeclared(`${id}:opts-delay`)
82
+ }
83
+ }
84
+
85
+ const schedulesOf = (queue?: string): ScheduleDeclaration[] =>
86
+ (cfg.queue?.schedules ?? []).filter(schedule => queue == null || schedule.queue === queue)
87
+
88
+ const assertSchedules = (): void => {
89
+ const seen = new Set<string>()
90
+ for (const schedule of schedulesOf()) {
91
+ assertSchedule(schedule)
92
+ if (seen.has(schedule.id)) {
93
+ throw new ScheduleMisdeclared(`${schedule.id}:duplicate`)
94
+ }
95
+ seen.add(schedule.id)
96
+ }
97
+ }
98
+
99
+ return { queueOf, queueOfJob, isListening, assertSchedule, schedulesOf, assertSchedules }
100
+ }
101
+
102
+ export const queueConfigOf = memoHelper.oncePer(makeQueueConfigHelper)
@@ -0,0 +1,14 @@
1
+ import type { RouteDeclaration, RouteModel } from '@owlmeans/route'
2
+ import type { QueueRouteOptions } from '../types.js'
3
+
4
+ /** Reads a route declaration as one carried by the queue transport. */
5
+ export interface QueueRouteHelper {
6
+ /** Whether a declaration belongs to the queue transport. */
7
+ isQueueRoute: (route: RouteDeclaration | RouteModel) => boolean
8
+ /**
9
+ * Read and validate queue-owned route options.
10
+ *
11
+ * @throws {UnknownQueue} for a route of another protocol, or queue options that do not validate
12
+ */
13
+ queueRouteOptions: (route: RouteDeclaration | RouteModel) => Pick<QueueRouteOptions, 'queue' | 'reply'>
14
+ }
@@ -0,0 +1,33 @@
1
+ import type { RouteDeclaration, RouteModel } from '@owlmeans/route'
2
+ import { UnknownQueue } from './errors.js'
3
+ import { QUEUE_PROTOCOL } from './consts.js'
4
+ import type { QueueRouteOptions } from './types.js'
5
+ import type { QueueRouteHelper } from './queue-route/types.js'
6
+
7
+ export const createQueueRouteHelper = (): QueueRouteHelper => {
8
+ const isQueueRoute = (route: RouteDeclaration | RouteModel): boolean =>
9
+ ('route' in route ? route.route : route).protocol === QUEUE_PROTOCOL
10
+
11
+ const queueRouteOptions = (
12
+ route: RouteDeclaration | RouteModel,
13
+ ): Pick<QueueRouteOptions, 'queue' | 'reply'> => {
14
+ const declaration = 'route' in route ? route.route : route
15
+ if (declaration.protocol !== QUEUE_PROTOCOL
16
+ || declaration.protocolOptions == null
17
+ || typeof declaration.protocolOptions !== 'object') {
18
+ throw new UnknownQueue(`${declaration.alias}: not a queue protocol`)
19
+ }
20
+ const options = declaration.protocolOptions as Record<string, unknown>
21
+ if (typeof options.queue !== 'string' || options.queue.trim() === '') {
22
+ throw new UnknownQueue(`${declaration.alias}: route declares no queue`)
23
+ }
24
+ if (options.reply != null && typeof options.reply !== 'boolean') {
25
+ throw new UnknownQueue(`${declaration.alias}: invalid queue reply option`)
26
+ }
27
+ return { queue: options.queue, reply: options.reply as boolean | undefined }
28
+ }
29
+
30
+ return { isQueueRoute, queueRouteOptions }
31
+ }
32
+
33
+ export const queueRouteHelper = createQueueRouteHelper()
package/src/route.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { backend, type RouteDeclaration, type RouteModel, type RouteOptions } from '@owlmeans/route'
2
- import { UnknownQueue } from './errors.js'
3
2
  import { QUEUE_PROTOCOL } from './consts.js'
4
3
  import type { QueueRouteOptions } from './types.js'
4
+ import { queueRouteHelper } from './queue-route.js'
5
5
 
6
6
  /** Declare a backend route carried by a queue. */
7
7
  export const job = (
@@ -15,26 +15,9 @@ export const job = (
15
15
  return declaration
16
16
  }
17
17
 
18
- /** Whether a declaration belongs to the queue transport. */
19
- export const isQueueRoute = (route: RouteDeclaration | RouteModel): boolean =>
20
- ('route' in route ? route.route : route).protocol === QUEUE_PROTOCOL
18
+ /** @deprecated compat:factory-refactor — use `queueRouteHelper.isQueueRoute(…)` */
19
+ export const isQueueRoute = (route: RouteDeclaration | RouteModel): boolean => queueRouteHelper.isQueueRoute(route)
21
20
 
22
- /** Read and validate queue-owned route options. */
23
- export const queueRouteOptions = (
24
- route: RouteDeclaration | RouteModel,
25
- ): Pick<QueueRouteOptions, 'queue' | 'reply'> => {
26
- const declaration = 'route' in route ? route.route : route
27
- if (declaration.protocol !== QUEUE_PROTOCOL
28
- || declaration.protocolOptions == null
29
- || typeof declaration.protocolOptions !== 'object') {
30
- throw new UnknownQueue(`${declaration.alias}: not a queue protocol`)
31
- }
32
- const options = declaration.protocolOptions as Record<string, unknown>
33
- if (typeof options.queue !== 'string' || options.queue.trim() === '') {
34
- throw new UnknownQueue(`${declaration.alias}: route declares no queue`)
35
- }
36
- if (options.reply != null && typeof options.reply !== 'boolean') {
37
- throw new UnknownQueue(`${declaration.alias}: invalid queue reply option`)
38
- }
39
- return { queue: options.queue, reply: options.reply as boolean | undefined }
40
- }
21
+ /** @deprecated compat:factory-refactor — use `queueRouteHelper.queueRouteOptions(…)` */
22
+ export const queueRouteOptions = (route: RouteDeclaration | RouteModel): Pick<QueueRouteOptions, 'queue' | 'reply'> =>
23
+ queueRouteHelper.queueRouteOptions(route)
package/src/transport.ts CHANGED
@@ -3,7 +3,7 @@ import { type AbstractRequest, type AbstractResponse, type EntrypointTransport,
3
3
  import { ResilientError } from '@owlmeans/error'
4
4
  import type { Config, Context, JobEnvelope, JobReply, QueueAppend } from './types.js'
5
5
  import { DEFAULT_JOB_TIMEOUT, QUEUE_PROTOCOL } from './consts.js'
6
- import { queueRouteOptions } from './route.js'
6
+ import { queueRouteHelper } from './queue-route.js'
7
7
 
8
8
  /**
9
9
  * The producing half of the bridge: it turns a call on a QUEUE entrypoint into a job, and the
@@ -15,7 +15,7 @@ import { queueRouteOptions } from './route.js'
15
15
  * the request rather than from the entrypoint: the far side rebuilds a request, and anything only
16
16
  * HTTP understands would not survive the trip anyway.
17
17
  */
18
- export const makeQueueTransport = (alias: string = transportAlias(QUEUE_PROTOCOL)) => {
18
+ export const makeQueueTransport = (alias: string = transportAlias(QUEUE_PROTOCOL)): EntrypointTransport => {
19
19
  const location = `queue-transport:${alias}`
20
20
 
21
21
  const service = createService<EntrypointTransport>(alias, {
@@ -26,7 +26,7 @@ export const makeQueueTransport = (alias: string = transportAlias(QUEUE_PROTOCOL
26
26
  const entrypoint = context.entrypoint<CommonEntrypoint>(req.alias)
27
27
  const route = entrypoint.route.route
28
28
 
29
- const { queue, reply: waitsForReply } = queueRouteOptions(route)
29
+ const { queue, reply: waitsForReply } = queueRouteHelper.queueRouteOptions(route)
30
30
 
31
31
  const jobs = (context as unknown as QueueAppend).jobs<JobEnvelope, JobReply<T>>(queue)
32
32
 
package/src/types.ts CHANGED
@@ -321,3 +321,6 @@ export interface QueueRouteOptions extends Omit<RouteOptions, 'protocol' | 'prot
321
321
  queue: string
322
322
  reply?: boolean
323
323
  }
324
+
325
+ /** The context the queue protocol helper is bound to: an app context with the queue appended. */
326
+ export interface QueueContext extends Context<Config>, QueueAppend {}
package/src/worker.ts CHANGED
@@ -1,53 +1,7 @@
1
1
  import type { Middleware } from '@owlmeans/context'
2
- import { MiddlewareStage, MiddlewareType, AppType } from '@owlmeans/context'
3
- import type { CommonEntrypoint } from '@owlmeans/entrypoint'
4
- import type { Config, Context, JobContext, JobEnvelope, JobProcessor, QueueWorkerService } from './types.js'
5
- import { handleJob } from './bridge.js'
2
+ import { MiddlewareStage, MiddlewareType } from '@owlmeans/context'
3
+ import type { Config, Context, QueueWorkerService } from './types.js'
6
4
  import { DEFAULT_ALIAS } from './consts.js'
7
- import { isQueueRoute, queueRouteOptions } from './route.js'
8
-
9
- /**
10
- * Every queued entrypoint this process both SERVES and LISTENS to, grouped by queue.
11
- *
12
- * Both halves are required and they answer different questions. Serving is about code — the alias
13
- * was bound here, so a handler exists. Listening is about deployment — this process was
14
- * configured to consume that queue. A worker that bound queues by what it can serve would make
15
- * every deployment of the same binary a worker for everything it happens to import.
16
- */
17
- export const servedJobs = <C extends Config, T extends Context<C>>(
18
- context: T
19
- ): Map<string, CommonEntrypoint[]> => {
20
- const listen = context.cfg.queue?.listen ?? []
21
- const served = new Map<string, CommonEntrypoint[]>()
22
-
23
- context.entrypoints<CommonEntrypoint>().forEach(entrypoint => {
24
- const route = entrypoint.route.route
25
- if (route.type !== AppType.Backend || !isQueueRoute(route)) {
26
- return
27
- }
28
- if (route.service != null && route.service !== context.cfg.service) {
29
- return
30
- }
31
- const { queue } = queueRouteOptions(route)
32
- if (entrypoint.handle == null || !listen.includes(queue)) {
33
- return
34
- }
35
-
36
- served.set(queue, [...(served.get(queue) ?? []), entrypoint])
37
- })
38
-
39
- return served
40
- }
41
-
42
- /**
43
- * A processor that runs a queued entrypoint call. The driver dispatches by job name, and an
44
- * entrypoint job's name IS its alias — which is what lets one worker carry both entrypoint jobs
45
- * and the internal steps an application registers with `process()`.
46
- */
47
- export const entrypointProcessor = <C extends Config, T extends Context<C>>(
48
- context: T
49
- ): JobProcessor<JobEnvelope, unknown> =>
50
- async (job: JobContext<JobEnvelope>) => await handleJob(context, job)
51
5
 
52
6
  /**
53
7
  * Start the worker once the context is ready.
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
- import { assertFresh, requestOf } from '../src/bridge.js'
2
+ import { queueBridgeHelper } from '../src/bridge.js'
3
3
  import { EnvelopeExpired } from '../src/errors.js'
4
4
  import type { JobEnvelope } from '../src/types.js'
5
5
 
@@ -9,7 +9,7 @@ const envelope = (enqueuedAt?: string): JobEnvelope => ({
9
9
 
10
10
  describe('rebuilding the request', () => {
11
11
  test('the envelope becomes an ordinary request', () => {
12
- const request = requestOf(envelope(), '/story/:id/code')
12
+ const request = queueBridgeHelper.requestOf(envelope(), '/story/:id/code')
13
13
 
14
14
  expect(request.alias).toBe('story:code')
15
15
  expect(request.params).toEqual({ id: '7' })
@@ -22,7 +22,7 @@ describe('rebuilding the request', () => {
22
22
  * checking, so they have to arrive as empty containers rather than as undefined.
23
23
  */
24
24
  test('absent parts arrive as empty, never undefined', () => {
25
- const request = requestOf({ alias: 'story:code' }, '/x')
25
+ const request = queueBridgeHelper.requestOf({ alias: 'story:code' }, '/x')
26
26
 
27
27
  expect(request.params).toEqual({})
28
28
  expect(request.headers).toEqual({})
@@ -32,13 +32,13 @@ describe('rebuilding the request', () => {
32
32
 
33
33
  describe('envelope freshness', () => {
34
34
  test('a fresh envelope passes', () => {
35
- expect(() => assertFresh(envelope(new Date().toISOString()), 60)).not.toThrow()
35
+ expect(() => queueBridgeHelper.assertFresh(envelope(new Date().toISOString()), 60)).not.toThrow()
36
36
  })
37
37
 
38
38
  test('an envelope older than the window is refused', () => {
39
39
  const old = new Date(Date.now() - 120_000).toISOString()
40
40
 
41
- expect(() => assertFresh(envelope(old), 60)).toThrow(EnvelopeExpired)
41
+ expect(() => queueBridgeHelper.assertFresh(envelope(old), 60)).toThrow(EnvelopeExpired)
42
42
  })
43
43
 
44
44
  /**
@@ -49,16 +49,16 @@ describe('envelope freshness', () => {
49
49
  test('a long wait in the queue is not what expiry is for', () => {
50
50
  const enqueued = new Date(Date.now() - 3600_000).toISOString()
51
51
 
52
- expect(() => assertFresh(envelope(enqueued), 86_400)).not.toThrow()
52
+ expect(() => queueBridgeHelper.assertFresh(envelope(enqueued), 86_400)).not.toThrow()
53
53
  })
54
54
 
55
55
  test('no configured window means no check', () => {
56
56
  const ancient = new Date(0).toISOString()
57
57
 
58
- expect(() => assertFresh(envelope(ancient), undefined)).not.toThrow()
58
+ expect(() => queueBridgeHelper.assertFresh(envelope(ancient), undefined)).not.toThrow()
59
59
  })
60
60
 
61
61
  test('an envelope without a timestamp is not judged', () => {
62
- expect(() => assertFresh(envelope(undefined), 60)).not.toThrow()
62
+ expect(() => queueBridgeHelper.assertFresh(envelope(undefined), 60)).not.toThrow()
63
63
  })
64
64
  })
@@ -1,5 +1,6 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
- import { declareQueue, isListening, listenQueues, queueOf, queueOfJob } from '../src/config.js'
2
+ import { declareQueue, listenQueues } from '../src/config.js'
3
+ import { queueConfigOf } from '../src/queue-config.js'
3
4
  import { UnknownQueue } from '../src/errors.js'
4
5
  import type { Config } from '../src/types.js'
5
6
 
@@ -9,7 +10,7 @@ describe('declaring queues', () => {
9
10
  test('a declaration records the queue and the jobs it accepts', () => {
10
11
  const config = declareQueue(cfg(), 'work', ['story:code', 'story:gate'])
11
12
 
12
- expect(queueOf(config, 'work').jobs).toEqual(['story:code', 'story:gate'])
13
+ expect(queueConfigOf(config).queueOf('work').jobs).toEqual(['story:code', 'story:gate'])
13
14
  })
14
15
 
15
16
  /**
@@ -22,18 +23,18 @@ describe('declaring queues', () => {
22
23
  config = declareQueue(config, 'work', ['story:code', 'story:gate'])
23
24
 
24
25
  expect(config.queue?.queues).toHaveLength(1)
25
- expect(queueOf(config, 'work').jobs).toHaveLength(2)
26
+ expect(queueConfigOf(config).queueOf('work').jobs).toHaveLength(2)
26
27
  })
27
28
 
28
29
  test('asking for a queue nobody declared is an error, not undefined', () => {
29
- expect(() => queueOf(cfg(), 'missing')).toThrow(UnknownQueue)
30
+ expect(() => queueConfigOf(cfg()).queueOf('missing')).toThrow(UnknownQueue)
30
31
  })
31
32
 
32
33
  test('a job name resolves back to the queue that accepts it', () => {
33
34
  const config = declareQueue(declareQueue(cfg(), 'work', ['story:code']), 'ops', ['files:get'])
34
35
 
35
- expect(queueOfJob(config, 'files:get')?.name).toBe('ops')
36
- expect(queueOfJob(config, 'nothing:declared')).toBeUndefined()
36
+ expect(queueConfigOf(config).queueOfJob('files:get')?.name).toBe('ops')
37
+ expect(queueConfigOf(config).queueOfJob('nothing:declared')).toBeUndefined()
37
38
  })
38
39
  })
39
40
 
@@ -45,7 +46,7 @@ describe('listening', () => {
45
46
  test('declaring a queue does not make the process listen to it', () => {
46
47
  const config = declareQueue(cfg(), 'work', ['story:code'])
47
48
 
48
- expect(isListening(config, 'work')).toBe(false)
49
+ expect(queueConfigOf(config).isListening('work')).toBe(false)
49
50
  expect(config.queue?.listen ?? []).toHaveLength(0)
50
51
  })
51
52
 
@@ -54,13 +55,13 @@ describe('listening', () => {
54
55
  config = listenQueues(config, 'work', 'ops')
55
56
 
56
57
  expect(config.queue?.listen).toEqual(['work', 'ops'])
57
- expect(isListening(config, 'ops')).toBe(true)
58
+ expect(queueConfigOf(config).isListening('ops')).toBe(true)
58
59
  })
59
60
 
60
61
  test('listening and declaring do not overwrite each other', () => {
61
62
  const config = declareQueue(listenQueues(cfg(), 'work'), 'work', ['story:code'])
62
63
 
63
- expect(isListening(config, 'work')).toBe(true)
64
- expect(queueOf(config, 'work').jobs).toEqual(['story:code'])
64
+ expect(queueConfigOf(config).isListening('work')).toBe(true)
65
+ expect(queueConfigOf(config).queueOf('work').jobs).toEqual(['story:code'])
65
66
  })
66
67
  })
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test'
2
2
  import { contract, protocol, schema } from '@owlmeans/entrypoint'
3
3
  import { route } from '@owlmeans/route'
4
4
  import type { JSONSchemaType } from 'ajv'
5
- import { enqueueProtocol, job, waitForProtocol, type JobRecord } from '../src/index.js'
5
+ import { job, queueProtocolOf, type JobRecord } from '../src/index.js'
6
6
 
7
7
  interface Input { value: number }
8
8
  interface Output { doubled: number }
@@ -35,23 +35,23 @@ const context = (reply: unknown = { value: { doubled: 6 } }) => {
35
35
  describe('protocol-aware queue calls', () => {
36
36
  test('derives the broker address and keeps per-call options', async () => {
37
37
  const ctx = context()
38
- const queued = await enqueueProtocol(ctx as never, declaration, { body: { value: 3 } }, {
38
+ const queued = await queueProtocolOf(ctx as never).enqueue(declaration, { body: { value: 3 } }, {
39
39
  delay: 250, attempts: 3, id: 'projection-1',
40
40
  })
41
41
  expect(queued.queue).toBe('test')
42
42
  expect(ctx.created[0]?.name).toBe(declaration.alias)
43
43
  expect(ctx.created[0]?.opts).toEqual({ delay: 250, attempts: 3, id: 'projection-1' })
44
- expect(await waitForProtocol(ctx as never, declaration, queued as never)).toEqual({ doubled: 6 })
44
+ expect(await queueProtocolOf(ctx as never).waitFor(declaration, queued as never)).toEqual({ doubled: 6 })
45
45
  })
46
46
 
47
47
  test('rejects raw aliases and non-queue declarations at runtime', async () => {
48
- await expect(enqueueProtocol(context() as never, declaration.alias as never, {} as never)).rejects.toThrow()
48
+ await expect(queueProtocolOf(context() as never).enqueue(declaration.alias as never, {} as never)).rejects.toThrow()
49
49
  const http = protocol(route('test:http', '/http'), contract())
50
- await expect(enqueueProtocol(context() as never, http, {})).rejects.toThrow()
50
+ await expect(queueProtocolOf(context() as never).enqueue(http, {})).rejects.toThrow()
51
51
  })
52
52
 
53
53
  test('rejects a job from another queue or protocol', async () => {
54
- await expect(waitForProtocol(context() as never, declaration, {
54
+ await expect(queueProtocolOf(context() as never).waitFor(declaration, {
55
55
  id: 'x', queue: 'other', name: declaration.alias, data: {},
56
56
  } as never)).rejects.toThrow()
57
57
  })
@@ -1,7 +1,6 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
- import {
3
- assertSchedule, assertSchedules, declareQueue, declareSchedule, queueOf, schedulesOf
4
- } from '../src/config.js'
2
+ import { declareQueue, declareSchedule } from '../src/config.js'
3
+ import { queueConfigOf } from '../src/queue-config.js'
5
4
  import { ScheduleMisdeclared, UnknownJobName, UnknownQueue } from '../src/errors.js'
6
5
  import type { Config, ScheduleDeclaration } from '../src/types.js'
7
6
 
@@ -16,23 +15,23 @@ const nightly: ScheduleDeclaration = {
16
15
  }
17
16
 
18
17
  const misdeclared = (schedule: Partial<ScheduleDeclaration>): () => void =>
19
- () => assertSchedule(cfg(), { ...nightly, ...schedule } as ScheduleDeclaration)
18
+ () => queueConfigOf(cfg()).assertSchedule({ ...nightly, ...schedule } as ScheduleDeclaration)
20
19
 
21
20
  describe('declaring schedules', () => {
22
21
  test('a declaration is recorded and read back by its queue', () => {
23
22
  const config = declareSchedule(cfg(), nightly)
24
23
 
25
- expect(schedulesOf(config, 'maintenance')).toEqual([nightly])
26
- expect(schedulesOf(config, 'work')).toEqual([])
27
- expect(schedulesOf(config)).toHaveLength(1)
24
+ expect(queueConfigOf(config).schedulesOf('maintenance')).toEqual([nightly])
25
+ expect(queueConfigOf(config).schedulesOf('work')).toEqual([])
26
+ expect(queueConfigOf(config).schedulesOf()).toHaveLength(1)
28
27
  })
29
28
 
30
29
  test('re-declaring an id replaces rather than appends', () => {
31
30
  let config = declareSchedule(cfg(), nightly)
32
31
  config = declareSchedule(config, { ...nightly, pattern: '0 4 * * *' })
33
32
 
34
- expect(schedulesOf(config)).toHaveLength(1)
35
- expect(schedulesOf(config)[0].pattern).toBe('0 4 * * *')
33
+ expect(queueConfigOf(config).schedulesOf()).toHaveLength(1)
34
+ expect(queueConfigOf(config).schedulesOf()[0].pattern).toBe('0 4 * * *')
36
35
  })
37
36
 
38
37
  /**
@@ -44,14 +43,14 @@ describe('declaring schedules', () => {
44
43
 
45
44
  expect(() => declareSchedule(config, { ...nightly, queue: 'missing' })).toThrow(UnknownQueue)
46
45
  expect(() => declareSchedule(config, { ...nightly, name: 'story:code' })).toThrow(UnknownJobName)
47
- expect(schedulesOf(config)).toHaveLength(0)
46
+ expect(queueConfigOf(config).schedulesOf()).toHaveLength(0)
48
47
  })
49
48
 
50
49
  test('re-declaring the queue keeps its schedules', () => {
51
50
  const config = declareQueue(declareSchedule(cfg(), nightly), 'maintenance', ['reconcile'])
52
51
 
53
- expect(queueOf(config, 'maintenance').jobs).toEqual(['reconcile'])
54
- expect(schedulesOf(config)).toEqual([nightly])
52
+ expect(queueConfigOf(config).queueOf('maintenance').jobs).toEqual(['reconcile'])
53
+ expect(queueConfigOf(config).schedulesOf()).toEqual([nightly])
55
54
  })
56
55
  })
57
56
 
@@ -87,7 +86,7 @@ describe('asserting every schedule', () => {
87
86
  const config = declareSchedule(cfg(), nightly)
88
87
  config.queue?.schedules?.push({ ...nightly, pattern: '0 5 * * *' })
89
88
 
90
- expect(() => assertSchedules(config)).toThrow(/duplicate/)
89
+ expect(() => queueConfigOf(config).assertSchedules()).toThrow(/duplicate/)
91
90
  })
92
91
 
93
92
  /** The configuration is plain data; a schedule written into it directly is checked too. */
@@ -95,6 +94,6 @@ describe('asserting every schedule', () => {
95
94
  const config = declareSchedule(cfg(), nightly)
96
95
  config.queue?.schedules?.push({ id: 'orphan', queue: 'maintenance', name: 'gone', every: 1_000 })
97
96
 
98
- expect(() => assertSchedules(config)).toThrow(UnknownJobName)
97
+ expect(() => queueConfigOf(config).assertSchedules()).toThrow(UnknownJobName)
99
98
  })
100
99
  })
@@ -1,3 +0,0 @@
1
- import type { Config, Context, QueueAppend } from './types.js'
2
-
3
- export type QueueContext = Context<Config> & QueueAppend