@owlmeans/queue 0.1.18-rc.16 → 0.1.18-rc.18
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 +204 -84
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/queue/SKILL.md +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,129 +1,249 @@
|
|
|
1
1
|
# @owlmeans/queue
|
|
2
2
|
|
|
3
|
-
Job queues as resources,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
Job queues as resources, plus the route protocol that makes a queued call look like any other call.
|
|
4
|
+
Use it when work must outlive the request. Three cases qualify: the user would otherwise wait more
|
|
5
|
+
than about five seconds, accepted work must survive a process restart, or the work is retried
|
|
6
|
+
against a third party (payment capture, provider sync, bulk mail, report generation, model
|
|
7
|
+
pipelines). A request that finishes in under about two seconds and makes no external call should
|
|
8
|
+
not queue: do it inline. Check for a smaller fix first, such as an index, a batch write or a cached
|
|
9
|
+
aggregate. The framework has no scheduler, so recurring work is triggered by a platform cron that
|
|
10
|
+
calls an entrypoint or enqueues a job.
|
|
11
|
+
|
|
12
|
+
This package is contracts only and carries no broker code. A driver implements them
|
|
13
|
+
(`@owlmeans/redis-queue` does, over BullMQ). Depend on this package from a shared contract package,
|
|
14
|
+
and on the driver only where the application wires itself up.
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
bun add @owlmeans/queue@^0.1.18-rc.
|
|
19
|
+
bun add @owlmeans/queue@^0.1.18-rc.17
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
##
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
## Concepts
|
|
23
|
+
|
|
24
|
+
- **Queue declaration**: `declareQueue(cfg, name, jobs, opts?)` in the shared backend config. It
|
|
25
|
+
names a queue, the job names it accepts, and its worker and default job options. It is an
|
|
26
|
+
address, not a deployment.
|
|
27
|
+
- **Listen**: `listenQueues(cfg, ...names)` in one process's own config says which queues *that
|
|
28
|
+
process* consumes. A process that listens to nothing is a producer only.
|
|
29
|
+
- **Queued protocol**: an immutable entrypoint protocol whose route uses `job()` instead of
|
|
30
|
+
`backend()` (`RouteProtocols.QUEUE`). Callers use `ctx.entrypoint(protocol).call(...)` unchanged,
|
|
31
|
+
and the QUEUE transport carries the call through the broker.
|
|
32
|
+
- **Job record**: `ctx.jobs(queue)` returns a `QueueResource`, the ordinary `Resource<JobRecord>`
|
|
33
|
+
contract plus `wait`, `flow`, `counts` and `close`. `create` enqueues, `list` inspects and `take`
|
|
34
|
+
cancels.
|
|
35
|
+
- **Processor**: a function registered with `QueueWorkerService.process(queue, name, fn)` that gets
|
|
36
|
+
a `JobContext`. It must be safe to run twice and must `touch()` in long loops.
|
|
37
|
+
- **Single-flight id**: a `JobOptions.id` derived from what the job is about (`develop:<storyId>`).
|
|
38
|
+
A duplicate enqueue then returns the existing job instead of a second one.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Declare queues in the shared backend config
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { declareQueue } from '@owlmeans/queue'
|
|
46
|
+
|
|
47
|
+
// Split lanes by how long work takes, not by topic: a one-second read queued behind
|
|
48
|
+
// a thirty-minute job is a UI that looks broken.
|
|
49
|
+
declareQueue(cfg, APP_OPS, [appProtocols.files.get.alias, appProtocols.report.build.alias], {
|
|
50
|
+
worker: { concurrency: 16, lockDuration: 30_000 }
|
|
51
|
+
})
|
|
29
52
|
|
|
30
|
-
declareQueue(cfg,
|
|
31
|
-
worker: { concurrency:
|
|
53
|
+
declareQueue(cfg, APP_BILLING, [appProtocols.invoice.capture.alias], {
|
|
54
|
+
worker: { concurrency: 4, lockDuration: 30_000 },
|
|
55
|
+
defaults: { attempts: 3, backoff: { type: 'exponential', delay: 250 } }
|
|
32
56
|
})
|
|
33
57
|
```
|
|
34
58
|
|
|
35
|
-
|
|
59
|
+
The worker process names what it consumes, in its own config:
|
|
36
60
|
|
|
37
|
-
```
|
|
38
|
-
|
|
61
|
+
```ts
|
|
62
|
+
import { listenQueues } from '@owlmeans/queue'
|
|
63
|
+
|
|
64
|
+
listenQueues(cfg, APP_OPS, APP_BILLING) // the api process omits this and only produces
|
|
39
65
|
```
|
|
40
66
|
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
Both processes build their context through the same backend factory, which wires the driver once:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { appendRedisQueue } from '@owlmeans/redis-queue'
|
|
43
71
|
|
|
44
|
-
|
|
72
|
+
appendRedis<C, T>(context)
|
|
73
|
+
appendRedisQueue(context, { hooks: billingQueueHooks(context) })
|
|
74
|
+
```
|
|
45
75
|
|
|
46
|
-
|
|
76
|
+
### Declare a queued entrypoint
|
|
77
|
+
|
|
78
|
+
```ts
|
|
47
79
|
import { contract, protocol, typed } from '@owlmeans/entrypoint'
|
|
48
|
-
import { job } from '@owlmeans/route'
|
|
80
|
+
import { backend, job, route } from '@owlmeans/route'
|
|
81
|
+
|
|
82
|
+
const aliases = { base: 'app:report', build: 'app:report:build' } as const
|
|
83
|
+
const reportBase = protocol(route(aliases.base, '/reports', backend({ service: APP_WORKER })), contract())
|
|
84
|
+
|
|
85
|
+
export const appProtocols = {
|
|
86
|
+
report: {
|
|
87
|
+
base: reportBase,
|
|
88
|
+
build: protocol(
|
|
89
|
+
route(aliases.build, '/:id/build',
|
|
90
|
+
job({ parent: reportBase, service: APP_WORKER, queue: APP_OPS, timeout: 30_000 })),
|
|
91
|
+
contract.request({ params: typed<ReportParams>(ReportParamsSchema) }, typed<ReportResult>()),
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
} as const
|
|
95
|
+
```
|
|
49
96
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
97
|
+
The worker process serves it like any backend entrypoint; the HTTP server skips QUEUE routes. A
|
|
98
|
+
producer calls it with no knowledge of the broker:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const result = await context.entrypoint(appProtocols.report.build).call({ params: { id } })
|
|
55
102
|
```
|
|
56
103
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
104
|
+
Add `reply: false` to the `job()` options to resolve `Accepted` with `{ id, queue }` as soon as the
|
|
105
|
+
broker has taken the job.
|
|
106
|
+
|
|
107
|
+
### Enqueue with broker options and wait
|
|
60
108
|
|
|
61
|
-
|
|
109
|
+
When a producer needs a job id, a delay or retries, pass the same protocol object to the typed
|
|
110
|
+
helpers. Never pass its alias.
|
|
62
111
|
|
|
63
|
-
```
|
|
64
|
-
|
|
112
|
+
```ts
|
|
113
|
+
import { enqueueProtocol, waitForProtocol } from '@owlmeans/queue'
|
|
65
114
|
|
|
66
|
-
const queued = await
|
|
67
|
-
|
|
115
|
+
const queued = await enqueueProtocol(context, appProtocols.invoice.capture, {
|
|
116
|
+
body: { invoiceId },
|
|
117
|
+
}, {
|
|
118
|
+
id: `capture:${invoiceId}:${attemptSequence}`, // derived from what the job is about
|
|
119
|
+
delay: 250, attempts: 3, backoff: { type: 'exponential', delay: 250 },
|
|
120
|
+
})
|
|
68
121
|
|
|
69
|
-
const
|
|
70
|
-
await jobs.take(queued.id) // cancel and return
|
|
122
|
+
const receipt = await waitForProtocol(context, appProtocols.invoice.capture, queued, { timeout: 120_000 })
|
|
71
123
|
```
|
|
72
124
|
|
|
73
|
-
`
|
|
74
|
-
|
|
75
|
-
contract cannot express is added: `wait`, `flow` and `counts`.
|
|
125
|
+
`queueJobOf(req)` gives a protocol handler `{ id, name, queue, attempt, touch }` when it must compare
|
|
126
|
+
the broker job with a claim it persisted.
|
|
76
127
|
|
|
77
|
-
|
|
78
|
-
enqueue a no-op, which is what makes an admission step safe to retry.
|
|
128
|
+
### Processors, hooks and job graphs
|
|
79
129
|
|
|
80
|
-
|
|
130
|
+
```ts
|
|
131
|
+
import { DEFAULT_ALIAS } from '@owlmeans/queue'
|
|
132
|
+
import type { QueueWorkerService } from '@owlmeans/queue'
|
|
81
133
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
await
|
|
134
|
+
const worker = context.service<QueueWorkerService>(DEFAULT_ALIAS)
|
|
135
|
+
|
|
136
|
+
worker.process(APP_OPS, 'app:import:rows', async job => {
|
|
137
|
+
for (const chunk of chunks(job.data.fileId)) {
|
|
138
|
+
await job.touch() // renew the lock on every iteration
|
|
139
|
+
await importChunk(chunk) // idempotent: skips rows already imported
|
|
87
140
|
}
|
|
88
141
|
return { ok: true }
|
|
89
142
|
})
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
`touch()` is not optional in a long loop. The broker judges liveness by the lock, so going quiet
|
|
93
|
-
for longer than `lockDuration` is indistinguishable from a dead worker and the job is re-run
|
|
94
|
-
elsewhere while this one is still going.
|
|
95
143
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
re-spends the tokens that just failed.
|
|
102
|
-
|
|
103
|
-
## Job graphs
|
|
144
|
+
worker.hooks({
|
|
145
|
+
onJobDead: async (record, reason) => { // retries exhausted: compensate
|
|
146
|
+
await imports.patch({ id: record.data.importId, status: 'failed', error: reason })
|
|
147
|
+
}
|
|
148
|
+
})
|
|
104
149
|
|
|
105
|
-
|
|
106
|
-
await jobs.flow({
|
|
107
|
-
name: '
|
|
108
|
-
{ name: '
|
|
150
|
+
// children complete before the parent; siblings run in parallel
|
|
151
|
+
await context.jobs(APP_OPS).flow({
|
|
152
|
+
name: 'app:import:finalize', data: ref, children: [
|
|
153
|
+
{ name: 'app:import:validate', data: ref, children: [{ name: 'app:import:rows', data: ref }] }
|
|
109
154
|
]
|
|
110
155
|
})
|
|
111
156
|
```
|
|
112
157
|
|
|
113
|
-
|
|
158
|
+
### Jobs are records
|
|
114
159
|
|
|
115
|
-
|
|
160
|
+
```ts
|
|
161
|
+
import { JobState } from '@owlmeans/queue'
|
|
116
162
|
|
|
117
|
-
|
|
118
|
-
its own class, so `instanceof` keeps working across the broker. `QueueTimeout` means the WAIT
|
|
119
|
-
ended, not the job — read the job back to learn what became of it.
|
|
163
|
+
const jobs = context.jobs<ImportInput, ImportResult>(APP_OPS)
|
|
120
164
|
|
|
121
|
-
|
|
165
|
+
const failed = await jobs.list({ state: JobState.Failed }, { size: 20 })
|
|
166
|
+
const counts = await jobs.counts()
|
|
167
|
+
await jobs.take(jobId) // cancel and return
|
|
168
|
+
```
|
|
122
169
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
-
|
|
126
|
-
|
|
170
|
+
## Processor rules
|
|
171
|
+
|
|
172
|
+
- **`touch()` in every long loop.** The broker judges liveness by the lock. Silence longer than
|
|
173
|
+
`lockDuration` looks like a dead worker, so the job is re-run elsewhere while this one is still
|
|
174
|
+
going.
|
|
175
|
+
- **Children report, parents decide.** A child returns `{ ok: false, error }` for a domain failure.
|
|
176
|
+
Throwing means "infrastructure broke, retry me". Parents read `children()` / `failedChildren()`.
|
|
177
|
+
- **Retries default to one attempt** (`DEFAULT_ATTEMPTS`), because a blind retry of model work
|
|
178
|
+
re-spends the tokens that just failed. Raise `attempts` per queue for cheap idempotent work.
|
|
179
|
+
- **`job.signal` is not an interrupt.** An orderly stop closes workers first and waits for jobs in
|
|
180
|
+
flight to return.
|
|
181
|
+
|
|
182
|
+
## Guards and errors
|
|
183
|
+
|
|
184
|
+
`handleJob` rebuilds the request from the envelope and runs the entrypoint's guards the way the
|
|
185
|
+
HTTP boundary does, attaching the entity. A guarded queued entrypoint therefore needs the
|
|
186
|
+
producer's credentials in the envelope `headers`. `cfg.queue.envelopeTtl` (seconds) bounds how long
|
|
187
|
+
after `enqueuedAt` an envelope is still accepted; past it, the job answers `EnvelopeExpired`.
|
|
188
|
+
|
|
189
|
+
A refusal the caller must recognise is carried in the reply and rebuilt on the producing side as its
|
|
190
|
+
own class, so `instanceof` keeps working across the broker. `QueueTimeout` means the WAIT ended,
|
|
191
|
+
not the job, so read the job back to learn what became of it.
|
|
192
|
+
|
|
193
|
+
## API
|
|
194
|
+
|
|
195
|
+
| Symbol | Kind | Purpose |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `declareQueue(cfg, name, jobs, opts?)` | function | Declare a queue and the job names it accepts; re-declaring replaces |
|
|
198
|
+
| `listenQueues(cfg, ...names)` | function | Name the queues this process consumes |
|
|
199
|
+
| `queueOf(cfg, name)`, `queueOfJob(cfg, job)`, `isListening(cfg, name)` | function | Read declarations back; `queueOf` throws `UnknownQueue` |
|
|
200
|
+
| `enqueueProtocol(ctx, protocol, request, options?)` | function | Enqueue a QUEUE protocol with `JobOptions`, typed by the protocol |
|
|
201
|
+
| `waitForProtocol(ctx, protocol, job, { timeout }?)` | function | Wait for that job and unwrap the typed reply |
|
|
202
|
+
| `queueJobOf(request)` | function | `QueueJobMeta` of the broker job behind a handler's request, or `null` |
|
|
203
|
+
| `appendQueueTransport(ctx, alias?)`, `makeQueueTransport(alias?)` | function | Register the QUEUE transport so `call()` routes through the broker |
|
|
204
|
+
| `queueWorkerMiddleware(alias?)` | function | Start the worker at the Ready stage, only in a process that listens |
|
|
205
|
+
| `handleJob(ctx, job)`, `entrypointProcessor(ctx)`, `servedJobs(ctx)` | function | The bridge a driver dispatches entrypoint jobs through |
|
|
206
|
+
| `requestOf(envelope)`, `assertFresh(envelope, ttl?)` | function | Rebuild a request from an envelope; enforce `envelopeTtl` |
|
|
207
|
+
| `QueueConfig`, `QueueDeclaration`, `QueueWorkerOptions`, `JobOptions` | type | Configuration shapes |
|
|
208
|
+
| `QueueResource<D, R>` | type | A queue as a resource, plus `queue`, `wait`, `flow`, `counts`, `close` |
|
|
209
|
+
| `JobRecord<D, R>`, `JobEvent<R>`, `FlowSpec<D>` | type | A job as a record, a lifecycle event, a graph node |
|
|
210
|
+
| `QueueWorkerService` | type | `process`, `start`, `stop`, `listening`, `hooks` |
|
|
211
|
+
| `JobContext<D>`, `JobProcessor<D, R>` | type | What a processor receives; the processor signature |
|
|
212
|
+
| `QueueHooks` | type | `wrapHandler`, `onJobResult`, `onJobStalled`, `onJobDead` |
|
|
213
|
+
| `QueueJobMeta`, `JobEnvelope`, `JobReply<T>` | type | Broker identity for a handler, the call envelope, the reply shape |
|
|
214
|
+
| `QueueAppend`, `QueueDriver` | type | The `ctx.jobs(queue?)` mixin a driver installs; what a driver supplies |
|
|
215
|
+
| `Config`, `Context`, `QueueTransportRequest`, `QueueTransportResponse` | type | Context, config and transport types |
|
|
216
|
+
| `JobState`, `JobEventType` | enum | Job lifecycle states and event types |
|
|
217
|
+
| `isSettled(state?)` | function | Whether a state is final |
|
|
218
|
+
| `DEFAULT_ALIAS`, `DEFAULT_JOB_TIMEOUT`, `DEFAULT_ATTEMPTS` | const | `'queue'`, `60_000` ms, `1` |
|
|
219
|
+
| `QueueError`, `QueueTimeout`, `UnknownJob`, `UnknownJobName`, `UnknownQueue`, `QueueNotListening`, `JobNotServed`, `EnvelopeExpired` | class | Error family |
|
|
220
|
+
|
|
221
|
+
## Common pitfalls
|
|
222
|
+
|
|
223
|
+
- **Queueing work the request could finish.** It adds a second process to deploy and a place to fail.
|
|
224
|
+
- **Passing alias strings to `enqueueProtocol`/`waitForProtocol`.** They take the protocol object and
|
|
225
|
+
reject strings and non-QUEUE declarations.
|
|
226
|
+
- **Enqueueing a job name the queue did not declare.** It throws `UnknownJobName` rather than
|
|
227
|
+
parking a job nothing can process.
|
|
228
|
+
- **Registering a processor in a process that does not `listen`.** It throws `QueueNotListening`.
|
|
229
|
+
- **Putting `listen` in the shared declaration.** Every deployment of that binary would then
|
|
230
|
+
consume everything.
|
|
231
|
+
- **Random job ids on an admission step.** Derive the id from the subject, or a retried request
|
|
232
|
+
enqueues twice.
|
|
233
|
+
- **Long loops without `touch()`**, or processors that are not safe to run twice.
|
|
234
|
+
- **Throwing from a child for a domain failure.** Return `{ ok: false, error }` instead.
|
|
235
|
+
- **Equal child ids across queues in one flow.** `children()` is keyed by bare id and collapses them.
|
|
236
|
+
- **A self-re-enqueueing job as a cron.** Use a platform `CronJob`. `delay` defers a single job once.
|
|
237
|
+
- **Calling `ctx.jobs()` with no name once a second queue exists.** It throws `UnknownQueue`.
|
|
238
|
+
|
|
239
|
+
## Related packages
|
|
240
|
+
|
|
241
|
+
- [`@owlmeans/redis-queue`](../redis-queue): the BullMQ driver, and where queue integration tests live
|
|
242
|
+
- [`@owlmeans/route`](../route): `RouteProtocols.QUEUE` and the `job()` builder
|
|
243
|
+
- [`@owlmeans/entrypoint`](../entrypoint): protocol declarations and the transport seam
|
|
244
|
+
- [`@owlmeans/resource`](../resource): the `Resource` contract jobs are read through
|
|
245
|
+
- [`@owlmeans/server-job`](../server-job) / [`@owlmeans/client-job`](../client-job): listing,
|
|
246
|
+
cancelling and following jobs from an application's UI
|
|
127
247
|
|
|
128
248
|
<!-- owlmeans:agent-guidance:start -->
|
|
129
249
|
## Agent guidance
|
|
@@ -133,7 +253,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
133
253
|
your project's skill store (`.agents/skills/`):
|
|
134
254
|
|
|
135
255
|
```sh
|
|
136
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
256
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.21
|
|
137
257
|
```
|
|
138
258
|
|
|
139
259
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/queue",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.18",
|
|
5
|
+
"generatedAt": "2026-09-15T12:21:31.953Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/queue
|
|
9
9
|
|
|
10
10
|
**Layer:** Infra
|
|
11
|
-
**Install:** `"@owlmeans/queue": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/queue": "^0.1.18-rc.18"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
Contracts only. It carries no broker code — a driver package (`@owlmeans/redis-queue`) implements
|
|
14
14
|
them. Depend on this one from a shared contract package; depend on the driver only where the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/queue",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
35
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
36
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
37
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
38
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
39
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
40
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
34
|
+
"@owlmeans/auth": "^0.1.18-rc.18",
|
|
35
|
+
"@owlmeans/auth-common": "^0.1.18-rc.22",
|
|
36
|
+
"@owlmeans/context": "^0.1.18-rc.17",
|
|
37
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.20",
|
|
38
|
+
"@owlmeans/error": "^0.1.18-rc.17",
|
|
39
|
+
"@owlmeans/resource": "^0.1.18-rc.18",
|
|
40
|
+
"@owlmeans/route": "^0.1.18-rc.18"
|
|
41
41
|
}
|
|
42
42
|
}
|