@owlmeans/route 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 CHANGED
@@ -1,13 +1,12 @@
1
1
  # @owlmeans/route
2
2
 
3
- Route model factory and type definitions for OwlMeans entrypoints.
4
-
5
- ## Overview
6
-
7
- - `route()` creates a `RouteModel` used as the first argument to `protocol()` or `openProtocol()`
8
- - `frontend()` / `backend()` / `socket()` helpers set the route's `AppType` and parent
9
- - `RouteMethod` enum covers HTTP verbs; `RouteProtocols` covers `http`/`ws`
10
- - This package is a dependency of `@owlmeans/entrypoint` — you rarely use it directly unless defining entrypoint-level routes
3
+ The route declaration vocabulary for OwlMeans entrypoints. An app imports it in its shared contract
4
+ package, next to `@owlmeans/entrypoint`, to state for each entrypoint the URL segment, parent,
5
+ HTTP method, app type (backend or frontend) and transport (HTTP, WebSocket or queue). It is not a
6
+ router and serves nothing: server binding is `@owlmeans/server-route` / `@owlmeans/server-app`,
7
+ client binding is `@owlmeans/client-route` / `@owlmeans/client-entrypoint`, UI routing is
8
+ `@owlmeans/router`. Application code usually reads addresses through the entrypoint accessors
9
+ (`path()`, `mount()`, `address()`) rather than through this package's `./utils`.
11
10
 
12
11
  A `RouteDeclaration` is plain, immutable data: its `path` is the segment the route contributes under
13
12
  its parent, and it is never rewritten. Where the route actually answers — the full path, the mount
@@ -17,88 +16,256 @@ context that asks, so the same declaration serves a server and a browser alike.
17
16
  ## Installation
18
17
 
19
18
  ```bash
20
- bun add @owlmeans/route@^0.1.18-rc.8
19
+ bun add @owlmeans/route@^0.1.18-rc.17
21
20
  ```
22
21
 
23
- ## Usage
22
+ ## Concepts
23
+
24
+ - **Declaration** — the `RouteDeclaration` a `route()` call produces: `alias`, `path` (the segment
25
+ under `parent`), `type`, `method`, `protocol`, the service coordinates, and the queue fields.
26
+ - **Model** — `RouteModel` is `{ route: RouteDeclaration }` and nothing more. `protocol()` and
27
+ `openProtocol()` from `@owlmeans/entrypoint` take it as their first argument.
28
+ - **Marker** — `backend()`, `frontend()`, `socket()`, `job()` return `Partial<RouteOptions>` that set
29
+ the app type and protocol; `route()` accepts them as its options.
30
+ - **Parent** — a route nests under another entrypoint. Pass the parent protocol object
31
+ (`parent: storyProtocols.base`); the declaration stores only its alias.
32
+ - **Service** — `service` names the configured service (`cfg.services`) that answers the route.
33
+ Without it the route belongs to the asking context's own service or the default one of its type.
34
+ - **Protocol** — `RouteProtocols` picks the transport that carries a call: HTTP request, socket
35
+ frame or queued job. The call site never chooses.
24
36
 
