@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.
- package/README.md +17 -16
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/queue/SKILL.md +26 -24
- package/agent-meta/skills/scheduled-jobs/SKILL.md +1 -1
- package/build/bridge/types.d.ts +25 -0
- package/build/bridge/types.d.ts.map +1 -0
- package/build/bridge/types.js +2 -0
- package/build/bridge/types.js.map +1 -0
- package/build/bridge.d.ts +5 -28
- package/build/bridge.d.ts.map +1 -1
- package/build/bridge.js +33 -104
- package/build/bridge.js.map +1 -1
- package/build/config.d.ts +6 -22
- package/build/config.d.ts.map +1 -1
- package/build/config.js +10 -92
- package/build/config.js.map +1 -1
- package/build/entrypoint-jobs/types.d.ts +29 -0
- package/build/entrypoint-jobs/types.d.ts.map +1 -0
- package/build/entrypoint-jobs/types.js +2 -0
- package/build/entrypoint-jobs/types.js.map +1 -0
- package/build/entrypoint-jobs.d.ts +5 -0
- package/build/entrypoint-jobs.d.ts.map +1 -0
- package/build/entrypoint-jobs.js +84 -0
- package/build/entrypoint-jobs.js.map +1 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/build/protocol/types.d.ts +12 -0
- package/build/protocol/types.d.ts.map +1 -0
- package/build/protocol/types.js +2 -0
- package/build/protocol/types.js.map +1 -0
- package/build/protocol.d.ts +6 -3
- package/build/protocol.d.ts.map +1 -1
- package/build/protocol.js +57 -50
- package/build/protocol.js.map +1 -1
- package/build/queue-config/types.d.ts +29 -0
- package/build/queue-config/types.d.ts.map +1 -0
- package/build/queue-config/types.js +2 -0
- package/build/queue-config/types.js.map +1 -0
- package/build/queue-config.d.ts +9 -0
- package/build/queue-config.d.ts.map +1 -0
- package/build/queue-config.js +83 -0
- package/build/queue-config.js.map +1 -0
- package/build/queue-route/types.d.ts +14 -0
- package/build/queue-route/types.d.ts.map +1 -0
- package/build/queue-route/types.js +2 -0
- package/build/queue-route/types.js.map +1 -0
- package/build/queue-route.d.ts +4 -0
- package/build/queue-route.d.ts.map +1 -0
- package/build/queue-route.js +24 -0
- package/build/queue-route.js.map +1 -0
- package/build/route.d.ts +2 -2
- package/build/route.d.ts.map +1 -1
- package/build/route.js +5 -20
- package/build/route.js.map +1 -1
- package/build/transport.d.ts.map +1 -1
- package/build/transport.js +2 -2
- package/build/transport.js.map +1 -1
- package/build/types.d.ts +3 -0
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +2 -1
- package/build/types.local.d.ts.map +1 -1
- package/build/worker.d.ts +0 -17
- package/build/worker.d.ts.map +1 -1
- package/build/worker.js +1 -36
- package/build/worker.js.map +1 -1
- package/package.json +8 -8
- package/src/bridge/types.ts +25 -0
- package/src/bridge.ts +38 -118
- package/src/config.ts +14 -105
- package/src/entrypoint-jobs/types.ts +29 -0
- package/src/entrypoint-jobs.ts +105 -0
- package/src/index.ts +8 -0
- package/src/protocol/types.ts +16 -0
- package/src/protocol.ts +74 -53
- package/src/queue-config/types.ts +29 -0
- package/src/queue-config.ts +102 -0
- package/src/queue-route/types.ts +14 -0
- package/src/queue-route.ts +33 -0
- package/src/route.ts +6 -23
- package/src/transport.ts +3 -3
- package/src/types.ts +3 -0
- package/src/worker.ts +2 -48
- package/tests/bridge.spec.ts +8 -8
- package/tests/config.spec.ts +11 -10
- package/tests/protocol.spec.ts +6 -6
- package/tests/schedule.spec.ts +13 -14
- 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
|
-
/**
|
|
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
|
-
/**
|
|
23
|
-
export const queueRouteOptions = (
|
|
24
|
-
route
|
|
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 {
|
|
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
|
|
3
|
-
import type {
|
|
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.
|
package/tests/bridge.spec.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import {
|
|
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
|
})
|
package/tests/config.spec.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import { declareQueue,
|
|
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(
|
|
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(
|
|
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(() =>
|
|
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(
|
|
36
|
-
expect(queueOfJob(
|
|
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(
|
|
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(
|
|
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(
|
|
64
|
-
expect(queueOf(
|
|
64
|
+
expect(queueConfigOf(config).isListening('work')).toBe(true)
|
|
65
|
+
expect(queueConfigOf(config).queueOf('work').jobs).toEqual(['story:code'])
|
|
65
66
|
})
|
|
66
67
|
})
|
package/tests/protocol.spec.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
})
|
package/tests/schedule.spec.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
-
() =>
|
|
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(
|
|
26
|
-
expect(schedulesOf(
|
|
27
|
-
expect(
|
|
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(
|
|
35
|
-
expect(
|
|
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(
|
|
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(
|
|
54
|
-
expect(
|
|
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(() =>
|
|
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(() =>
|
|
97
|
+
expect(() => queueConfigOf(config).assertSchedules()).toThrow(UnknownJobName)
|
|
99
98
|
})
|
|
100
99
|
})
|
package/src/types.local.ts
DELETED