@owlmeans/client-entrypoint 0.1.18-rc.21 → 0.1.18-rc.22

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,111 +1,200 @@
1
1
  # @owlmeans/client-entrypoint
2
2
 
3
- Client-side entrypoint system: binds shared protocol declarations into API-calling client views.
4
-
5
- ## Overview
6
-
7
- - `bind(protocol, opts?)` — binds one shared protocol declaration
8
- - `bindAll(protocolTree)` — binds every protocol in a shared declaration tree
9
- - `bindScreen(protocol, handler, opts?)` — binds a screen renderer to a shared frontend protocol
10
- - `ClientProtocolEntrypoint<Protocol>` exposes three explicit verbs: `call()` for the value,
11
- `invoke()` for the value plus its outcome, and `url()` for the address
12
- - `stab` — no-op handler for entrypoints that only need a URL (no logic)
13
- - `provideRequest(alias, path)` — creates an `AbstractRequest` for programmatic entrypoint calls
14
- - `pickPerSchema(schema, obj)` — extracts fields from an object matching an AJV schema
3
+ Client-side binding of shared entrypoint protocols. It turns an immutable `@owlmeans/entrypoint`
4
+ declaration into a context entrypoint with three verbs: `call()`, `invoke()` and `url()`. Every
5
+ browser and native OwlMeans app uses it, usually through the `bind` / `bindAll` / `bindScreen`
6
+ re-exports of `@owlmeans/web-client` or `@owlmeans/web-panel`. Protocols are declared with
7
+ [`@owlmeans/entrypoint`](../entrypoint), never here. Server handlers are bound with
8
+ [`@owlmeans/server-entrypoint`](../server-entrypoint).
15
9
 
16
10
  ## Installation
17
11
 
18
12
  ```bash
19
- bun add @owlmeans/client-entrypoint@^0.1.18-rc.12
13
+ bun add @owlmeans/client-entrypoint@^0.1.18-rc.21
20
14
  ```
21
15
 
16
+ ## Concepts
17
+
18
+ - **Protocol** — the shared, immutable declaration of one route, its typed request sections, its
19
+ response, guards and gate. Binding never mutates it, and `context.entrypoint(protocol)` derives
20
+ the request and reply types from it.
21
+ - **Binding** — `bind`, `bindAll` and `bindScreen` materialize protocols into
22
+ `ClientProtocolEntrypoint<Protocol>` values, which the app hands to `context.registerEntrypoints`.
23
+ - **Screen** — a binding that carries a renderer. It is addressed by `url()`; `call()` and
24
+ `invoke()` throw and point the caller at `url()`.
25
+ - **Verbs** — `call()` resolves to the value and throws the reply's error; `invoke()` resolves to
26
+ `{ value, outcome }`; `url()` builds the address with `:params` filled in and the query appended.
27
+ - **Transport selection** — the route's protocol decides the carrier. When a service is registered
28
+ under that protocol's transport alias (a socket or queue transport), it takes the call. Otherwise
29
+ the API client named by `cfg.webService` does — a string, or a map keyed by service name with a
30
+ default key. Callers never branch on it.
31
+ - **Call options** — a request holds only the contract's sections (`params`, `query`, `body`,
32
+ `headers`) plus `CallOptions`: `auth`, `host`, `base`, `unsecure`, `timeout` and `signal`.
33
+
22
34
  ## Usage
23
35
 
24
- Bind shared protocols for browser API calls and screens:
36
+ ### 1. Bind a protocol tree and screens
25
37
 
26
- ```typescript
27
- import { bindAll, bindScreen, stab } from '@owlmeans/client-entrypoint'
28
- import { appEntrypoints as protocols } from 'my-app-common'
38
+ ```ts
29
39
  import { handler } from '@owlmeans/client'
40
+ import { bind, bindAll, bindScreen, stab } from '@owlmeans/client-entrypoint'
41
+ import { apiProtocols, webProtocols } from 'my-app-common'
30
42
  import { ProjectListScreen } from './screens/project-list.js'
31
43
 
