@owlmeans/client-planning 0.1.18-rc.8 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -11
- package/agent-meta/manifest.json +62 -4
- package/agent-meta/skills/client-planning/SKILL.md +60 -78
- package/agent-meta/skills/client-planning/parts/aux-mirrors.md +61 -0
- package/agent-meta/skills/client-planning/parts/mirror.md +67 -0
- package/agent-meta/skills/client-planning/parts/validation.md +60 -0
- package/build/client.d.ts +1 -1
- package/build/client.d.ts.map +1 -1
- package/build/client.js +66 -14
- package/build/client.js.map +1 -1
- package/build/commits.d.ts +2 -13
- package/build/commits.d.ts.map +1 -1
- package/build/commits.js +190 -132
- package/build/commits.js.map +1 -1
- package/build/consts.d.ts +4 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +10 -4
- package/build/consts.js.map +1 -1
- package/build/definitions.d.ts +13 -0
- package/build/definitions.d.ts.map +1 -0
- package/build/definitions.js +70 -0
- package/build/definitions.js.map +1 -0
- package/build/events/types.d.ts +34 -0
- package/build/events/types.d.ts.map +1 -0
- package/build/events/types.js +2 -0
- package/build/events/types.js.map +1 -0
- package/build/events.d.ts +10 -21
- package/build/events.d.ts.map +1 -1
- package/build/events.js +104 -102
- package/build/events.js.map +1 -1
- package/build/facade.d.ts.map +1 -1
- package/build/facade.js +86 -49
- package/build/facade.js.map +1 -1
- package/build/feed.d.ts +4 -4
- package/build/feed.d.ts.map +1 -1
- package/build/feed.js +50 -27
- package/build/feed.js.map +1 -1
- package/build/helper/types.d.ts +15 -0
- package/build/helper/types.d.ts.map +1 -0
- package/build/helper/types.js +2 -0
- package/build/helper/types.js.map +1 -0
- package/build/helper.d.ts +7 -7
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +16 -8
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6 -0
- package/build/index.js.map +1 -1
- package/build/lifecycle/types.d.ts +27 -0
- package/build/lifecycle/types.d.ts.map +1 -0
- package/build/lifecycle/types.js +2 -0
- package/build/lifecycle/types.js.map +1 -0
- package/build/lifecycle.d.ts +3 -0
- package/build/lifecycle.d.ts.map +1 -0
- package/build/lifecycle.js +157 -0
- package/build/lifecycle.js.map +1 -0
- package/build/resource-feed.d.ts +5 -0
- package/build/resource-feed.d.ts.map +1 -0
- package/build/resource-feed.js +97 -0
- package/build/resource-feed.js.map +1 -0
- package/build/resources.d.ts +6 -0
- package/build/resources.d.ts.map +1 -0
- package/build/resources.js +93 -0
- package/build/resources.js.map +1 -0
- package/build/stores.d.ts +4 -11
- package/build/stores.d.ts.map +1 -1
- package/build/stores.js +11 -35
- package/build/stores.js.map +1 -1
- package/build/sync/types.d.ts +18 -0
- package/build/sync/types.d.ts.map +1 -0
- package/build/sync/types.js +2 -0
- package/build/sync/types.js.map +1 -0
- package/build/sync.d.ts +4 -0
- package/build/sync.d.ts.map +1 -0
- package/build/sync.js +26 -0
- package/build/sync.js.map +1 -0
- package/build/types.d.ts +48 -3
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +13 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/record/types.d.ts +28 -0
- package/build/utils/record/types.d.ts.map +1 -0
- package/build/utils/record/types.js +2 -0
- package/build/utils/record/types.js.map +1 -0
- package/build/utils/record.d.ts +3 -24
- package/build/utils/record.d.ts.map +1 -1
- package/build/utils/record.js +64 -70
- package/build/utils/record.js.map +1 -1
- package/build/validation/consts.d.ts +23 -0
- package/build/validation/consts.d.ts.map +1 -0
- package/build/validation/consts.js +289 -0
- package/build/validation/consts.js.map +1 -0
- package/build/validation/types.d.ts +17 -0
- package/build/validation/types.d.ts.map +1 -0
- package/build/validation/types.js +2 -0
- package/build/validation/types.js.map +1 -0
- package/build/validation.d.ts +10 -0
- package/build/validation.d.ts.map +1 -0
- package/build/validation.js +171 -0
- package/build/validation.js.map +1 -0
- package/package.json +20 -18
- package/src/client.ts +58 -15
- package/src/commits.ts +152 -184
- package/src/consts.ts +12 -4
- package/src/definitions.ts +93 -0
- package/src/events/types.ts +35 -0
- package/src/events.ts +118 -108
- package/src/facade.ts +87 -55
- package/src/feed.ts +47 -28
- package/src/helper/types.ts +15 -0
- package/src/helper.ts +27 -10
- package/src/index.ts +12 -0
- package/src/lifecycle/types.ts +28 -0
- package/src/lifecycle.ts +123 -0
- package/src/resource-feed.ts +79 -0
- package/src/resources.ts +84 -0
- package/src/stores.ts +11 -40
- package/src/sync/types.ts +18 -0
- package/src/sync.ts +40 -0
- package/src/types.local.ts +14 -0
- package/src/types.ts +53 -5
- package/src/utils/record/types.ts +27 -0
- package/src/utils/record.ts +68 -71
- package/src/validation/consts.ts +289 -0
- package/src/validation/types.ts +18 -0
- package/src/validation.ts +140 -0
- package/tests/app.spec.ts +72 -0
- package/tests/context.ts +46 -15
- package/tests/csp.spec.ts +119 -0
- package/tests/definitions.spec.ts +74 -0
- package/tests/draft7.spec.ts +13 -0
- package/tests/events.spec.ts +13 -9
- package/tests/facade.spec.ts +2 -1
- package/tests/lifecycle/context.ts +29 -0
- package/tests/lifecycle-close.spec.ts +103 -0
- package/tests/lifecycle-commits.spec.ts +92 -0
- package/tests/lifecycle-definitions.spec.ts +28 -0
- package/tests/lifecycle-feeds.spec.ts +92 -0
- package/tests/lifecycle-sockets.spec.ts +76 -0
- package/tests/replies.spec.ts +81 -0
- package/tests/resources.spec.ts +116 -0
- package/tests/stores.spec.ts +13 -11
- package/tests/validation.spec.ts +127 -0
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# @owlmeans/client-planning
|
|
2
2
|
|
|
3
3
|
The client half of OwlMeans planning: a `PlanningFacade` over the protocol tree a server mounted
|
|
4
|
-
with [`@owlmeans/server-planning`](../server-planning),
|
|
5
|
-
commits, and a commit wait that subscribes first and then long-polls. It is React-free, so the same
|
|
4
|
+
with [`@owlmeans/server-planning`](../server-planning), optional state mirrors of cards, links,
|
|
5
|
+
commits, assignees, teams, comments and mentions, and a commit wait that subscribes first and then long-polls. It is React-free, so the same
|
|
6
6
|
code runs in a browser app and in a Node client. Use `@owlmeans/server-planning` instead wherever the
|
|
7
7
|
planning service runs in-process.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
bun add @owlmeans/client-planning@^0.1.18
|
|
12
|
+
bun add @owlmeans/client-planning@^0.1.18
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Concepts
|
|
@@ -17,14 +17,46 @@ bun add @owlmeans/client-planning@^0.1.18-rc.8
|
|
|
17
17
|
- **Remote facade** — the same `PlanningFacade` interface the server answers; every method is one
|
|
18
18
|
entrypoint call, and `modelOf(record, facade)` models behave identically on both sides.
|
|
19
19
|
- **One id space** — projects, cards and specifications share ONE card store; a list reaches it
|
|
20
|
-
through `syncCards(store, items, where)`, never `replace()`.
|
|
20
|
+
through `syncHelper.syncCards(store, items, where)`, never `replace()`.
|
|
21
21
|
- **Commit** — `execute()` returns a receipt for a pending transition; `receipt.committed()` /
|
|
22
22
|
`commits.wait()` resolve with the folded card, or throw `CommitTimeout` / `CommitFailed`.
|
|
23
23
|
- **Socket seam** — the commit socket is opened by an injected `PlanningSocketOpener`; without one
|
|
24
24
|
the client long-polls.
|
|
25
|
-
- **`head > seq`** — a card with a transition allocated and not yet folded; `applyReceipt` marks it
|
|
25
|
+
- **`head > seq`** — a card with a transition allocated and not yet folded; the mirror's `applyReceipt` marks it
|
|
26
26
|
the moment the server answers.
|
|
27
27
|
|
|
28
|
+
## Forms under a strict Content Security Policy
|
|
29
|
+
|
|
30
|
+
Use the exact resolved card or assignee type's field schema:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { makePlanningFieldValidator } from '@owlmeans/client-planning'
|
|
34
|
+
|
|
35
|
+
const form = makePlanningFieldValidator(resolvedType.fields)
|
|
36
|
+
const checked = form.validateFields(fields)
|
|
37
|
+
if (checked.valid) {
|
|
38
|
+
// Submit this same fields object through the stock planning facade.
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The runtime interpreter supports dynamic organization and project definitions without `eval` or
|
|
43
|
+
`new Function`. It preserves the schema and payload, nested objects, required fields, enum/const,
|
|
44
|
+
dates, references, compositions and AJV nullable semantics; string formats use native
|
|
45
|
+
`ajv-formats` full-mode predicates. Errors contain field `path`, `keyword`, `schemaPath` and a
|
|
46
|
+
bounded message without submitted values. The server's authoritative AJV validates every write.
|
|
47
|
+
|
|
48
|
+
The schema contract is Draft 7 plus `$defs` and `nullable`, with native AJV reference-sibling
|
|
49
|
+
semantics. References must resolve within the supplied schema. Malformed schemas, missing
|
|
50
|
+
references, same-instance reference cycles, other drafts/custom validation keywords, unknown or
|
|
51
|
+
numeric formats, format-limit extensions and `multipleOf` throw `SchemaInvalid` at preparation.
|
|
52
|
+
The interpreter's floating tolerance and native AJV's multiple semantics differ, so that keyword
|
|
53
|
+
is explicitly refused. JSON fields cannot contain dot/dollar keys or non-JSON values. Standard
|
|
54
|
+
annotations and `x-` metadata remain annotations. At most 64 validation errors are returned.
|
|
55
|
+
|
|
56
|
+
Surface preparation errors and keep save disabled. Do not remove constraints, choose another
|
|
57
|
+
schema or relax CSP. Registry `validator` / `assigneeValidator` compile with native AJV and need
|
|
58
|
+
code generation; browser forms under strict CSP use this API instead.
|
|
59
|
+
|
|
28
60
|
## Usage
|
|
29
61
|
|
|
30
62
|
Wire the client into a context with the server's own tree:
|
|
@@ -36,9 +68,14 @@ import { appendPlanningClient, appendPlanningStores } from '@owlmeans/client-pla
|
|
|
36
68
|
const planningProtocols = makePlanningProtocols({
|
|
37
69
|
base: { alias: 'app:api:planning', path: '/planning', service: API },
|
|
38
70
|
guards: DEFAULT_GUARD,
|
|
71
|
+
resources: true,
|
|
72
|
+
definitions: true,
|
|
39
73
|
})
|
|
40
74
|
|
|
41
|
-
appendPlanningClient(context, {
|
|
75
|
+
appendPlanningClient(context, {
|
|
76
|
+
protocols: planningProtocols, poll: 20,
|
|
77
|
+
scopeKey: () => `${currentEntitySlug}:${currentSessionId}`,
|
|
78
|
+
})
|
|
42
79
|
appendPlanningStores(context)
|
|
43
80
|
```
|
|
44
81
|
|
|
@@ -77,18 +114,61 @@ Give a browser its commit socket through the host's own opener:
|
|
|
77
114
|
```typescript
|
|
78
115
|
appendPlanningClient(context, {
|
|
79
116
|
protocols: planningProtocols,
|
|
80
|
-
socket: async (protocol, request) => await ws(context.entrypoint(protocol), request),
|
|
117
|
+
socket: async (protocol, request) => await socketClientHelper.ws(context.entrypoint(protocol), request),
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Auxiliary resources and session lifecycle
|
|
122
|
+
|
|
123
|
+
The remote facade supports the same `assignees`, `teams`, `comments` and `mentions` methods as
|
|
124
|
+
the server, including compare-and-set writes, canonical team membership/project attachment,
|
|
125
|
+
effective project assignees and mention-cache rebuilding. `definitions` supports assignee types
|
|
126
|
+
when the server mounts writable schemas. Relationships include endpoint resource kinds and named
|
|
127
|
+
inverse views; models resolve schemas through their nearest primary project.
|
|
128
|
+
|
|
129
|
+
`appendPlanningStores` registers four auxiliary mirrors beside cards, links and commits. Reads
|
|
130
|
+
and writes through the facade update those mirrors without replacing newer versions. A resource
|
|
131
|
+
feed refreshes the requested subsets through the stock API:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { makePlanningResourceFeed } from '@owlmeans/client-planning'
|
|
135
|
+
|
|
136
|
+
const resources = makePlanningResourceFeed(context, {
|
|
137
|
+
assignees: { retired: false, size: 0 }, teams: { size: 0 },
|
|
138
|
+
comments: { card: cardId, size: 0 }, mentions: { card: cardId, size: 0 },
|
|
139
|
+
refresh: 5_000,
|
|
81
140
|
})
|
|
141
|
+
await resources.ready
|
|
142
|
+
// Read context.planningStores().assignees, teams, comments and mentions.
|
|
143
|
+
await resources.stop()
|
|
82
144
|
```
|
|
83
145
|
|
|
146
|
+
`scopeKey` identifies the current organization slug and authenticated session. A changed key
|
|
147
|
+
cancels old requests and feeds, drains local mutations and clears mirrors; capture a new facade
|
|
148
|
+
and feed for the new session. `await context.planning().close()` releases feeds, subscriptions,
|
|
149
|
+
commit waits and sockets, including carriers that open late. Stopped requests cannot repopulate
|
|
150
|
+
cleared mirrors, even when a transport ignores cancellation.
|
|
151
|
+
|
|
152
|
+
The server derives authorization scope from credentials. Wire records omit native organization
|
|
153
|
+
`entityId`; the client hydrates empty advisory metadata for the shared record shape. Never use
|
|
154
|
+
that placeholder to authorize a request, and never send an organization record id on the wire.
|
|
155
|
+
|
|
84
156
|
## API
|
|
85
157
|
|
|
86
158
|
- `appendPlanningClient(context, options)`, `makePlanningClientService(context, options)`
|
|
87
159
|
- `makeRemoteFacade(context, protocols, scope, opts)`, `makeRemoteCommitSource(context, protocols, opts?)`
|
|
88
|
-
- `appendPlanningStores(context, aliases?)
|
|
89
|
-
- `
|
|
160
|
+
- `appendPlanningStores(context, aliases?)`
|
|
161
|
+
- `syncHelper` — `.syncCards(store, items, where?, opts?)`, `.syncLinks(store, items, where?, opts?)`
|
|
162
|
+
- `planningMirrorOf(stores)` (`makePlanningMirror`) — `.applyCommitEvent(event, facade?)`,
|
|
163
|
+
`.applyReceipt(view)`, `.applyCards(cards)`
|
|
90
164
|
- `makePlanningFeed(context, opts?)`
|
|
91
|
-
- `
|
|
165
|
+
- `makePlanningResourceFeed(context, opts?)`, `PlanningResourceFeedOptions`
|
|
166
|
+
- `makePlanningFieldValidator(schema)`, `PlanningFieldValidator`, `PlanningFieldValidation`, `PlanningFieldError`
|
|
167
|
+
- `makePlanningClientLifecycle`, `PlanningClientLifecycle`; service `close()` and `scopeKey`
|
|
168
|
+
- `planningContextOf(context)` (`makePlanningContextHelper`) — `.facade(scope?)`, `.model(card, scope?)`,
|
|
169
|
+
`.stores()`
|
|
170
|
+
- The former plain functions (`syncCards`, `applyCards`, `planningOf`, …) remain as deprecated
|
|
171
|
+
delegates.
|
|
92
172
|
- `CARDS`, `LINKS`, `COMMITS`, `DEFAULT_STORE_ALIASES`, `LONG_POLL_GRACE`, `EARLY_POLL_LADDER`, `EARLY_POLL_MS`
|
|
93
173
|
- Types: `PlanningClientOptions`, `PlanningClientService`, `WithPlanningClient`,
|
|
94
174
|
`PlanningSocketOpener`, `RemoteCommitSource`, `RemoteCommitSourceOptions`, `RemoteFacadeOptions`,
|
|
@@ -103,6 +183,9 @@ appendPlanningClient(context, {
|
|
|
103
183
|
- `CommitTimeout` leaves the transition pending — re-wait or re-read, do not re-execute without a `key`.
|
|
104
184
|
- A client refuses `store()`, `committed()` and plugins carrying hooks with `PlanningUnsupported`.
|
|
105
185
|
- Mount one feed per screen; every other reader uses the store.
|
|
186
|
+
- Auxiliary updates/removals require `{ version: record.version }`. Their `fields` replace the
|
|
187
|
+
previous object; card `changes.fields` merge.
|
|
188
|
+
- After a scope/session change, acquire new facades and feeds. Old ones are deliberately stale.
|
|
106
189
|
|
|
107
190
|
## Related packages
|
|
108
191
|
|
|
@@ -119,7 +202,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
119
202
|
your project's skill store (`.agents/skills/`):
|
|
120
203
|
|
|
121
204
|
```sh
|
|
122
|
-
npx @owlmeans/agent-skills@^0.1.18
|
|
205
|
+
npx @owlmeans/agent-skills@^0.1.18
|
|
123
206
|
```
|
|
124
207
|
|
|
125
208
|
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
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/client-planning",
|
|
4
|
-
"version": "0.1.18
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18",
|
|
5
|
+
"generatedAt": "2026-10-28T00:31:34.741Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -10,7 +10,65 @@
|
|
|
10
10
|
"name": "client-planning",
|
|
11
11
|
"category": "package-specific",
|
|
12
12
|
"file": "skills/client-planning/SKILL.md",
|
|
13
|
-
"canonicalPath": ".agents/skills/client-planning/SKILL.md"
|
|
13
|
+
"canonicalPath": ".agents/skills/client-planning/SKILL.md",
|
|
14
|
+
"parts": [
|
|
15
|
+
{
|
|
16
|
+
"name": "aux-mirrors",
|
|
17
|
+
"file": "skills/client-planning/parts/aux-mirrors.md",
|
|
18
|
+
"description": "The assignee, team, comment and mention mirrors and resource feeds, and the authenticated scope lifecycle (scopeKey, close, lifecycle.run and lifecycle.mutate).",
|
|
19
|
+
"purposes": [
|
|
20
|
+
"state",
|
|
21
|
+
"auth"
|
|
22
|
+
],
|
|
23
|
+
"mentions": [
|
|
24
|
+
"makePlanningResourceFeed",
|
|
25
|
+
"scopeKey",
|
|
26
|
+
"lifecycle.run",
|
|
27
|
+
"lifecycle.mutate",
|
|
28
|
+
"registerFeed",
|
|
29
|
+
"client:scope-changed",
|
|
30
|
+
"PlanningReply"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "mirror",
|
|
35
|
+
"file": "skills/client-planning/parts/mirror.md",
|
|
36
|
+
"description": "The state mirror's one id space and fold rules, makePlanningFeed, and optimistic writes with head over seq.",
|
|
37
|
+
"purposes": [
|
|
38
|
+
"state",
|
|
39
|
+
"list"
|
|
40
|
+
],
|
|
41
|
+
"mentions": [
|
|
42
|
+
"appendPlanningStores",
|
|
43
|
+
"planningMirrorOf",
|
|
44
|
+
"applyCommitEvent",
|
|
45
|
+
"applyReceipt",
|
|
46
|
+
"applyCards",
|
|
47
|
+
"syncHelper",
|
|
48
|
+
"syncCards",
|
|
49
|
+
"syncLinks",
|
|
50
|
+
"makePlanningFeed",
|
|
51
|
+
"PlanningFeed",
|
|
52
|
+
"useStoreList"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "validation",
|
|
57
|
+
"file": "skills/client-planning/parts/validation.md",
|
|
58
|
+
"description": "CSP-safe browser validation of a served field schema with makePlanningFieldValidator — the supported keywords, refusals and limits, and the external references behind them.",
|
|
59
|
+
"purposes": [
|
|
60
|
+
"form"
|
|
61
|
+
],
|
|
62
|
+
"mentions": [
|
|
63
|
+
"makePlanningFieldValidator",
|
|
64
|
+
"validateFields",
|
|
65
|
+
"@cfworker/json-schema",
|
|
66
|
+
"JSON_SCHEMA_DRAFT_07",
|
|
67
|
+
"unsafe-eval",
|
|
68
|
+
"SchemaInvalid"
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
]
|
|
14
72
|
}
|
|
15
73
|
]
|
|
16
74
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: client-planning
|
|
3
|
-
description: How to use @owlmeans/client-planning — appendPlanningClient for the remote planning facade, appendPlanningStores and
|
|
3
|
+
description: How to use @owlmeans/client-planning — appendPlanningClient for the remote planning facade, appendPlanningStores and planningMirrorOf 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
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/client-planning
|
|
9
9
|
|
|
10
10
|
**Layer:** Client
|
|
11
|
-
**Install:** `"@owlmeans/client-planning": "^0.1.18
|
|
11
|
+
**Install:** `"@owlmeans/client-planning": "^0.1.18"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
The client half of OwlMeans planning. It answers the `PlanningFacade` interface of
|
|
14
14
|
`@owlmeans/planning` over the protocol tree a server mounted with `@owlmeans/server-planning`, keeps
|
|
@@ -24,13 +24,15 @@ CLI). React hooks over the mirror are `useStoreModel` / `useStoreList` from `@ow
|
|
|
24
24
|
| `makePlanningClientService(context, options)` | The service itself, for a host that registers it on its own |
|
|
25
25
|
| `makeRemoteFacade(context, protocols, scope, opts)` | One facade — every method is one entrypoint call |
|
|
26
26
|
| `makeRemoteCommitSource(context, protocols, opts?)` | `status` / `subscribe` / `wait` over `commit.get` and `commit.events` |
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
27
|
+
| `makeRemoteDefinitions(context, protocols, opts?)` | Data-defined types and flows over a tree declared with `definitions: true` |
|
|
28
|
+
| `appendPlanningStores(context, aliases?)` / `planningContextOf(context).stores()` | The state mirror, and a lookup that answers `null` without one |
|
|
29
|
+
| `syncHelper.syncCards(store, items, where?, opts?)` / `syncHelper.syncLinks(...)` | Make the mirror agree with a list WITHIN a scope |
|
|
30
|
+
| `planningMirrorOf(stores).applyCommitEvent(event, facade?)` / `.applyReceipt(view)` / `.applyCards(cards)` | The folds |
|
|
30
31
|
| `makePlanningFeed(context, opts?)` | Subscribe, seed, fold, refresh — `{ connected, seeded, error, ready, refresh, stop }` |
|
|
31
|
-
| `
|
|
32
|
+
| `planningContextOf(context).facade(scope?)` / `.model(card, scope?)` | The facade / a model with the schemas loaded |
|
|
33
|
+
| `makePlanningFieldValidator(schema)` | CSP-safe validation of an exact served field schema; `{ validateFields }` |
|
|
32
34
|
| `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` |
|
|
35
|
+
| Types | `PlanningClientOptions`, `PlanningClientService`, `WithPlanningClient`, `PlanningSocketOpener`, `RemoteCommitSource`, `RemoteDefinitions`, `RemoteDefinitionsOptions`, `PlanningStores`, `PlanningStoreAliases`, `WithPlanningStores`, `PlanningCommitRecord`, `PlanningFeed`, `PlanningFeedOptions`, `PlanningFeedState`, `SyncOptions`, `PlanningContextHelper`, `PlanningMirror`, `SyncHelper` |
|
|
34
36
|
|
|
35
37
|
## Wiring
|
|
36
38
|
|
|
@@ -59,36 +61,66 @@ appendPlanningStores(context) // a browser mirror; a Node client usually
|
|
|
59
61
|
- `bind: false` when the host already bound the tree.
|
|
60
62
|
- The service answers under `PLANNING_SERVICE`, the alias the server's service uses, so
|
|
61
63
|
`context.planning().for(scope)` is the same call in a handler and in a screen. The scope is
|
|
62
|
-
**advisory
|
|
63
|
-
`actor` is never sent.
|
|
64
|
+
**advisory** — it changes nothing the server decides: the server takes the entity and the actor
|
|
65
|
+
from the credential. The execution's own `actor` is never sent.
|
|
64
66
|
- A client runs no middleware and folds nothing: `store()`, `committed()` and a plugin carrying
|
|
65
67
|
`before`/`after`/`store`/`mintCode` answer `PlanningUnsupported`. `use()` accepts a schemas-only
|
|
66
|
-
plugin, layered over the server's bundle.
|
|
68
|
+
plugin, layered over the server's bundle; its `schemas.app` merges over the served declarations
|
|
69
|
+
(`bundle.app`) in plugin order, and a malformed one is refused (`SchemaInvalid`) before anything
|
|
70
|
+
changes.
|
|
67
71
|
- `timeout` (ms) bounds every call that is not a long poll; a long poll's deadline is its own hold
|
|
68
72
|
plus `LONG_POLL_GRACE`.
|
|
69
73
|
|
|
70
74
|
### The schema bundle
|
|
71
75
|
|
|
72
76
|
`model()` needs the flows and types (`can()`, `available()`, `statusOf()` read the registry with no
|
|
73
|
-
round trip). The bundle
|
|
77
|
+
round trip). The bundle also carries the application's declarations (`registry.app()`), so
|
|
78
|
+
`registry.fieldsSchema(type)` / `validator(type)` include its base card fields and a definition's
|
|
79
|
+
`app` bag reads back as written. The bundle is fetched in the background once the context is **Ready** and a failure
|
|
74
80
|
there is swallowed — a browser that is not signed in yet cannot read it, and a context must never
|
|
75
81
|
fail or wait over it. `model()` and `service.loadSchemas()` load it on first use regardless, and a
|
|
76
82
|
failed load is retried by the next caller. `schemas: false` skips the background load.
|
|
77
83
|
|
|
84
|
+
### Data-defined types and flows
|
|
85
|
+
|
|
86
|
+
A tree declared with `definitions: true` (the server's store holds data-defined schemas) gives the
|
|
87
|
+
service and every facade `definitions`: `registry(project?)` / `bundle(project?)` read
|
|
88
|
+
`schema.list?project=` and are cached per project; `putType`/`putFlow` (compare-and-set on the
|
|
89
|
+
declaration's `version`), `define`, `seed` and `retire` go through `schema.define` and drop the
|
|
90
|
+
whole cache — an organization-wide write reaches every project's layer — and make the next
|
|
91
|
+
`model()` reload the service's own bundle. `records` is the server's alone (`PlanningUnsupported`).
|
|
92
|
+
`model(card)` of a card resolves its type in its project's layer (a project's own id, a card's
|
|
93
|
+
nearest primary project ancestor); a specification's type is always code's. A tree without `definitions` has none of this.
|
|
94
|
+
|
|
95
|
+
## Mounting in a target
|
|
96
|
+
|
|
97
|
+
The web half of the target mount (`planning` → Mounting in a target, in that skill's `mounting` part).
|
|
98
|
+
|
|
99
|
+
- `appendPlanningClient(context, { protocols, bind: false, schemas: false })` — the target binds
|
|
100
|
+
its whole api tree itself, and a signed-out visitor may read nothing, so the bundle loads on
|
|
101
|
+
first use — plus `appendPlanningStores(context)`. No `socket`: the target serves no commit socket,
|
|
102
|
+
so `commits.wait` long-polls.
|
|
103
|
+
- Screens read the mirror with `useStoreList` / `useStoreModel` (`@owlmeans/client`), never a
|
|
104
|
+
fetched array kept in component state; lists reach the store through `syncHelper.syncCards` or
|
|
105
|
+
`makePlanningFeed` (with `refresh`), NEVER `replace()` — the one card store holds every kind.
|
|
106
|
+
- Every write is `facade.execute(...)` with a `key` (a retried click or a double submit answers
|
|
107
|
+
the first receipt) and `{ wait: true }` where the screen shows the result.
|
|
108
|
+
- Never call a route with an empty id; a picker offers no archived parent (`intrinsic: closed`).
|
|
109
|
+
|
|
78
110
|
## Reading
|
|
79
111
|
|
|
80
112
|
```typescript
|
|
81
113
|
const planning = context.planning().for()
|
|
82
114
|
|
|
83
|
-
const
|
|
84
|
-
const board = await planning.cards.summary([
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
115
|
+
const tools = await planning.cards.list({ parent: shedId, type: 'shed:tool', sort: ['order'], size: 0 })
|
|
116
|
+
const board = await planning.cards.summary([shedId])
|
|
117
|
+
const rules = await planning.specifications.current(shedId, 'house-rules')
|
|
118
|
+
const drill = await planning.model(drillId)
|
|
119
|
+
drill.available().map(rule => rule.name)
|
|
88
120
|
```
|
|
89
121
|
|
|
90
122
|
- 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.
|
|
123
|
+
(`wireHelper.encodeWorkcardQuery` and its twins). Never pass a wire shape to the facade.
|
|
92
124
|
- `cards.load(id)` answers `null` for a missing card — and for another entity's card, which the
|
|
93
125
|
server deliberately reports the same way.
|
|
94
126
|
- `cards.count(query)` is a one-row list read for its `total`; the tree declares no count route.
|
|
@@ -96,54 +128,6 @@ story.available().map(rule => rule.name)
|
|
|
96
128
|
project-wide read answers `PlanningUnsupported` on a client.
|
|
97
129
|
- `size: 0` is "no limit"; a paged server answers 100 rows when no size is asked for.
|
|
98
130
|
|
|
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
131
|
## Waiting for a commit
|
|
148
132
|
|
|
149
133
|
`execute()` is asynchronous: the receipt names a PENDING transition until the store folds it.
|
|
@@ -179,24 +163,12 @@ service; releasing a subscription never closes it — `context.planning().close(
|
|
|
179
163
|
|
|
180
164
|
A Node client passes no opener and relies on the long poll.
|
|
181
165
|
|
|
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
166
|
## Gotchas
|
|
192
167
|
|
|
193
168
|
- 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
169
|
- A refusal crosses the hop as its class (`IllegalTransition`, `WorkcardConflict`,
|
|
196
170
|
`FieldsInvalid`, a plugin's own class) — branch on `instanceof`, never on message text.
|
|
197
171
|
- `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
172
|
|
|
201
173
|
## Depends On
|
|
202
174
|
|
|
@@ -209,4 +181,14 @@ gets `WorkcardConflict` — reload and retry.
|
|
|
209
181
|
- `planning` — records, flows, the fold, the models
|
|
210
182
|
- `server-planning` — the handlers this addresses, the executor, the commit hub
|
|
211
183
|
- `client-job` — the same seed-then-fold shape for safe application-job views
|
|
212
|
-
- `state` — `syncCards` is built on its criteria engine and `purge`
|
|
184
|
+
- `state` — `syncHelper.syncCards` is built on its criteria engine and `purge`
|
|
185
|
+
|
|
186
|
+
## Parts
|
|
187
|
+
|
|
188
|
+
Load a part on top of this core when its job is in play.
|
|
189
|
+
|
|
190
|
+
| Part | Load it when |
|
|
191
|
+
|---|---|
|
|
192
|
+
| [validation](parts/validation.md) | Validating planning `fields` in a browser form under a strict CSP (`makePlanningFieldValidator`) |
|
|
193
|
+
| [mirror](parts/mirror.md) | Reading lists from the state mirror (`syncHelper`, `planningMirrorOf`), mounting `makePlanningFeed`, or relying on `head > seq` |
|
|
194
|
+
| [aux-mirrors](parts/aux-mirrors.md) | Assignee, team, comment or mention mirrors and feeds, or switching organization / signing out (`scopeKey`, `close()`) |
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aux-mirrors
|
|
3
|
+
description: The assignee, team, comment and mention mirrors and resource feeds, and the authenticated scope lifecycle (scopeKey, close, lifecycle.run and lifecycle.mutate).
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: state, auth
|
|
6
|
+
mentions: makePlanningResourceFeed, scopeKey, lifecycle.run, lifecycle.mutate, registerFeed, client:scope-changed, PlanningReply
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/client-planning — Auxiliary mirrors and the scope lifecycle
|
|
11
|
+
|
|
12
|
+
## Auxiliary mirrors and authenticated scope lifecycle
|
|
13
|
+
|
|
14
|
+
With `resources: true` in the shared protocol tree, every remote facade exposes the complete
|
|
15
|
+
assignee/team/comment/mention API, including versioned writes, team membership and project
|
|
16
|
+
attachment. `appendPlanningStores` registers separate stock typed mirrors for all four resources.
|
|
17
|
+
`makePlanningResourceFeed` seeds and polls selected resource queries; use it with the stock stores
|
|
18
|
+
and `useStoreList` / `useStoreModel`. Auxiliary writes update the mirror only after success.
|
|
19
|
+
An active-assignee query (`retired: false`) includes an absent or null retirement flag. Local
|
|
20
|
+
mirror queries and feed reconciliation use `{ retired: { $ne: true } }`, matching the native
|
|
21
|
+
facade rather than strict equality to false. A filtered poll removes only its unchanged matching
|
|
22
|
+
snapshot rows, preserving unrelated retired identities needed by existing references.
|
|
23
|
+
Comments use trusted server authors; mentions are read-only derived cache commands.
|
|
24
|
+
|
|
25
|
+
Supply `PlanningClientOptions.scopeKey` from authenticated organization entity slug, profile/user
|
|
26
|
+
id, session id and authorization revision; return undefined when signed out. Before switching
|
|
27
|
+
organization or signing out, **await** `service.close()` before adopting the credential. It cancels
|
|
28
|
+
requests and commit waits, invalidates receipts (including already-settled receipts), stops every
|
|
29
|
+
registered feed, removes socket subscriptions, closes the carrier and rejects late socket openers.
|
|
30
|
+
A stale operation answers `client:scope-changed`; cancellation never rolls back an accepted server
|
|
31
|
+
write. A new subscription opens a fresh carrier after close.
|
|
32
|
+
|
|
33
|
+
Close drains admitted **local** mirror writes before clearing every stock store and schema cache.
|
|
34
|
+
New calls during an explicit close are refused, including hosts without `scopeKey`. Concurrent
|
|
35
|
+
closes share the boundary. If clearing fails, close rejects and fresh work stays blocked; retain
|
|
36
|
+
the old credential and retry close explicitly. An observed identity change also cancels old work
|
|
37
|
+
and queues clearing before the new identity's mirror writes. Reload schemas and create fresh feeds
|
|
38
|
+
after credential adoption. Feed query scope alone is insufficient for this lifecycle.
|
|
39
|
+
|
|
40
|
+
The service's `lifecycle` coordinates stock facades, receipts, feeds and schema writes. A custom
|
|
41
|
+
mirror integration captures an operation with `lifecycle.run`, performs remote reads through
|
|
42
|
+
`operation.wait` with `operation.signal`, then uses `lifecycle.mutate(operation, write)` for local
|
|
43
|
+
writes; never hold or nest that queue over a request, socket opener or another queued mutation.
|
|
44
|
+
`planningMirrorOf(stores)` accepts `{ lifecycle, operation }` as its optional last argument and
|
|
45
|
+
fetches missing frame records/links before entering the queue. Direct mirror/sync helpers without
|
|
46
|
+
that option are local primitives: their caller owns isolation and cleanup.
|
|
47
|
+
|
|
48
|
+
Planning protocols return `PlanningReply<T>` without storage organization identifiers. Stock
|
|
49
|
+
facades hydrate records, receipts, schema scopes and socket frames with `entityId: ''` only to
|
|
50
|
+
preserve the shared facade shape. That value is an advisory placeholder, never an organization
|
|
51
|
+
identity or a credential. Use `scopeKey` and the authenticated organization entity slug for
|
|
52
|
+
identity changes and access isolation; native server records keep their verified stable scope.
|
|
53
|
+
|
|
54
|
+
Resource feed reconciliation snapshots mirror versions before requesting a list. Only unchanged
|
|
55
|
+
snapshot rows absent from that response are removed, so a new local write or a newer version
|
|
56
|
+
survives an older in-flight poll. Stopped feeds cannot publish. Track custom feeds through
|
|
57
|
+
`registerFeed` when their lifecycle must end with the planning service. Card feeds use the same
|
|
58
|
+
identity fence and stop registration; component cleanup awaits `stop()` when it must observe a
|
|
59
|
+
completed boundary. Stop cancels unresolved seeds and drains admitted local changes without waiting
|
|
60
|
+
for the network; neither late callbacks nor authoritative deletion passes may publish afterward.
|
|
61
|
+
A schema write from an old generation cannot invalidate a newer generation's cached registry.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mirror
|
|
3
|
+
description: The state mirror's one id space and fold rules, makePlanningFeed, and optimistic writes with head over seq.
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: state, list
|
|
6
|
+
mentions: appendPlanningStores, planningMirrorOf, applyCommitEvent, applyReceipt, applyCards, syncHelper, syncCards, syncLinks, makePlanningFeed, PlanningFeed, useStoreList
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/client-planning — The mirror and the feed
|
|
11
|
+
|
|
12
|
+
## The mirror — one id space
|
|
13
|
+
|
|
14
|
+
`appendPlanningStores` registers ONE card store for every kind. Projects, cards and specifications
|
|
15
|
+
share an id space on the server, so they share one store here: a store per kind lets a card list
|
|
16
|
+
that reloads drop the projects beside it.
|
|
17
|
+
|
|
18
|
+
That is also why a list reaches the store through **`syncHelper.syncCards(store, items, where)`,
|
|
19
|
+
never `replace()`**: every card given is written, every card matching `where` that the list does not
|
|
20
|
+
name is dropped, and everything outside `where` is left alone. An unchanged card is not rewritten, so a
|
|
21
|
+
periodic re-seed wakes no subscriber. `opts.keep` protects ids a commit wrote while the list was in
|
|
22
|
+
flight.
|
|
23
|
+
|
|
24
|
+
The fold rules, shared by `syncHelper.syncCards`, the mirror's `applyCards` and `applyCommitEvent`, and
|
|
25
|
+
`commits.wait`:
|
|
26
|
+
|
|
27
|
+
- an OLDER `seq` never overwrites a newer record — frames and list answers race;
|
|
28
|
+
- a committed `delete` removes the row and every link touching it; a project's delete also drops
|
|
29
|
+
the rows whose `parents` hold it (the server purged them in the same fold);
|
|
30
|
+
- a frame for an id the store never saw still writes a row;
|
|
31
|
+
- a `failed` commit leaves the card alone and records the reason in the commit store;
|
|
32
|
+
- a frame without `record` (a cross-process bus carries ids only) is re-read through the facade —
|
|
33
|
+
without one, only the commit row is written;
|
|
34
|
+
- a committed `link`/`unlink` re-lists the card's outgoing links; links written together with a
|
|
35
|
+
`create` arrive with the next link seed.
|
|
36
|
+
|
|
37
|
+
## The feed
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const feed = makePlanningFeed(context, {
|
|
41
|
+
query: { parent: shedId, type: 'shed:tool' },
|
|
42
|
+
filter: { project: shedId },
|
|
43
|
+
refresh: 15_000,
|
|
44
|
+
onChange: state => setState(state),
|
|
45
|
+
})
|
|
46
|
+
await feed.ready // never rejects — read feed.error
|
|
47
|
+
// …
|
|
48
|
+
await feed.stop() // stops folding; the shared socket stays open
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
1. Subscribes to commits FIRST (the socket, when an opener exists).
|
|
52
|
+
2. Seeds: `cards.list` (unpaged unless the query pages) → `syncHelper.syncCards` over `where`
|
|
53
|
+
(`queryHelper.criteriaOf(query)` by default). A PAGED seed only writes — a page cannot say what
|
|
54
|
+
does not exist.
|
|
55
|
+
3. Folds every frame with `planningMirrorOf(stores).applyCommitEvent`, and re-seeds every `refresh`
|
|
56
|
+
ms — the authoritative backstop to a socket that can drop frames.
|
|
57
|
+
|
|
58
|
+
`connected` reports whether a socket was open when the feed subscribed; `seeded` tells "nothing
|
|
59
|
+
there" from "not loaded yet". A React hook wraps the feed in an effect and stops it on unmount.
|
|
60
|
+
|
|
61
|
+
## Optimistic writes and `head > seq`
|
|
62
|
+
|
|
63
|
+
`seq` is the last transition folded into a card, `head` the highest allocated. The mirror's
|
|
64
|
+
`applyReceipt` (run by `execute` whenever stores are registered) raises the stored card's `head` to
|
|
65
|
+
the new transition's `seq`, so `model.pending()` is true before any frame arrives; the commit's fold
|
|
66
|
+
brings `seq` up to it. The mirror only ever grows `head`, so a list fetched before the append does not
|
|
67
|
+
clear the marker. A model's `expectSeq` default and its `WorkcardConflict` are `planning` → Models.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: validation
|
|
3
|
+
description: CSP-safe browser validation of a served field schema with makePlanningFieldValidator — the supported keywords, refusals and limits, and the external references behind them.
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: form
|
|
6
|
+
mentions: makePlanningFieldValidator, validateFields, @cfworker/json-schema, JSON_SCHEMA_DRAFT_07, unsafe-eval, SchemaInvalid
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/client-planning — Browser field validation
|
|
11
|
+
|
|
12
|
+
## Browser field validation under CSP
|
|
13
|
+
|
|
14
|
+
Bind `makePlanningFieldValidator(registry.fieldsSchema(type))` to the registry of the resolved
|
|
15
|
+
organization or project bundle — the type's own `fields` plus the application's base card fields
|
|
16
|
+
(`planning` → types part); with no base fields it IS `type.fields`. Passing `type.fields` alone
|
|
17
|
+
skips the base fields the server still enforces. Its `validateFields(fields)` returns `{ valid, errors }`; errors
|
|
18
|
+
carry decoded `path`, `keyword`, `schemaPath` and a bounded value-free `message`. Construct one JSON
|
|
19
|
+
fields object, validate it and send that same object. No coercion, defaults or property removal
|
|
20
|
+
occurs, and the schema is left unchanged. Preserve nested money objects and calendar date strings.
|
|
21
|
+
The native server validates every write again.
|
|
22
|
+
|
|
23
|
+
The validator interprets schemas with `@cfworker/json-schema`, with no `eval`, `new Function` or
|
|
24
|
+
network reference loading. Building, loading and merging a registry is safe under a strict
|
|
25
|
+
`script-src` — `loadSchemas()`, `definitions.registry()`/`bundle()`, `model()`, `makeSchemaRegistry(bundle)`,
|
|
26
|
+
`scopedRegistryOf` — because a page runs only the closed-form checks of the served `app` declarations
|
|
27
|
+
(`planning` → types part). Do not call `.validator()`, `.assigneeValidator()`, `appSchemaHelper.assertBag`,
|
|
28
|
+
`scopedSchemaHelper.assert*Schema` or `validateHelper.*` in a browser with strict `script-src`: those
|
|
29
|
+
remain native AJV compilers, the server's.
|
|
30
|
+
The Draft 7 meta-schema is DATA in `src/validation/consts.ts` (`JSON_SCHEMA_DRAFT_07`, pinned to ajv's
|
|
31
|
+
`refs/json-schema-draft-07.json` by `tests/draft7.spec.ts`), never an import of that file: Node ESM needs
|
|
32
|
+
`with { type: 'json' }` on it, and a bundler that also meets ajv's own attribute-less `require` of the
|
|
33
|
+
same file refuses the pair (Rollup: "import attributes for the same module are always consistent"),
|
|
34
|
+
which fails every target web build that mounts the planning client.
|
|
35
|
+
Static AJV standalone compilation is suitable only when the schema cannot change after building;
|
|
36
|
+
organization and project definitions require the runtime interpreter.
|
|
37
|
+
|
|
38
|
+
The supported contract is Draft 7's object/array/scalar, required, enum/const, ranges, string
|
|
39
|
+
length/pattern, dependencies, boolean, composition and conditional keywords; `$defs`, local/nested
|
|
40
|
+
`$id` references and AJV's `nullable` are also supported. `$ref` sibling constraints are enforced as
|
|
41
|
+
by native planning AJV. String formats use the same `ajv-formats` full-mode predicates, including
|
|
42
|
+
real calendar dates and timezone-aware times. `nullable` adds null to the declared type while
|
|
43
|
+
leaving enum and const constraints intact. All references must resolve inside the supplied field
|
|
44
|
+
schema; productive recursive schemas are supported, same-instance reference cycles are refused.
|
|
45
|
+
Plain JSON values and field names without dot or dollar characters are required.
|
|
46
|
+
|
|
47
|
+
Preparation throws `SchemaInvalid` for malformed schemas, unresolved references, unsupported
|
|
48
|
+
drafts/keywords/formats, numeric formats, `formatMinimum`/`formatMaximum` extensions and
|
|
49
|
+
`multipleOf`. The latter is refused because the interpreter's epsilon and native AJV's quotient
|
|
50
|
+
semantics disagree for fractional and very large numbers. Standard annotation keywords and `x-`
|
|
51
|
+
annotations do not add validation. Never discard a refused constraint, invent a substitute schema,
|
|
52
|
+
enable `unsafe-eval` or proceed with a ready form after preparation fails. Show the error and retain
|
|
53
|
+
disabled save controls. Validation reports at most 64 errors, with paths bounded to 512 characters.
|
|
54
|
+
|
|
55
|
+
## External docs
|
|
56
|
+
|
|
57
|
+
- https://ajv.js.org/security.html#content-security-policy — runtime AJV requires unsafe-eval; standalone build compilation preserves strict CSP for static schemas.
|
|
58
|
+
- https://www.npmjs.com/package/@cfworker/json-schema/v/4.1.1 — the interpreter supports dynamic schemas without code generation; select an explicit vocabulary and check its known limitations (4.1.1).
|
|
59
|
+
- https://github.com/ajv-validator/ajv-formats#full-and-fast-validation-modes — full-mode predicates validate formats completely and are reusable through the plugin's `get` API (3.0.1).
|
|
60
|
+
- https://ajv.js.org/json-schema.html#nullable-openapi — nullable expands the declared type without adding null to enum or const.
|