@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.
Files changed (67) hide show
  1. package/README.md +128 -0
  2. package/agent-meta/manifest.json +16 -0
  3. package/agent-meta/skills/client-planning/SKILL.md +212 -0
  4. package/build/client.d.ts +26 -0
  5. package/build/client.d.ts.map +1 -0
  6. package/build/client.js +120 -0
  7. package/build/client.js.map +1 -0
  8. package/build/commits.d.ts +17 -0
  9. package/build/commits.d.ts.map +1 -0
  10. package/build/commits.js +205 -0
  11. package/build/commits.js.map +1 -0
  12. package/build/consts.d.ts +27 -0
  13. package/build/consts.d.ts.map +1 -0
  14. package/build/consts.js +29 -0
  15. package/build/consts.js.map +1 -0
  16. package/build/events.d.ts +27 -0
  17. package/build/events.d.ts.map +1 -0
  18. package/build/events.js +109 -0
  19. package/build/events.js.map +1 -0
  20. package/build/facade.d.ts +19 -0
  21. package/build/facade.d.ts.map +1 -0
  22. package/build/facade.js +130 -0
  23. package/build/facade.js.map +1 -0
  24. package/build/feed.d.ts +18 -0
  25. package/build/feed.d.ts.map +1 -0
  26. package/build/feed.js +123 -0
  27. package/build/feed.js.map +1 -0
  28. package/build/helper.d.ts +11 -0
  29. package/build/helper.d.ts.map +1 -0
  30. package/build/helper.js +10 -0
  31. package/build/helper.js.map +1 -0
  32. package/build/index.d.ts +10 -0
  33. package/build/index.d.ts.map +1 -0
  34. package/build/index.js +9 -0
  35. package/build/index.js.map +1 -0
  36. package/build/stores.d.ts +28 -0
  37. package/build/stores.d.ts.map +1 -0
  38. package/build/stores.js +61 -0
  39. package/build/stores.js.map +1 -0
  40. package/build/types.d.ts +147 -0
  41. package/build/types.d.ts.map +1 -0
  42. package/build/types.js +2 -0
  43. package/build/types.js.map +1 -0
  44. package/build/utils/record.d.ts +25 -0
  45. package/build/utils/record.d.ts.map +1 -0
  46. package/build/utils/record.js +72 -0
  47. package/build/utils/record.js.map +1 -0
  48. package/package.json +55 -0
  49. package/src/client.ts +153 -0
  50. package/src/commits.ts +247 -0
  51. package/src/consts.ts +38 -0
  52. package/src/events.ts +121 -0
  53. package/src/facade.ts +169 -0
  54. package/src/feed.ts +131 -0
  55. package/src/helper.ts +18 -0
  56. package/src/index.ts +10 -0
  57. package/src/stores.ts +81 -0
  58. package/src/types.ts +163 -0
  59. package/src/utils/record.ts +91 -0
  60. package/tests/commits.spec.ts +67 -0
  61. package/tests/context.ts +301 -0
  62. package/tests/events.spec.ts +95 -0
  63. package/tests/facade.spec.ts +83 -0
  64. package/tests/feed.spec.ts +39 -0
  65. package/tests/stores.spec.ts +67 -0
  66. package/tests/tsconfig.json +17 -0
  67. 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"}
@@ -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"}