25
- Define routes for protocols:
26
-
27
- ```typescript
28
- import { route, frontend, backend, socket, RouteMethod } from '@owlmeans/route'
29
- import { contract, openProtocol, protocol, typed } from '@owlmeans/entrypoint'
37
+ ## Usage
30
38
 
31
- // Backend REST route
32
- const createProtocol = protocol(
33
- route('story-create', '/stories', backend('api', RouteMethod.POST)),
34
- contract(typed<CreateStory>()),
35
- )
36
-
37
- // Frontend client route nested under parent
38
- const storyProtocol = openProtocol(
39
- route('story-view', '/stories/:id', frontend('app'))
40
- )
41
-
42
- // WebSocket route
43
- const wsProtocol = protocol(
44
- route('story-ws', '/stories/stream', socket('api')),
45
- contract(typed<StoryEvent>()),
46
- )
39
+ ### A backend protocol tree
40
+
41
+ Aliases stay private to the declaration module; the exported value is a tree of protocol objects.
42
+
43
+ ```ts
44
+ import { contract, protocol } from '@owlmeans/entrypoint'
45
+ import { backend, route, RouteMethod } from '@owlmeans/route'
46
+ import { DEFAULT_GUARD } from '@owlmeans/auth-common'
47
+ import { InvoiceSchema, CreateInvoiceBodySchema } from './schemas.js'
48
+
49
+ const aliases = {
50
+ base: 'my-app:api:invoice:base',
51
+ list: 'my-app:api:invoice:list',
52
+ create: 'my-app:api:invoice:create',
53
+ } as const
54
+
55
+ const base = protocol(route(aliases.base, '/invoice', backend()), contract(), {
56
+ guards: DEFAULT_GUARD,
57
+ })
58
+
59
+ export const invoiceProtocols = {
60
+ base,
61
+ list: protocol(
62
+ route(aliases.list, '/', backend({ parent: base, method: RouteMethod.GET })),
63
+ contract(InvoiceSchema),
64
+ ),
65
+ create: protocol(
66
+ route(aliases.create, '/', backend({ parent: base, method: RouteMethod.POST })),
67
+ contract(CreateInvoiceBodySchema, InvoiceSchema),
68
+ ),
69
+ } as const
47
70
  ```
48
71
 
49
- ## API
72
+ `invoiceProtocols.create` resolves to `/invoice` on the backend service, answering `POST`, and
73
+ inherits the guard declared on `base`.
50
74
 
51
- ### `route(alias, path, opts?): RouteModel`
75
+ ### Path params, screens and default children
52
76
 
53
- Creates a route model. `path` is the segment this route contributes under its parent. `opts` can be a `RouteOptions` object or the result of `frontend()`, `backend()`, or `socket()`.
77
+ Path params use the `:name` form and are typed through `contract.request`. `frontend()` marks a
78
+ screen; `default: true` marks the child screen shown at the parent's path.
79
+
80
+ ```ts
81
+ import { contract, openProtocol, protocol, typed } from '@owlmeans/entrypoint'
82
+ import { backend, frontend, route, RouteMethod } from '@owlmeans/route'
83
+ import type { ProjectParams, Project, RenameProject } from './types.js'
84
+ import { ProjectParamsSchema, RenameProjectSchema } from './schemas.js'
85
+
86
+ const aliases = {
87
+ base: 'my-app:api:project:base',
88
+ rename: 'my-app:api:project:rename',
89
+ web: 'my-app:web:project:base',
90
+ dashboard: 'my-app:web:project:dashboard',
91
+ overview: 'my-app:web:project:overview',
92
+ } as const
93
+
94
+ const base = openProtocol(route(aliases.base, '/project', backend()))
95
+ const web = openProtocol(route(aliases.web, '/project/:projectId', frontend()))
96
+
97
+ export const projectProtocols = {
98
+ base,
99
+ rename: protocol(
100
+ route(aliases.rename, '/:id/rename', backend({ parent: base, method: RouteMethod.PATCH })),
101
+ contract.request(
102
+ { params: typed<ProjectParams>(ProjectParamsSchema), body: typed<RenameProject>(RenameProjectSchema) },
103
+ typed<Project>(),
104
+ ),
105
+ ),
106
+ web: {
107
+ base: web,
108
+ dashboard: openProtocol(route(aliases.dashboard, '/dashboard', frontend({ parent: web }))),
109
+ overview: openProtocol(route(aliases.overview, '/', frontend({ parent: web, default: true }))),
110
+ },
111
+ } as const
112
+ ```
54
113
 
55
- ### `frontend(parent?, sticky?): Partial<RouteOptions>`
114
+ ### Sockets, queued jobs and other services
56
115
 
57
- Returns options marking the route as frontend (`AppType.Frontend`), optionally with a parent alias.
116
+ `socket()` and `job()` are `backend()` with a different protocol. A route pointed at another
117
+ service carries `service`; a queue route also names its `queue`, `timeout` and, optionally, `reply`.
58
118
 
59
- ### `backend(parent?, method?): Partial<RouteOptions>`
119
+ ```ts
120
+ import { contract, openProtocol, protocol, typed } from '@owlmeans/entrypoint'
121
+ import { job, route, RouteMethod, socket } from '@owlmeans/route'
122
+ import { GUARD_ED25519 } from '@owlmeans/auth-common'
123
+ import type { ProjectParams, StoryUpdate, Story } from './types.js'
124
+ import { ProjectParamsSchema } from './schemas.js'
125
+
126
+ const WORKER = 'my-app-worker'
127
+ const aliases = {
128
+ updates: 'my-app:api:update:base',
129
+ story: 'my-app:api:update:story',
130
+ jobs: 'my-app:worker:story:base',
131
+ develop: 'my-app:worker:story:develop',
132
+ cleanup: 'my-app:worker:story:cleanup',
133
+ } as const
134
+
135
+ const updates = openProtocol(route(aliases.updates, '/update'))
136
+ const jobs = openProtocol(route(aliases.jobs, '/story', { service: WORKER }), { guards: GUARD_ED25519 })
137
+
138
+ export const storyProtocols = {
139
+ updates,
140
+ // WebSocket subscription under /update
141
+ story: protocol(
142
+ route(aliases.story, '/story/:id', socket({ parent: updates })),
143
+ contract.request({ params: typed<ProjectParams>(ProjectParamsSchema) }, typed<StoryUpdate>()),
144
+ ),
145
+ jobs,
146
+ // Queued job: the caller waits up to timeout ms for the job's result
147
+ develop: protocol(
148
+ route(aliases.develop, '/:id/develop', job({
149
+ parent: jobs, method: RouteMethod.POST, service: WORKER, queue: 'story-work', timeout: 600_000,
150
+ })),
151
+ contract.request({ params: typed<ProjectParams>(ProjectParamsSchema) }, typed<Story>()),
152
+ ),
153
+ // reply: false resolves once the broker accepts the job; the outcome is Accepted
154
+ cleanup: protocol(
155
+ route(aliases.cleanup, '/:id/cleanup', job({
156
+ parent: jobs, method: RouteMethod.POST, service: WORKER, queue: 'story-ops', reply: false, timeout: 30_000,
157
+ })),
158
+ contract.request({ params: typed<ProjectParams>(ProjectParamsSchema) }, typed<unknown>()),
159
+ ),
160
+ } as const
161
+ ```
60
162
 
61
- Returns options marking the route as backend (`AppType.Backend`), optionally with a parent alias and `RouteMethod`.
163
+ Which queues a process consumes is configuration of that process (`listenQueues()`), never part of
164
+ the declaration.
62
165
 
63
- ### `socket(parent?): Partial<RouteOptions>`
166
+ ### Reading where a route answers
64
167
 
65
- Returns options for a WebSocket route with `RouteProtocols.SOCKET`.
168
+ Once protocols are registered on a context, the entrypoint accessors answer address questions. The
169
+ `./utils` helpers are the same functions over a bare declaration.
66
170
 
67
- ### `RouteMethod`
171
+ ```ts
172
+ import { AppType } from '@owlmeans/context'
173
+ import { service } from '@owlmeans/config'
174
+ import { resolveAddress, resolveMount } from '@owlmeans/route/utils'
175
+ import { invoiceProtocols } from 'my-app-common'
68
176
 