32
- const appEntrypoints = [
33
- ...bindAll(protocols.api),
34
- bindScreen(protocols.web.projectList, handler(ProjectListScreen)),
35
- bindScreen(protocols.web.project, stab),
44
+ export const clientBindings = [
45
+ // Every API declaration the browser may call, route parents included.
46
+ ...bindAll(apiProtocols),
47
+ // A backend route outside that tree.
48
+ bind(apiProtocols.health),
49
+ bindScreen(webProtocols.projectList, handler(ProjectListScreen)),
50
+ // A frontend route only ever addressed by URL, with no component of its own here.
51
+ bindScreen(webProtocols.projectExport, stab),
36
52
  ]
37
- ```
38
-
39
- Call an entrypoint from a service:
40
53
 
41
- ```typescript
42
- const agentEntrypoint = ctx.entrypoint(agent.project.create)
43
- const result = await agentEntrypoint.call({
44
- body: { prompt: payload.prompt, entity: req.auth?.entitySlug }
45
- })
54
+ context.registerEntrypoints(clientBindings)
46
55
  ```
47
56
 
48
- Take the outcome when it decides what happens next, and build a link with `url()`:
57
+ ### 2. Per-binding options
49
58
 
50
- ```typescript
51
- const { value, outcome } = await agentEntrypoint.invoke({ body: payload })
59
+ `bind` and `bindScreen` take `ClientEntrypointOptions`. Use `bind` instead of `bindAll` when one
60
+ protocol needs something the rest do not.
52
61
 
53
- const href = await ctx.entrypoint(protocols.web.projectList)
54
- .url({ params: { id: value.id } }, { absolute: true })
55
- ```
56
-
57
- ## API
62
+ ```ts
63
+ import { authProtocols } from '@owlmeans/auth-common'
64
+ import { bind, bindScreen, stab } from '@owlmeans/client-entrypoint'
65
+ import { apiProtocols, MY_APP_WEB } from 'my-app-common'
58
66
 
59
- ### `bind<Protocol>(protocol, opts?): ClientProtocolEntrypoint<Protocol>`
67
+ export const extraBindings = [
68
+ // Validate the request against the protocol's schemas before it leaves the browser.
69
+ bind(apiProtocols.project.create, { validateOnCall: true }),
70
+ // Pin a frontend route to an explicitly selected service.
71
+ bindScreen(authProtocols.flowEnter, stab, { routeOptions: { overrides: { service: MY_APP_WEB } } }),
72
+ ]
73
+ ```
60
74
 
61
- Materializes one immutable protocol declaration for a browser context. The declaration is never mutated.
75
+ ### 3. Call, invoke, url
62
76
 
63
- ### `bindAll(protocolTree): ClientProtocolEntrypoint[]`
77
+ ```ts
78
+ import { EntrypointOutcome } from '@owlmeans/entrypoint'
64
79
 
65
- Materializes every protocol in a shared declaration tree, preserving each protocol reference for typed context lookup.
80
+ const create = ctx.entrypoint(apiProtocols.project.create)
66
81
 
67
- ### `bindScreen<Protocol>(protocol, handler, opts?): ClientProtocolEntrypoint<Protocol>`
82
+ // Value only. The body type comes from the shared contract; do not add a generic here.
83
+ const project = await create.call({ body: { name: form.name } })
68
84
 
69
- Materializes a frontend protocol and attaches its renderer.
85
+ // Value and outcome, when the outcome decides the next step.
86
+ const { value, outcome } = await ctx.entrypoint(apiProtocols.project.get).invoke({ params: { id } })
87
+ if (outcome !== EntrypointOutcome.Ok) {
88
+ throw new ProjectMissing(id)
89
+ }
70
90
 
71
- ### `stab: RefedEntrypointHandler`
91
+ // A screen address; absolute when the route belongs to another service or when asked for.
92
+ const href = await ctx.entrypoint(webProtocols.project).url({ params: { id: value.id } }, { absolute: true })
93
+ ```
72
94
 
73
- No-op handler for frontend-only entrypoints that are addressed by URL rather than called.
95
+ ### 4. Timeouts and cancellation
96
+
97
+ ```ts
98
+ const controller = new AbortController()
99
+ const timer = setTimeout(() => controller.abort(), 15_000)
100
+
101
+ try {
102
+ const files = await ctx.entrypoint(apiProtocols.files.list).call({
103
+ params: { id: projectId },
104
+ query: { path: '/' },
105
+ timeout: 10_000,
106
+ signal: controller.signal,
107
+ })
108
+ showFiles(files)
109
+ } finally {
110
+ clearTimeout(timer)
111
+ }
112
+ ```
74
113
 
75
- ### `ClientProtocolEntrypoint<Protocol>` (type)
114
+ A mutation route (`POST`, `PUT`, `PATCH`) always sends a body — `{}` when the contract has none —
115
+ so `call({ params })` is enough. Never add `{ body: {} }` as a workaround.
76
116
 
77
- - `call(request?)` — addresses the entrypoint over the wire and resolves to the value, throwing
78
- whatever error the reply carried
79
- - `invoke(request?)` — the same round trip, resolving to `{ value, outcome }`
80
- - `url(request?, { absolute? })` — builds the URL this entrypoint addresses, with `:params` filled in
81
- and the query appended; absolute when the route belongs to another service or `absolute` is asked for
82
- - `validate(request?)` — validates the request against the entrypoint filter schema
83
- - `segment()` / `path()` / `mount()` — the segment this entrypoint contributes, that segment under its
84
- ancestors, and the same path under the service base. All three are computed from the declaration
85
- and the context on every call — nothing is written back into the route.
117
+ ### 5. Narrowing a payload to a schema
86
118
 
87
- An entrypoint carrying a renderer *is* a screen: it is addressed by URL, never called over the wire,
88
- so `call()` and `invoke()` throw and point the caller at `url()`.
119
+ `pickPerSchema(object, schema)` keeps only the keys the AJV schema declares and skips `null` values.
120
+ It is useful when a form model carries more fields than the contract accepts.
89
121
 
90
- ### `provideRequest<T>(alias, path): AbstractRequest<T>`
122
+ ```ts
123
+ import { pickPerSchema } from '@owlmeans/client-entrypoint'
124
+ import type { ProjectUpdate } from 'my-app-common'
125
+ import { ProjectUpdateSchema } from 'my-app-common'
91
126
 
92
- Creates a minimal request object for programmatic `call()` invocations.
127
+ const body = pickPerSchema<typeof formValues, ProjectUpdate>(formValues, ProjectUpdateSchema)
128
+ await ctx.entrypoint(apiProtocols.project.update).call({ params: { id }, body })
129
+ ```
93
130
 
94
- ### `pickPerSchema<T>(schema, obj): Partial<T>`
131
+ ## API
95
132
 
96
- Extracts only the keys present in the AJV schema from `obj`.
133
+ ### `@owlmeans/client-entrypoint`
134
+
135
+ | Symbol | Kind | Purpose |
136
+ |---|---|---|
137
+ | `bind(protocol, options?)` | function | Materialize one protocol for a client context |
138
+ | `bindAll(tree)` | function | Materialize every protocol in a named declaration tree |
139
+ | `bindScreen(protocol, handler, options?)` | function | Materialize a frontend protocol with its renderer |
140
+ | `stab` | const | No-op `RefedEntrypointHandler` for a URL-only frontend binding |
141
+ | `provideRequest(alias, path)` | function | A minimal `AbstractRequest` for a dynamic boundary (for example a socket request) |
142
+ | `pickPerSchema(object, schema)` | function | Keep only the non-null keys an AJV schema declares |
143
+ | `ClientProtocolEntrypoint<Protocol>` | type | The bound view of a protocol — see *Entrypoint members* |
144
+ | `ClientEntrypoint<T, R>` | type | The untyped bound entrypoint shape the protocol view is built from |
145
+ | `ClientEntrypointOptions` | type | `routeOptions?` (`ClientRouteOptions`), `validateOnCall?`, plus common entrypoint options |
146
+ | `ClientRequest` | type | `AbstractRequest` as the client sends it |
147
+ | `EntrypointCall`, `EntrypointInvoke`, `EntrypointReply`, `EntrypointUrl`, `EntrypointUrlOptions`, `EntrypointFilter` | type | Verb signatures; `EntrypointReply` is `{ value, outcome }`, `EntrypointUrlOptions` is `{ absolute? }` |
148
+ | `EntrypointRef`, `RefedEntrypointHandler` | type | The handler-factory contract `bindScreen` and `stab` use |
149
+ | `ClientEntrypointError` | class | Base client entrypoint error |
150
+ | `ClientValidationError` | class | Thrown by `validate()` when a request does not match its schema |
151
+
152
+ ### Entrypoint members
153
+
154
+ | Member | Purpose |
155
+ |---|---|
156
+ | `call(request?)` | Round trip; resolves to the value and throws the reply's error |
157
+ | `invoke(request?)` | Same round trip; resolves to `{ value, outcome }` |
158
+ | `url(request?, { absolute? })` | The address, with `:params` filled and the query appended |
159
+ | `validate(request?)` | Check the request against the protocol's filter schemas |
160
+ | `segment()`, `path()`, `mount()` | This route's segment, the segment under its ancestors, and that path under the service base — computed on every call, never written back |
161
+ | `protocol` | The declaration the binding was made from |
97
162
 
98
163
  ### `@owlmeans/client-entrypoint/utils`
99
164
 
100
- The low-level pair the verbs are built on, for code that holds an entrypoint reference directly:
101
-
102
- - `entrypointUrl(ref, request, opts?)` — the address behind `url()`
103
- - `apiInvoke(ref, opts?)` — the round trip behind `invoke()`
104
-
105
- ## Related Packages
106
-
107
- - [`@owlmeans/entrypoint`](../entrypoint) — `CommonEntrypoint` base that gets materialized
108
- - [`@owlmeans/client`](../client) — `useNavigate` navigates by a bound protocol's `url()`
165
+ | Symbol | Kind | Purpose |
166
+ |---|---|---|
167
+ | `apiInvoke(ref, options?)` | function | The round trip behind `invoke()` |
168
+ | `apiHandler(ref)` | function | The handler that picks the transport service or the API client |
169
+ | `entrypointUrl(ref, request?, options?)` | function | The address behind `url()` |
170
+ | `validate(ref)` | function | The filter behind `validate()` |
171
+ | `isEntrypoint(value)` | function | Re-export from `@owlmeans/entrypoint/utils` |
172
+
173
+ ## Common pitfalls
174
+
175
+ - **Pass protocol objects, never alias strings.** `context.entrypoint(protocol)` is the lookup;
176
+ `entrypointRef<Request, Response>(alias)` from `@owlmeans/entrypoint` is reserved for a dynamic
177
+ remote declaration that cannot be imported.
178
+ - **Do not add a result generic at the call site.** When a type is wrong, fix the shared contract.
179
+ - **Bind route parents too.** An unbound protocol throws "entrypoint not found" before the request
180
+ is sent, which a broad `catch` easily misreports as a server error.
181
+ - **Screens reject `call()` and `invoke()`.** Use `url()` or navigate to them.
182
+ - **`cfg.webService` must resolve.** A context whose `webService` names no client for the route's
183
+ service (and no default key) throws a `SyntaxError` on the first call.
184
+ - **Socket URLs are always absolute.** A relative value would resolve against the page origin, which
185
+ in a split deployment is the web host rather than the service answering the upgrade.
186
+ - **Never mutate a declaration or a shared declaration collection** to change a guard, gate or
187
+ service. Use binding options, or derive an immutable decoration in the shared package.
188
+
189
+ ## Related packages
190
+
191
+ - [`@owlmeans/entrypoint`](../entrypoint) — `protocol()`, `contract()`, `typed()` and protocol trees
192
+ - [`@owlmeans/client`](../client) — `handler`, `useNavigate`, `useEntrypoint`
193
+ - [`@owlmeans/client-route`](../client-route) — client route models and `ClientRouteOptions`
194
+ - [`@owlmeans/api`](../api) — the HTTP client that carries calls
195
+ - [`@owlmeans/client-socket`](../client-socket) — socket transport for socket protocols
196
+ - [`@owlmeans/server-entrypoint`](../server-entrypoint) — the server-side counterpart
197
+ - [`@owlmeans/web-client`](../web-client) — re-exports the binding helpers for browser apps
109
198
 
110
199
  <!-- owlmeans:agent-guidance:start -->
111
200
  ## Agent guidance
@@ -115,7 +204,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
115
204
  your project's skill store (`.agents/skills/`):
116
205
 
117
206
  ```sh
118
- npx @owlmeans/agent-skills@^0.1.18-rc.20
207
+ npx @owlmeans/agent-skills@^0.1.18-rc.21
119
208
  ```
120
209
 
121
210
  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/client-entrypoint",
4
- "version": "0.1.18-rc.21",
5
- "generatedAt": "2026-09-12T14:18:54.785Z",
4
+ "version": "0.1.18-rc.22",
5
+ "generatedAt": "2026-09-15T12:21:31.897Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -7,7 +7,7 @@ user-invocable: false
7
7
 
8
8
  # @owlmeans/client-entrypoint
9
9
 
10
- **Install:** `bun add @owlmeans/client-entrypoint@^0.1.18-rc.19`
10
+ **Install:** `bun add @owlmeans/client-entrypoint@^0.1.18-rc.21`
11
11
 
12
12
  Bind a declaration from `@owlmeans/entrypoint`; never construct or replace a contextual
13
13
  entrypoint by alias.
@@ -15,13 +15,13 @@ entrypoint by alias.
15
15
  ```ts
16
16
  import { bind, bindAll, bindScreen } from '@owlmeans/client-entrypoint'
17
17
 
18
- context.registerEntrypoints(bindAll(projectEntrypoints))
19
- context.registerEntrypoint(bind(projectEntrypoints.health))
20
- context.registerEntrypoint(bindScreen(projectEntrypoints.home, handler(Home)))
18
+ context.registerEntrypoints(bindAll(projectProtocols))
19
+ context.registerEntrypoint(bind(projectProtocols.health))
20
+ context.registerEntrypoint(bindScreen(projectProtocols.home, handler(Home)))
21
21
 
22
- const value = await context.entrypoint(projectEntrypoints.create).call({ body })
23
- const { value, outcome } = await context.entrypoint(projectEntrypoints.create).invoke({ body })
24
- const href = await context.entrypoint(projectEntrypoints.edit).url({ params: { id } })
22
+ const value = await context.entrypoint(projectProtocols.create).call({ body })
23
+ const { value, outcome } = await context.entrypoint(projectProtocols.create).invoke({ body })
24
+ const href = await context.entrypoint(projectProtocols.edit).url({ params: { id } })
25
25
  ```
26
26
 
27
27
  `bindAll(tree)` flattens a nested named declaration tree while preserving the union of its protocol
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/client-entrypoint",
3
- "version": "0.1.18-rc.21",
3
+ "version": "0.1.18-rc.22",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -28,15 +28,15 @@
28
28
  }
29
29
  },
30
30
  "dependencies": {
31
- "@owlmeans/api": "^0.1.18-rc.21",
32
- "@owlmeans/client-config": "^0.1.18-rc.20",
33
- "@owlmeans/client-context": "^0.1.18-rc.21",
34
- "@owlmeans/client-route": "^0.1.18-rc.21",
35
- "@owlmeans/config": "^0.1.18-rc.20",
36
- "@owlmeans/context": "^0.1.18-rc.16",
37
- "@owlmeans/entrypoint": "^0.1.18-rc.19",
38
- "@owlmeans/error": "^0.1.18-rc.16",
39
- "@owlmeans/route": "^0.1.18-rc.17",
31
+ "@owlmeans/api": "^0.1.18-rc.22",
32
+ "@owlmeans/client-config": "^0.1.18-rc.21",
33
+ "@owlmeans/client-context": "^0.1.18-rc.22",
34
+ "@owlmeans/client-route": "^0.1.18-rc.22",
35
+ "@owlmeans/config": "^0.1.18-rc.21",
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/route": "^0.1.18-rc.18",
40
40
  "ajv-formats": "^3.0.1",
41
41
  "qs": "^6.13.0"
42
42
  },