@owlmeans/client-entrypoint 0.1.18-rc.20 → 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 +160 -71
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client-entrypoint/SKILL.md +7 -7
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -1,111 +1,200 @@
|
|
|
1
1
|
# @owlmeans/client-entrypoint
|
|
2
2
|
|
|
3
|
-
Client-side
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
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.
|
|
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
|
-
|
|
36
|
+
### 1. Bind a protocol tree and screens
|
|
25
37
|
|
|
26
|
-
```
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
### 2. Per-binding options
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
`bind` and `bindScreen` take `ClientEntrypointOptions`. Use `bind` instead of `bindAll` when one
|
|
60
|
+
protocol needs something the rest do not.
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
### 3. Call, invoke, url
|
|
62
76
|
|
|
63
|
-
|
|
77
|
+
```ts
|
|
78
|
+
import { EntrypointOutcome } from '@owlmeans/entrypoint'
|
|
64
79
|
|
|
65
|
-
|
|
80
|
+
const create = ctx.entrypoint(apiProtocols.project.create)
|
|
66
81
|
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
+
const body = pickPerSchema<typeof formValues, ProjectUpdate>(formValues, ProjectUpdateSchema)
|
|
128
|
+
await ctx.entrypoint(apiProtocols.project.update).call({ params: { id }, body })
|
|
129
|
+
```
|
|
93
130
|
|
|
94
|
-
|
|
131
|
+
## API
|
|
95
132
|
|
|
96
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/client-entrypoint",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
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.
|
|
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(
|
|
19
|
-
context.registerEntrypoint(bind(
|
|
20
|
-
context.registerEntrypoint(bindScreen(
|
|
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(
|
|
23
|
-
const { value, outcome } = await context.entrypoint(
|
|
24
|
-
const href = await context.entrypoint(
|
|
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.
|
|
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.
|
|
32
|
-
"@owlmeans/client-config": "^0.1.18-rc.
|
|
33
|
-
"@owlmeans/client-context": "^0.1.18-rc.
|
|
34
|
-
"@owlmeans/client-route": "^0.1.18-rc.
|
|
35
|
-
"@owlmeans/config": "^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/route": "^0.1.18-rc.
|
|
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
|
},
|