69
- ```typescript
70
- enum RouteMethod { GET, POST, PATCH, PUT, DELETE }
71
- ```
177
+ // Config side: the service the backend routes resolve against
178
+ const cfg = service({ service: 'my-app-api', type: AppType.Backend, host: 'localhost', port: 8080, base: 'api' }, baseCfg)
72
179
 
73
- ### `RouteProtocols`
180
+ // Runtime side, through the registered entrypoint
181
+ const entrypoint = context.entrypoint(invoiceProtocols.create)
182
+ entrypoint.path() // '/invoice'
183
+ entrypoint.mount() // '/api/invoice'
184
+ entrypoint.address() // { host: 'localhost', port: 8080, base: 'api', secure: true, protocol: 'http' }
185
+ entrypoint.isLocal() // true inside the my-app-api process
74
186
 
75
- ```typescript
76
- enum RouteProtocols { WEB = 'http', SOCKET = 'ws' }
187
+ // Same answers from a declaration
188
+ const declaration = invoiceProtocols.create.route.route
189
+ resolveMount(context, declaration)
190
+ resolveAddress(context, declaration)
77
191
  ```
78
192
 
79
- The protocol also selects the transport that carries a call to an entrypoint on this route.
80
-
81
- ### Types
193
+ ## API
82
194
 
