@owlmeans/client-planning 0.1.18-rc.0
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 +128 -0
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/client-planning/SKILL.md +212 -0
- package/build/client.d.ts +26 -0
- package/build/client.d.ts.map +1 -0
- package/build/client.js +120 -0
- package/build/client.js.map +1 -0
- package/build/commits.d.ts +17 -0
- package/build/commits.d.ts.map +1 -0
- package/build/commits.js +205 -0
- package/build/commits.js.map +1 -0
- package/build/consts.d.ts +27 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +29 -0
- package/build/consts.js.map +1 -0
- package/build/events.d.ts +27 -0
- package/build/events.d.ts.map +1 -0
- package/build/events.js +109 -0
- package/build/events.js.map +1 -0
- package/build/facade.d.ts +19 -0
- package/build/facade.d.ts.map +1 -0
- package/build/facade.js +130 -0
- package/build/facade.js.map +1 -0
- package/build/feed.d.ts +18 -0
- package/build/feed.d.ts.map +1 -0
- package/build/feed.js +123 -0
- package/build/feed.js.map +1 -0
- package/build/helper.d.ts +11 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +10 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +10 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +9 -0
- package/build/index.js.map +1 -0
- package/build/stores.d.ts +28 -0
- package/build/stores.d.ts.map +1 -0
- package/build/stores.js +61 -0
- package/build/stores.js.map +1 -0
- package/build/types.d.ts +147 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/record.d.ts +25 -0
- package/build/utils/record.d.ts.map +1 -0
- package/build/utils/record.js +72 -0
- package/build/utils/record.js.map +1 -0
- package/package.json +55 -0
- package/src/client.ts +153 -0
- package/src/commits.ts +247 -0
- package/src/consts.ts +38 -0
- package/src/events.ts +121 -0
- package/src/facade.ts +169 -0
- package/src/feed.ts +131 -0
- package/src/helper.ts +18 -0
- package/src/index.ts +10 -0
- package/src/stores.ts +81 -0
- package/src/types.ts +163 -0
- package/src/utils/record.ts +91 -0
- package/tests/commits.spec.ts +67 -0
- package/tests/context.ts +301 -0
- package/tests/events.spec.ts +95 -0
- package/tests/facade.spec.ts +83 -0
- package/tests/feed.spec.ts +39 -0
- package/tests/stores.spec.ts +67 -0
- package/tests/tsconfig.json +17 -0
- package/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @owlmeans/client-planning
|
|
2
|
+
|
|
3
|
+
The client half of OwlMeans planning: a `PlanningFacade` over the protocol tree a server mounted
|
|
4
|
+
with [`@owlmeans/server-planning`](../server-planning), an optional state mirror of cards, links and
|
|
5
|
+
commits, and a commit wait that subscribes first and then long-polls. It is React-free, so the same
|
|
6
|
+
code runs in a browser app and in a Node client. Use `@owlmeans/server-planning` instead wherever the
|
|
7
|
+
planning service runs in-process.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add @owlmeans/client-planning@^0.1.18-rc.0
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Concepts
|
|
16
|
+
|
|
17
|
+
- **Remote facade** — the same `PlanningFacade` interface the server answers; every method is one
|
|
18
|
+
entrypoint call, and `modelOf(record, facade)` models behave identically on both sides.
|
|
19
|
+
- **One id space** — projects, cards and specifications share ONE card store; a list reaches it
|
|
20
|
+
through `syncCards(store, items, where)`, never `replace()`.
|
|
21
|
+
- **Commit** — `execute()` returns a receipt for a pending transition; `receipt.committed()` /
|
|
22
|
+
`commits.wait()` resolve with the folded card, or throw `CommitTimeout` / `CommitFailed`.
|
|
23
|
+
- **Socket seam** — the commit socket is opened by an injected `PlanningSocketOpener`; without one
|
|
24
|
+
the client long-polls.
|
|
25
|
+
- **`head > seq`** — a card with a transition allocated and not yet folded; `applyReceipt` marks it
|
|
26
|
+
the moment the server answers.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
Wire the client into a context with the server's own tree:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { makePlanningProtocols } from '@owlmeans/planning'
|
|
34
|
+
import { appendPlanningClient, appendPlanningStores } from '@owlmeans/client-planning'
|
|
35
|
+
|
|
36
|
+
const planningProtocols = makePlanningProtocols({
|
|
37
|
+
base: { alias: 'app:api:planning', path: '/planning', service: API },
|
|
38
|
+
guards: DEFAULT_GUARD,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
appendPlanningClient(context, { protocols: planningProtocols, poll: 20 })
|
|
42
|
+
appendPlanningStores(context)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Read through the facade — rich queries, encoded for the wire by the package:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const planning = context.planning().for()
|
|
49
|
+
const { items } = await planning.cards.list({ parent: projectId, type: 'app:story', sort: ['order'], size: 0 })
|
|
50
|
+
const counts = await planning.cards.summary([projectId])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Transit a card and wait for the commit:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const story = await planning.model(items[0])
|
|
57
|
+
if (story.can('start')) {
|
|
58
|
+
const card = await (await story.transit('start')).committed({ timeout: 15_000 })
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Keep a mirror current with one feed per screen:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { makePlanningFeed } from '@owlmeans/client-planning'
|
|
66
|
+
|
|
67
|
+
const feed = makePlanningFeed(context, {
|
|
68
|
+
query: { parent: projectId }, filter: { project: projectId }, refresh: 15_000,
|
|
69
|
+
})
|
|
70
|
+
await feed.ready
|
|
71
|
+
// … read context.planningStores().cards with useStoreList from @owlmeans/client
|
|
72
|
+
await feed.stop()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Give a browser its commit socket through the host's own opener:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
appendPlanningClient(context, {
|
|
79
|
+
protocols: planningProtocols,
|
|
80
|
+
socket: async (protocol, request) => await ws(context.entrypoint(protocol), request),
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## API
|
|
85
|
+
|
|
86
|
+
- `appendPlanningClient(context, options)`, `makePlanningClientService(context, options)`
|
|
87
|
+
- `makeRemoteFacade(context, protocols, scope, opts)`, `makeRemoteCommitSource(context, protocols, opts?)`
|
|
88
|
+
- `appendPlanningStores(context, aliases?)`, `planningStoresOf(context)`, `syncCards`, `syncLinks`
|
|
89
|
+
- `applyCommitEvent(stores, event, facade?)`, `applyReceipt(stores, view)`, `applyCards(stores, cards)`
|
|
90
|
+
- `makePlanningFeed(context, opts?)`
|
|
91
|
+
- `planningOf(context, scope?)`, `planningModelOf(context, card, scope?)`
|
|
92
|
+
- `CARDS`, `LINKS`, `COMMITS`, `DEFAULT_STORE_ALIASES`, `LONG_POLL_GRACE`, `EARLY_POLL_LADDER`, `EARLY_POLL_MS`
|
|
93
|
+
- Types: `PlanningClientOptions`, `PlanningClientService`, `WithPlanningClient`,
|
|
94
|
+
`PlanningSocketOpener`, `RemoteCommitSource`, `RemoteCommitSourceOptions`, `RemoteFacadeOptions`,
|
|
95
|
+
`PlanningStores`, `PlanningStoreAliases`, `WithPlanningStores`, `PlanningCommitRecord`,
|
|
96
|
+
`SyncOptions`, `PlanningFeed`, `PlanningFeedOptions`, `PlanningFeedState`, `Config`, `Context`
|
|
97
|
+
|
|
98
|
+
## Common pitfalls
|
|
99
|
+
|
|
100
|
+
- The tree must match the server's alias for alias; a `socketBase` is the host's to bind.
|
|
101
|
+
- Never `replace()` the card store — it holds every kind.
|
|
102
|
+
- The scope passed to `for()` is advisory; the server takes the entity and actor from the credential.
|
|
103
|
+
- `CommitTimeout` leaves the transition pending — re-wait or re-read, do not re-execute without a `key`.
|
|
104
|
+
- A client refuses `store()`, `committed()` and plugins carrying hooks with `PlanningUnsupported`.
|
|
105
|
+
- Mount one feed per screen; every other reader uses the store.
|
|
106
|
+
|
|
107
|
+
## Related packages
|
|
108
|
+
|
|
109
|
+
- [`@owlmeans/planning`](../planning) — records, flows, the fold, models, the protocol tree
|
|
110
|
+
- [`@owlmeans/server-planning`](../server-planning) — the handlers, the executor, the memory store
|
|
111
|
+
- [`@owlmeans/state`](../state) — the store the mirror is built on
|
|
112
|
+
- [`@owlmeans/client`](../client) — `useStoreModel` / `useStoreList` over the mirror
|
|
113
|
+
|
|
114
|
+
<!-- owlmeans:agent-guidance:start -->
|
|
115
|
+
## Agent guidance
|
|
116
|
+
|
|
117
|
+
This package ships embedded agent skills under `agent-meta/`. After installing your
|
|
118
|
+
`@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
|
|
119
|
+
your project's skill store (`.agents/skills/`):
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.27
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The embedded files are version-matched to this package release. Do not edit them
|
|
126
|
+
directly — they are regenerated on each publish. To contribute guidance edits,
|
|
127
|
+
open a PR against the source monorepo.
|
|
128
|
+
<!-- owlmeans:agent-guidance:end -->
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"package": "@owlmeans/client-planning",
|
|
4
|
+
"version": "0.1.18-rc.0",
|
|
5
|
+
"generatedAt": "2026-09-16T15:11:05.003Z",
|
|
6
|
+
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
|
+
"entries": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "skill",
|
|
10
|
+
"name": "client-planning",
|
|
11
|
+
"category": "package-specific",
|
|
12
|
+
"file": "skills/client-planning/SKILL.md",
|
|
13
|
+
"canonicalPath": ".agents/skills/client-planning/SKILL.md"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: client-planning
|
|
3
|
+
description: How to use @owlmeans/client-planning — appendPlanningClient for the remote planning facade, appendPlanningStores and applyCommitEvent for the state mirror, makePlanningFeed, commits.wait (subscribe then long-poll), and the models that are the same objects the server uses. Auto-invoked when reading planning data in a browser or a Node client, mounting the commit feed, or wiring optimistic transitions.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
|
+
|
|
8
|
+
# @owlmeans/client-planning
|
|
9
|
+
|
|
10
|
+
**Layer:** Client
|
|
11
|
+
**Install:** `"@owlmeans/client-planning": "^0.1.18-rc.0"` in `dependencies`
|
|
12
|
+
|
|
13
|
+
The client half of OwlMeans planning. It answers the `PlanningFacade` interface of
|
|
14
|
+
`@owlmeans/planning` over the protocol tree a server mounted with `@owlmeans/server-planning`, keeps
|
|
15
|
+
an optional `@owlmeans/state` mirror of cards, links and commits current, and waits for commits.
|
|
16
|
+
It is **React-free**: the same package runs in a browser app and in a Node client (a connector, a
|
|
17
|
+
CLI). React hooks over the mirror are `useStoreModel` / `useStoreList` from `@owlmeans/client`.
|
|
18
|
+
|
|
19
|
+
## Key Exports
|
|
20
|
+
|
|
21
|
+
| Export | Description |
|
|
22
|
+
|--------|-------------|
|
|
23
|
+
| `appendPlanningClient(context, options)` | Bind the tree, register the service under `PLANNING_SERVICE`, add `context.planning()` |
|
|
24
|
+
| `makePlanningClientService(context, options)` | The service itself, for a host that registers it on its own |
|
|
25
|
+
| `makeRemoteFacade(context, protocols, scope, opts)` | One facade — every method is one entrypoint call |
|
|
26
|
+
| `makeRemoteCommitSource(context, protocols, opts?)` | `status` / `subscribe` / `wait` over `commit.get` and `commit.events` |
|
|
27
|
+
| `appendPlanningStores(context, aliases?)` / `planningStoresOf(context)` | The state mirror, and a lookup that answers `null` without one |
|
|
28
|
+
| `syncCards(store, items, where?, opts?)` / `syncLinks(...)` | Make the mirror agree with a list WITHIN a scope |
|
|
29
|
+
| `applyCommitEvent(stores, event, facade?)` / `applyReceipt(stores, view)` / `applyCards(stores, cards)` | The folds |
|
|
30
|
+
| `makePlanningFeed(context, opts?)` | Subscribe, seed, fold, refresh — `{ connected, seeded, error, ready, refresh, stop }` |
|
|
31
|
+
| `planningOf(context, scope?)` / `planningModelOf(context, card, scope?)` | The facade / a model with the schemas loaded |
|
|
32
|
+
| `CARDS`, `LINKS`, `COMMITS` | Store aliases (`planning-card-state`, `planning-link-state`, `planning-commit-state`) |
|
|
33
|
+
| Types | `PlanningClientOptions`, `PlanningClientService`, `WithPlanningClient`, `PlanningSocketOpener`, `RemoteCommitSource`, `PlanningStores`, `PlanningStoreAliases`, `WithPlanningStores`, `PlanningCommitRecord`, `PlanningFeed`, `PlanningFeedOptions`, `PlanningFeedState`, `SyncOptions` |
|
|
34
|
+
|
|
35
|
+
## Wiring
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { makePlanningProtocols } from '@owlmeans/planning'
|
|
39
|
+
import { appendPlanningClient, appendPlanningStores } from '@owlmeans/client-planning'
|
|
40
|
+
|
|
41
|
+
export const planningProtocols = makePlanningProtocols({
|
|
42
|
+
base: { alias: 'app:api:planning', path: '/planning', service: API },
|
|
43
|
+
guards: DEFAULT_GUARD,
|
|
44
|
+
socketBase: updateBase,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
appendPlanningClient(context, {
|
|
48
|
+
protocols: planningProtocols,
|
|
49
|
+
socket: async (protocol, request) => await ws(context.entrypoint(protocol), request),
|
|
50
|
+
poll: 20,
|
|
51
|
+
})
|
|
52
|
+
appendPlanningStores(context) // a browser mirror; a Node client usually skips it
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- The tree is **the server's tree** — the same `makePlanningProtocols` options, alias for alias.
|
|
56
|
+
Declare it once in the application's shared contract package and import it on both sides.
|
|
57
|
+
- When the commit feed hangs under a `socketBase`, the HOST binds that base too. A parent a
|
|
58
|
+
registry cannot resolve fails the whole context at init, not the one call that uses it.
|
|
59
|
+
- `bind: false` when the host already bound the tree.
|
|
60
|
+
- The service answers under `PLANNING_SERVICE`, the alias the server's service uses, so
|
|
61
|
+
`context.planning().for(scope)` is the same call in a handler and in a screen. The scope is
|
|
62
|
+
**advisory**: the server takes the entity and the actor from the credential. The execution's own
|
|
63
|
+
`actor` is never sent.
|
|
64
|
+
- A client runs no middleware and folds nothing: `store()`, `committed()` and a plugin carrying
|
|
65
|
+
`before`/`after`/`store`/`mintCode` answer `PlanningUnsupported`. `use()` accepts a schemas-only
|
|
66
|
+
plugin, layered over the server's bundle.
|
|
67
|
+
- `timeout` (ms) bounds every call that is not a long poll; a long poll's deadline is its own hold
|
|
68
|
+
plus `LONG_POLL_GRACE`.
|
|
69
|
+
|
|
70
|
+
### The schema bundle
|
|
71
|
+
|
|
72
|
+
`model()` needs the flows and types (`can()`, `available()`, `statusOf()` read the registry with no
|
|
73
|
+
round trip). The bundle is fetched in the background once the context is **Ready** and a failure
|
|
74
|
+
there is swallowed — a browser that is not signed in yet cannot read it, and a context must never
|
|
75
|
+
fail or wait over it. `model()` and `service.loadSchemas()` load it on first use regardless, and a
|
|
76
|
+
failed load is retried by the next caller. `schemas: false` skips the background load.
|
|
77
|
+
|
|
78
|
+
## Reading
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const planning = context.planning().for()
|
|
82
|
+
|
|
83
|
+
const stories = await planning.cards.list({ parent: projectId, type: STORY, sort: ['order'], size: 0 })
|
|
84
|
+
const board = await planning.cards.summary([projectId])
|
|
85
|
+
const brief = await planning.specifications.current(projectId, 'specification')
|
|
86
|
+
const story = await planning.model(storyId)
|
|
87
|
+
story.available().map(rule => rule.name)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- The facade takes the rich `WorkcardQuery`; it encodes the scalar wire shape itself
|
|
91
|
+
(`encodeWorkcardQuery` and its twins). Never pass a wire shape to the facade.
|
|
92
|
+
- `cards.load(id)` answers `null` for a missing card — and for another entity's card, which the
|
|
93
|
+
server deliberately reports the same way.
|
|
94
|
+
- `cards.count(query)` is a one-row list read for its `total`; the tree declares no count route.
|
|
95
|
+
- `transitions.list(query)` needs `card` — the tree reads the log through one card, so a
|
|
96
|
+
project-wide read answers `PlanningUnsupported` on a client.
|
|
97
|
+
- `size: 0` is "no limit"; a paged server answers 100 rows when no size is asked for.
|
|
98
|
+
|
|
99
|
+
## The mirror — one id space
|
|
100
|
+
|
|
101
|
+
`appendPlanningStores` registers ONE card store for every kind. Projects, cards and specifications
|
|
102
|
+
share an id space on the server, so they share one store here: a store per kind lets a card list
|
|
103
|
+
that reloads drop the projects beside it.
|
|
104
|
+
|
|
105
|
+
That is also why a list reaches the store through **`syncCards(store, items, where)`, never
|
|
106
|
+
`replace()`**: every card given is written, every card matching `where` that the list does not name
|
|
107
|
+
is dropped, and everything outside `where` is left alone. An unchanged card is not rewritten, so a
|
|
108
|
+
periodic re-seed wakes no subscriber. `opts.keep` protects ids a commit wrote while the list was in
|
|
109
|
+
flight.
|
|
110
|
+
|
|
111
|
+
The fold rules, shared by `syncCards`, `applyCards`, `applyCommitEvent` and `commits.wait`:
|
|
112
|
+
|
|
113
|
+
- an OLDER `seq` never overwrites a newer record — frames and list answers race;
|
|
114
|
+
- a committed `delete` removes the row and every link touching it; a project's delete also drops
|
|
115
|
+
the rows whose `parents` hold it (the server purged them in the same fold);
|
|
116
|
+
- a frame for an id the store never saw still writes a row;
|
|
117
|
+
- a `failed` commit leaves the card alone and records the reason in the commit store;
|
|
118
|
+
- a frame without `record` (a cross-process bus carries ids only) is re-read through the facade —
|
|
119
|
+
without one, only the commit row is written;
|
|
120
|
+
- a committed `link`/`unlink` re-lists the card's outgoing links; links written together with a
|
|
121
|
+
`create` arrive with the next link seed.
|
|
122
|
+
|
|
123
|
+
## The feed
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const feed = makePlanningFeed(context, {
|
|
127
|
+
query: { parent: projectId, type: STORY },
|
|
128
|
+
filter: { project: projectId },
|
|
129
|
+
refresh: 15_000,
|
|
130
|
+
onChange: state => setState(state),
|
|
131
|
+
})
|
|
132
|
+
await feed.ready // never rejects — read feed.error
|
|
133
|
+
// …
|
|
134
|
+
await feed.stop() // stops folding; the shared socket stays open
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
1. Subscribes to commits FIRST (the socket, when an opener exists).
|
|
138
|
+
2. Seeds: `cards.list` (unpaged unless the query pages) → `syncCards` over `where`
|
|
139
|
+
(`criteriaOf(query)` by default). A PAGED seed only writes — a page cannot say what does not
|
|
140
|
+
exist.
|
|
141
|
+
3. Folds every frame with `applyCommitEvent`, and re-seeds every `refresh` ms — the authoritative
|
|
142
|
+
backstop to a socket that can drop frames.
|
|
143
|
+
|
|
144
|
+
`connected` reports whether a socket was open when the feed subscribed; `seeded` tells "nothing
|
|
145
|
+
there" from "not loaded yet". A React hook wraps the feed in an effect and stops it on unmount.
|
|
146
|
+
|
|
147
|
+
## Waiting for a commit
|
|
148
|
+
|
|
149
|
+
`execute()` is asynchronous: the receipt names a PENDING transition until the store folds it.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const receipt = await planning.execute({ card: id, action: TransitionAction.Transit, transition: 'start' })
|
|
153
|
+
const card = await receipt.committed({ timeout: 15_000 }) // or execute(exec, { wait: true, timeout })
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`commits.wait(transition, { timeout })`:
|
|
157
|
+
|
|
158
|
+
1. **subscribes first** — a commit landing between a status read and a subscription is otherwise
|
|
159
|
+
missed until the deadline;
|
|
160
|
+
2. reads the status once with `wait: 0`;
|
|
161
|
+
3. long-polls `commit.get` in holds of `min(poll, remaining)` seconds, racing any socket frame;
|
|
162
|
+
4. at the deadline throws `CommitTimeout` — the transition STAYS pending, nothing is undone.
|
|
163
|
+
|
|
164
|
+
A `failed` commit throws `CommitFailed` with the store's reason. A settled commit is folded into
|
|
165
|
+
the mirror when stores are registered. A poll the server answers without holding is followed by a
|
|
166
|
+
short pause ladder instead of a tight loop, and a dropped hold is replaced by one non-holding read.
|
|
167
|
+
|
|
168
|
+
`execute({ wait: true })` never holds the POST on the wire: the receipt returns at once and the
|
|
169
|
+
wait above runs from the client. A lost response therefore never leaves a caller unsure whether
|
|
170
|
+
the transition was appended — the key (`exec.key`) still makes a retry of the POST itself safe.
|
|
171
|
+
|
|
172
|
+
## The socket seam
|
|
173
|
+
|
|
174
|
+
`socket` is a `PlanningSocketOpener` — `(protocol, request?) => Promise<Connection | null>`. The
|
|
175
|
+
package never imports `@owlmeans/client-socket`, whose helpers pull React; a browser host wraps its
|
|
176
|
+
own `ws()` there (which also puts the session token on the connection). `null` means "no socket
|
|
177
|
+
right now" and the next subscription asks again. ONE connection serves every subscription of the
|
|
178
|
+
service; releasing a subscription never closes it — `context.planning().close()` does.
|
|
179
|
+
|
|
180
|
+
A Node client passes no opener and relies on the long poll.
|
|
181
|
+
|
|
182
|
+
## Optimistic writes and `head > seq`
|
|
183
|
+
|
|
184
|
+
`seq` is the last transition folded into a card, `head` the highest allocated. `applyReceipt`
|
|
185
|
+
(run by `execute` whenever stores are registered) raises the stored card's `head` to the new
|
|
186
|
+
transition's `seq`, so `model.pending()` is true before any frame arrives; the commit's fold brings
|
|
187
|
+
`seq` up to it. The mirror only ever grows `head`, so a list fetched before the append does not
|
|
188
|
+
clear the marker. A model's writes default `expectSeq` to `record.head ?? record.seq`; a stale model
|
|
189
|
+
gets `WorkcardConflict` — reload and retry.
|
|
190
|
+
|
|
191
|
+
## Gotchas
|
|
192
|
+
|
|
193
|
+
- Mount a feed once per screen; read the store everywhere else.
|
|
194
|
+
- Do not `replace()` the card store — it holds every kind. Use `syncCards` with a `where`.
|
|
195
|
+
- A refusal crosses the hop as its class (`IllegalTransition`, `WorkcardConflict`,
|
|
196
|
+
`FieldsInvalid`, a plugin's own class) — branch on `instanceof`, never on message text.
|
|
197
|
+
- `CommitTimeout` is not a failure of the transition; re-wait or re-read the card.
|
|
198
|
+
- The scope passed to `for()` changes nothing the server decides; it exists so code written against
|
|
199
|
+
`PlanningFacade` reads the same on both sides.
|
|
200
|
+
|
|
201
|
+
## Depends On
|
|
202
|
+
|
|
203
|
+
- `@owlmeans/planning` — the facade interface, the protocol tree, the wire encoders, `modelOf`
|
|
204
|
+
- `@owlmeans/client-entrypoint` — `bindAll`, the typed `call()`
|
|
205
|
+
- `@owlmeans/state` — the mirror; `@owlmeans/socket` — `Connection`
|
|
206
|
+
|
|
207
|
+
## Related
|
|
208
|
+
|
|
209
|
+
- `planning` — records, flows, the fold, the models
|
|
210
|
+
- `server-planning` — the handlers this addresses, the executor, the commit hub
|
|
211
|
+
- `client-job` — the same seed-then-fold shape for queue jobs
|
|
212
|
+
- `state` — `syncCards` is built on its criteria engine and `purge`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Config, Context, PlanningClientOptions, PlanningClientService, WithPlanningClient } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The client planning service: a remote facade factory under the SAME alias the server registers
|
|
4
|
+
* its service under, so `context.planning().for(scope)` is one call on both sides.
|
|
5
|
+
*
|
|
6
|
+
* What only a server can do is refused rather than faked: a client runs no middleware and folds
|
|
7
|
+
* nothing, so `store()`, `committed()` and a plugin carrying hooks or a store answer
|
|
8
|
+
* `PlanningUnsupported`. A plugin carrying only schemas is accepted — it layers over the server's
|
|
9
|
+
* bundle.
|
|
10
|
+
*/
|
|
11
|
+
export declare const makePlanningClientService: <C extends Config, T extends Context<C>>(context: T, options: PlanningClientOptions) => PlanningClientService;
|
|
12
|
+
/**
|
|
13
|
+
* Wire the planning client into a client context: bind the protocol tree as client entrypoints
|
|
14
|
+
* (unless `bind: false`), register the service under `PLANNING_SERVICE`, and add
|
|
15
|
+
* `context.planning()`.
|
|
16
|
+
*
|
|
17
|
+
* The tree must be the one the server mounted — the same `makePlanningProtocols` options, alias for
|
|
18
|
+
* alias. When its commit feed hangs under a `socketBase`, that base belongs to the host and must be
|
|
19
|
+
* bound by the host too: a parent a registry cannot resolve fails the whole context at init.
|
|
20
|
+
*
|
|
21
|
+
* The schema bundle is fetched in the background once the context is READY, and a failure there is
|
|
22
|
+
* swallowed — a browser that is not signed in yet cannot read it, and a context must not fail over
|
|
23
|
+
* data it can fetch later. `model()` and `loadSchemas()` load it on first use regardless.
|
|
24
|
+
*/
|
|
25
|
+
export declare const appendPlanningClient: <C extends Config, T extends Context<C>>(context: T, options: PlanningClientOptions) => T & WithPlanningClient;
|
|
26
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,MAAM,EAAE,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,kBAAkB,EAClF,MAAM,YAAY,CAAA;AAEnB;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WACrE,CAAC,WAAW,qBAAqB,KACzC,qBAoFF,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WAChE,CAAC,WAAW,qBAAqB,KACzC,CAAC,GAAG,kBA0BN,CAAA"}
|
package/build/client.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { bindAll } from '@owlmeans/client-entrypoint';
|
|
2
|
+
import { createLazyService, MiddlewareStage, MiddlewareType } from '@owlmeans/context';
|
|
3
|
+
import { DEFAULT_PLUGIN_ORDER, makeSchemaRegistry, PLANNING_SERVICE, PlanningUnsupported, } from '@owlmeans/planning';
|
|
4
|
+
import { makeRemoteCommitSource } from './commits.js';
|
|
5
|
+
import { makeRemoteFacade } from './facade.js';
|
|
6
|
+
import { planningStoresOf } from './stores.js';
|
|
7
|
+
/**
|
|
8
|
+
* The client planning service: a remote facade factory under the SAME alias the server registers
|
|
9
|
+
* its service under, so `context.planning().for(scope)` is one call on both sides.
|
|
10
|
+
*
|
|
11
|
+
* What only a server can do is refused rather than faked: a client runs no middleware and folds
|
|
12
|
+
* nothing, so `store()`, `committed()` and a plugin carrying hooks or a store answer
|
|
13
|
+
* `PlanningUnsupported`. A plugin carrying only schemas is accepted — it layers over the server's
|
|
14
|
+
* bundle.
|
|
15
|
+
*/
|
|
16
|
+
export const makePlanningClientService = (context, options) => {
|
|
17
|
+
const registry = makeSchemaRegistry();
|
|
18
|
+
const plugins = [];
|
|
19
|
+
const stores = () => planningStoresOf(context);
|
|
20
|
+
const commits = makeRemoteCommitSource(context, options.protocols, {
|
|
21
|
+
socket: options.socket, poll: options.poll, timeout: options.timeout, stores,
|
|
22
|
+
});
|
|
23
|
+
const layerPlugins = () => plugins.forEach(plugin => {
|
|
24
|
+
plugin.schemas?.flows?.forEach(registry.registerFlow);
|
|
25
|
+
plugin.schemas?.types?.forEach(registry.registerType);
|
|
26
|
+
});
|
|
27
|
+
let loaded = false;
|
|
28
|
+
let loading = null;
|
|
29
|
+
const loadSchemas = async (opts) => {
|
|
30
|
+
if (loaded && opts?.force !== true) {
|
|
31
|
+
return registry;
|
|
32
|
+
}
|
|
33
|
+
if (loading == null || opts?.force === true) {
|
|
34
|
+
const attempt = (async () => {
|
|
35
|
+
const bundle = await context.entrypoint(options.protocols.schema.list).call({ timeout: options.timeout });
|
|
36
|
+
registry.load(bundle);
|
|
37
|
+
layerPlugins();
|
|
38
|
+
loaded = true;
|
|
39
|
+
return registry;
|
|
40
|
+
})();
|
|
41
|
+
loading = attempt;
|
|
42
|
+
// A failed load is forgotten, so the next caller asks again instead of inheriting the error.
|
|
43
|
+
attempt.catch(() => {
|
|
44
|
+
if (loading === attempt) {
|
|
45
|
+
loading = null;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return await loading;
|
|
50
|
+
};
|
|
51
|
+
const facadeFor = (scope) => makeRemoteFacade(context, options.protocols, { ...options.scope, ...scope }, {
|
|
52
|
+
commits, schemas: registry, loadSchemas, timeout: options.timeout, stores,
|
|
53
|
+
});
|
|
54
|
+
let fallback = null;
|
|
55
|
+
// Lazy, like the server's host: `context.planning()` answers before the context initializes.
|
|
56
|
+
return createLazyService(PLANNING_SERVICE, {
|
|
57
|
+
schemas: registry,
|
|
58
|
+
commits,
|
|
59
|
+
loadSchemas,
|
|
60
|
+
use: plugin => {
|
|
61
|
+
if (plugin.before != null || plugin.after != null || plugin.store != null || plugin.mintCode != null) {
|
|
62
|
+
throw new PlanningUnsupported(`client:plugin:${plugin.name}`);
|
|
63
|
+
}
|
|
64
|
+
const existing = plugins.findIndex(candidate => candidate.name === plugin.name);
|
|
65
|
+
if (existing >= 0) {
|
|
66
|
+
plugins.splice(existing, 1);
|
|
67
|
+
}
|
|
68
|
+
plugins.push(plugin);
|
|
69
|
+
plugins.sort((left, right) => (left.order ?? DEFAULT_PLUGIN_ORDER) - (right.order ?? DEFAULT_PLUGIN_ORDER));
|
|
70
|
+
layerPlugins();
|
|
71
|
+
},
|
|
72
|
+
plugins: () => [...plugins],
|
|
73
|
+
store: () => {
|
|
74
|
+
throw new PlanningUnsupported('client:store');
|
|
75
|
+
},
|
|
76
|
+
for: scope => scope == null ? (fallback ??= facadeFor({})) : facadeFor(scope),
|
|
77
|
+
committed: async () => {
|
|
78
|
+
throw new PlanningUnsupported('client:committed');
|
|
79
|
+
},
|
|
80
|
+
close: async () => await commits.close(),
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Wire the planning client into a client context: bind the protocol tree as client entrypoints
|
|
85
|
+
* (unless `bind: false`), register the service under `PLANNING_SERVICE`, and add
|
|
86
|
+
* `context.planning()`.
|
|
87
|
+
*
|
|
88
|
+
* The tree must be the one the server mounted — the same `makePlanningProtocols` options, alias for
|
|
89
|
+
* alias. When its commit feed hangs under a `socketBase`, that base belongs to the host and must be
|
|
90
|
+
* bound by the host too: a parent a registry cannot resolve fails the whole context at init.
|
|
91
|
+
*
|
|
92
|
+
* The schema bundle is fetched in the background once the context is READY, and a failure there is
|
|
93
|
+
* swallowed — a browser that is not signed in yet cannot read it, and a context must not fail over
|
|
94
|
+
* data it can fetch later. `model()` and `loadSchemas()` load it on first use regardless.
|
|
95
|
+
*/
|
|
96
|
+
export const appendPlanningClient = (context, options) => {
|
|
97
|
+
const ctx = context;
|
|
98
|
+
if (options.bind !== false) {
|
|
99
|
+
ctx.registerEntrypoints(bindAll(options.protocols));
|
|
100
|
+
}
|
|
101
|
+
const service = makePlanningClientService(context, options);
|
|
102
|
+
ctx.registerService(service);
|
|
103
|
+
ctx.planning = () => ctx.service(PLANNING_SERVICE);
|
|
104
|
+
if (options.schemas !== false) {
|
|
105
|
+
ctx.registerMiddleware({
|
|
106
|
+
type: MiddlewareType.Context,
|
|
107
|
+
stage: MiddlewareStage.Ready,
|
|
108
|
+
apply: async () => {
|
|
109
|
+
try {
|
|
110
|
+
await service.loadSchemas();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Best effort by design — see above.
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return ctx;
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context';
|
|
2
|
+
import type { PlanningProtocols } from '@owlmeans/planning';
|
|
3
|
+
import type { RemoteCommitSource, RemoteCommitSourceOptions } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* The commit source of a remote facade: the `commit.get` long poll and, when the host injects a
|
|
6
|
+
* socket opener, the `commit.events` feed.
|
|
7
|
+
*
|
|
8
|
+
* `wait` SUBSCRIBES FIRST and only then reads the status — a commit landing between a read and a
|
|
9
|
+
* subscription would otherwise be missed until the deadline. It then long-polls in holds of
|
|
10
|
+
* `min(poll, remaining)` seconds, so a client with no socket (a Node connector) still learns of a
|
|
11
|
+
* commit within one round trip of it landing, and a client with one learns from whichever arrives
|
|
12
|
+
* first. The deadline is a {@link CommitTimeout}; the transition stays pending — nothing is undone.
|
|
13
|
+
*
|
|
14
|
+
* ONE socket serves every subscription of the source; releasing a subscription never closes it.
|
|
15
|
+
*/
|
|
16
|
+
export declare const makeRemoteCommitSource: <C extends BasicConfig, T extends BasicContext<C>>(context: T, protocols: PlanningProtocols, opts?: RemoteCommitSourceOptions) => RemoteCommitSource;
|
|
17
|
+
//# sourceMappingURL=commits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commits.d.ts","sourceRoot":"","sources":["../src/commits.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAKlE,OAAO,KAAK,EAC+B,iBAAiB,EAC3D,MAAM,oBAAoB,CAAA;AAG3B,OAAO,KAAK,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;AAuB/E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GAAI,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,WAC5E,CAAC,aAAa,iBAAiB,SAAS,yBAAyB,KACzE,kBAuMF,CAAA"}
|