83
- - `RouteDeclaration` — the plain, immutable data a `route()` call produces: `alias`, `path`,
84
- `parent`, `method`, `protocol`, `secure`, plus the service coordinates
85
- - `RouteModel` — `{ route: RouteDeclaration }`, the model an entrypoint carries
86
- - `RouteAddress` — `{ host, port?, base?, secure, protocol }`, where a route answers once its
87
- service has been picked
195
+ ### `@owlmeans/route`
196
+
197
+ | Symbol | Kind | Purpose |
198
+ |--------|------|---------|
199
+ | `route(alias, path, opts?)` | function | Build a `RouteModel`; `opts` is `RouteOptions` or a parent alias string |
200
+ | `createRoute(alias, path, opts?)` | function | Build the bare `RouteDeclaration` (parent reference collapsed to its alias) |
201
+ | `makeRouteModel(declaration)` | function | Wrap a declaration as `{ route }` |
202
+ | `backend(opts?, method?)` | function | Mark `AppType.Backend`; `opts` is options, a parent alias or `null`; `method` is a `RouteMethod` or more options |
203
+ | `frontend(opts?, default?)` | function | Mark `AppType.Frontend`; `default` is a boolean flag or more options |
204
+ | `socket(opts?, secondary?)` | function | `backend()` with `protocol: RouteProtocols.SOCKET` |
205
+ | `job(opts?, secondary?)` | function | `backend()` with `protocol: RouteProtocols.QUEUE` |
206
+ | `service(alias, opts?)` | function | Set `service` on `opts` (mutated and returned) |
207
+ | `rtype(type, opts?)` | function | Set an arbitrary `AppType`; the primitive behind `backend()` / `frontend()` |
208
+ | `normalizePath(path)` | function | Trim whitespace and one leading and trailing `/` |
209
+ | `RouteMethod` | enum | `GET = 'get'`, `POST = 'post'`, `PATCH = 'patch'`, `PUT = 'put'`, `DELETE = 'delete'` |
210
+ | `RouteProtocols` | enum | `WEB = 'http'`, `SOCKET = 'ws'`, `QUEUE = 'queue'` |
211
+ | `SEP` | const | Path separator `'/'` |
212
+ | `PARAM` | const | Path param prefix `':'` |
213
+ | `BasicRoute` | interface | `type`, `service?`, `host?`, `port?`, `base?`, `internalHost?`, `internalPort?` |
214
+ | `RouteDeclaration` | interface | `BasicRoute` + `alias`, `path`, `parent?`, `default?`, `method?`, `protocol?`, `secure?`, `queue?`, `reply?`, `timeout?` |
215
+ | `RouteOptions` | interface | `Partial<RouteDeclaration>` whose `parent` is a `RouteParent` |
216
+ | `RouteParent` | type | `string \| EntrypointReference` |
217
+ | `RouteModel` | interface | `{ route: RouteDeclaration }` |
218
+ | `RouteAddress` | interface | `{ host, port?, base?, secure, protocol }` |
219
+ | `CommonServiceRoute` | interface | A service entry: `BasicRoute` + `service`, `home?`, `default?` |
220
+ | `ResolvedServiceRoute` | interface | A service entry that names its `host` |
88
221
 
89
222
  ### `@owlmeans/route/utils`
90
223
 
91
- The pure functions the entrypoint accessors are built on. Each takes a context and a declaration:
92
-
93
- - `resolveService(ctx, route)` / `isLocalRoute(ctx, route)`
94
- - `resolvePath(ctx, route)` — the route's segment under every ancestor's
95
- - `resolveMount(ctx, route)` — that path under the service base
96
- - `resolveAddress(ctx, route)` — host, port, scheme and protocol
97
-
98
- ## Related Packages
99
-
100
- - [`@owlmeans/entrypoint`](../entrypoint) — `protocol()` and `openProtocol()` consume a `RouteModel`
101
- - [`@owlmeans/server-app`](../server-app) — re-exports `route` as `broute` (backend) alongside `route`
224
+ The first six take `(context, declaration)`; the other four are pure and take no context.
225
+
226
+ | Symbol | Kind | Purpose |
227
+ |--------|------|---------|
228
+ | `resolvePath(context, route)` | function | Every ancestor's segment, then this one |
229
+ | `resolveMount(context, route)` | function | Service `base` + the full path |
230
+ | `resolveService(context, route)` | function | The named service, else the default of the route's type, else the first of that type |
231
+ | `resolveAddress(context, route)` | function | The `RouteAddress`; a hop over the service's internal host is never TLS |
232
+ | `isLocalRoute(context, route)` | function | Does the route belong to the context's own service |
233
+ | `getParentRoute(context, route)` | function | The parent declaration, with parentship-cycle detection |
234
+ | `overrideParams(route, overrides?, filter?)` | function | Fill only blank fields of `route` from `overrides`; mutates, returns nothing |
235
+ | `prependBase(route, path)` | function | `path` prefixed with `route.base` when set |
236
+ | `isServiceRoute(obj?)` | type guard | Is `obj` a `CommonServiceRoute` with a known `AppType` |
237
+ | `isServiceRouteResolved(route)` | type guard | Does the service entry name a host |
238
+ | `CreateRouteSignature<R>` | interface | Call signature shared by `route()` and `createRoute()` |
239
+
240
+ ## Common pitfalls
241
+
242
+ - Pass the parent protocol object in route options, never an exported alias string. Keep aliases
243
+ private to the declaration module and export a `*Protocols` tree.
244
+ - The positional marker forms (`backend(parent, method)`, `frontend(parent, true)`) accept a parent
245
+ only as an alias string: a protocol object in first position is spread into the options and
246
+ overwrites the child's alias. Pass protocols as `{ parent }`.
247
+ - `path` is only this route's segment. Do not repeat the parent's path in it; `resolvePath()` joins
248
+ the chain.
249
+ - `job()` routes must name `service`, `queue` and `timeout`. `reply: false` returns the job identity
250
+ with an `Accepted` outcome, not the job's result.
251
+ - Do not pass a context to the pure `./utils` helpers: `overrideParams(ctx, decl)` writes route
252
+ fields into the context object instead of resolving anything.
253
+ - A mis-wired tree (no `cfg.services`, a service without a host, a parentship cycle) throws
254
+ `SyntaxError` when an address is resolved. Fix the config; do not catch it.
255
+ - `transportAlias` and `EntrypointTransport` live in `@owlmeans/entrypoint`, not here. A route only
256
+ names its protocol.
257
+
258
+ ## Related packages
259
+
260
+ - [`@owlmeans/entrypoint`](../entrypoint) — `protocol()` / `openProtocol()` consume a `RouteModel` and
261
+ expose `path()`, `mount()`, `address()`, `isLocal()`
262
+ - [`@owlmeans/context`](../context) — `AppType`, `EntrypointReference` and the registry the address helpers walk
263
+ - [`@owlmeans/config`](../config) — `service()` declares the service entries routes resolve against
264
+ - [`@owlmeans/server-route`](../server-route) — server-side route model; re-exported by
265
+ [`@owlmeans/server-app`](../server-app) as `broute`
266
+ - [`@owlmeans/client-route`](../client-route) — marks a route as client-side and extracts its params
267
+ - [`@owlmeans/queue`](../queue) — the QUEUE transport a `job()` route is carried by
268
+ - [`@owlmeans/socket`](../socket) — the SOCKET transport contracts
102
269
 
103
270
  <!-- owlmeans:agent-guidance:start -->
104
271
  ## Agent guidance
@@ -108,7 +275,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
108
275
  your project's skill store (`.agents/skills/`):
109
276
 
110
277
  ```sh
111
- npx @owlmeans/agent-skills@^0.1.18-rc.19
278
+ npx @owlmeans/agent-skills@^0.1.18-rc.21
112
279
  ```
113
280
 
114
281
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/route",
4
- "version": "0.1.18-rc.16",
5
- "generatedAt": "2026-09-12T13:51:53.539Z",
4
+ "version": "0.1.18-rc.18",
5
+ "generatedAt": "2026-09-15T12:21:31.935Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/route
9
9
 
10
10
  **Layer:** Core
11
- **Install:** `"@owlmeans/route": "^0.1.18-rc.16"` in `dependencies`
11
+ **Install:** `"@owlmeans/route": "^0.1.18-rc.18"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
@@ -18,7 +18,7 @@ user-invocable: false
18
18
  | `frontend(options?, default?)` | Mark a route as a React page (web only) |
19
19
  | `backend(options?, method?)` | Mark a route as a backend endpoint |
20
20
  | `socket(options?, secondary?)` | A backend route answering over `RouteProtocols.SOCKET` |
21
- | `job(options?, secondary?)` | A backend route carried by the queue (`RouteProtocols.QUEUE`); its declaration adds `queue`, `reply` and `timeout` |
21
+ | `job(options?, secondary?)` | A backend route carried by the queue (`RouteProtocols.QUEUE`); its declaration names `service`, `queue`, `reply` and `timeout` |
22
22
  | `service(alias, options?)` | Point a route at a named service |
23
23
  | `RouteMethod` | enum of GET, POST, PUT, PATCH, DELETE |
24
24
  | `RouteProtocols` | enum of WEB (`http`), SOCKET (`ws`), QUEUE (`queue`) |
@@ -51,7 +51,8 @@ not by this package; a route only names the protocol.
51
51
  `job()` is `backend()` with `RouteProtocols.QUEUE`. Three declaration fields matter only for that
52
52
  protocol: `queue` (which queue carries it), `reply` and `timeout`. `reply: false` resolves once the
53
53
  broker has taken the job — the value is the job's identity and the outcome is `Accepted`, not the
54
- job's result; the default is to wait for the result.
54
+ job's result; the default is to wait for the result. Use the parent protocol object in a route
55
+ option (`job({ parent: storyProtocols.base, service, queue, timeout })`), never an exported alias string.
55
56
 
56
57
  ## Subpath Exports
57
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/route",
3
- "version": "0.1.18-rc.16",
3
+ "version": "0.1.18-rc.18",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -28,7 +28,7 @@
28
28
  }
29
29
  },
30
30
  "dependencies": {
31
- "@owlmeans/context": "^0.1.18-rc.15"
31
+ "@owlmeans/context": "^0.1.18-rc.17"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@owlmeans/dep-config": "workspace:*",