@owlmeans/server-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 (174) hide show
  1. package/README.md +150 -0
  2. package/agent-meta/manifest.json +16 -0
  3. package/agent-meta/skills/server-planning/SKILL.md +203 -0
  4. package/build/actions/cards.d.ts +16 -0
  5. package/build/actions/cards.d.ts.map +1 -0
  6. package/build/actions/cards.js +32 -0
  7. package/build/actions/cards.js.map +1 -0
  8. package/build/actions/commit.d.ts +14 -0
  9. package/build/actions/commit.d.ts.map +1 -0
  10. package/build/actions/commit.js +48 -0
  11. package/build/actions/commit.js.map +1 -0
  12. package/build/actions/execute.d.ts +13 -0
  13. package/build/actions/execute.d.ts.map +1 -0
  14. package/build/actions/execute.js +35 -0
  15. package/build/actions/execute.js.map +1 -0
  16. package/build/actions/index.d.ts +9 -0
  17. package/build/actions/index.d.ts.map +1 -0
  18. package/build/actions/index.js +9 -0
  19. package/build/actions/index.js.map +1 -0
  20. package/build/actions/links.d.ts +5 -0
  21. package/build/actions/links.d.ts.map +1 -0
  22. package/build/actions/links.js +8 -0
  23. package/build/actions/links.js.map +1 -0
  24. package/build/actions/schemas.d.ts +6 -0
  25. package/build/actions/schemas.d.ts.map +1 -0
  26. package/build/actions/schemas.js +9 -0
  27. package/build/actions/schemas.js.map +1 -0
  28. package/build/actions/specs.d.ts +10 -0
  29. package/build/actions/specs.d.ts.map +1 -0
  30. package/build/actions/specs.js +14 -0
  31. package/build/actions/specs.js.map +1 -0
  32. package/build/actions/transitions.d.ts +6 -0
  33. package/build/actions/transitions.d.ts.map +1 -0
  34. package/build/actions/transitions.js +8 -0
  35. package/build/actions/transitions.js.map +1 -0
  36. package/build/actions/watch.d.ts +12 -0
  37. package/build/actions/watch.d.ts.map +1 -0
  38. package/build/actions/watch.js +45 -0
  39. package/build/actions/watch.js.map +1 -0
  40. package/build/consts.d.ts +18 -0
  41. package/build/consts.d.ts.map +1 -0
  42. package/build/consts.js +19 -0
  43. package/build/consts.js.map +1 -0
  44. package/build/executor/changes.d.ts +31 -0
  45. package/build/executor/changes.d.ts.map +1 -0
  46. package/build/executor/changes.js +55 -0
  47. package/build/executor/changes.js.map +1 -0
  48. package/build/executor/code.d.ts +15 -0
  49. package/build/executor/code.d.ts.map +1 -0
  50. package/build/executor/code.js +59 -0
  51. package/build/executor/code.js.map +1 -0
  52. package/build/executor/receipt.d.ts +12 -0
  53. package/build/executor/receipt.d.ts.map +1 -0
  54. package/build/executor/receipt.js +56 -0
  55. package/build/executor/receipt.js.map +1 -0
  56. package/build/executor/resolve.d.ts +35 -0
  57. package/build/executor/resolve.d.ts.map +1 -0
  58. package/build/executor/resolve.js +136 -0
  59. package/build/executor/resolve.js.map +1 -0
  60. package/build/executor/validate.d.ts +13 -0
  61. package/build/executor/validate.d.ts.map +1 -0
  62. package/build/executor/validate.js +222 -0
  63. package/build/executor/validate.js.map +1 -0
  64. package/build/executor.d.ts +15 -0
  65. package/build/executor.d.ts.map +1 -0
  66. package/build/executor.js +113 -0
  67. package/build/executor.js.map +1 -0
  68. package/build/facade.d.ts +12 -0
  69. package/build/facade.d.ts.map +1 -0
  70. package/build/facade.js +131 -0
  71. package/build/facade.js.map +1 -0
  72. package/build/helper.d.ts +18 -0
  73. package/build/helper.d.ts.map +1 -0
  74. package/build/helper.js +32 -0
  75. package/build/helper.js.map +1 -0
  76. package/build/index.d.ts +13 -0
  77. package/build/index.d.ts.map +1 -0
  78. package/build/index.js +11 -0
  79. package/build/index.js.map +1 -0
  80. package/build/projection.d.ts +28 -0
  81. package/build/projection.d.ts.map +1 -0
  82. package/build/projection.js +40 -0
  83. package/build/projection.js.map +1 -0
  84. package/build/registry.d.ts +30 -0
  85. package/build/registry.d.ts.map +1 -0
  86. package/build/registry.js +103 -0
  87. package/build/registry.js.map +1 -0
  88. package/build/service.d.ts +40 -0
  89. package/build/service.d.ts.map +1 -0
  90. package/build/service.js +109 -0
  91. package/build/service.js.map +1 -0
  92. package/build/store/commits.d.ts +15 -0
  93. package/build/store/commits.d.ts.map +1 -0
  94. package/build/store/commits.js +147 -0
  95. package/build/store/commits.js.map +1 -0
  96. package/build/store/composite.d.ts +12 -0
  97. package/build/store/composite.d.ts.map +1 -0
  98. package/build/store/composite.js +150 -0
  99. package/build/store/composite.js.map +1 -0
  100. package/build/store/fold.d.ts +39 -0
  101. package/build/store/fold.d.ts.map +1 -0
  102. package/build/store/fold.js +220 -0
  103. package/build/store/fold.js.map +1 -0
  104. package/build/store/index.d.ts +6 -0
  105. package/build/store/index.d.ts.map +1 -0
  106. package/build/store/index.js +5 -0
  107. package/build/store/index.js.map +1 -0
  108. package/build/store/memory.d.ts +19 -0
  109. package/build/store/memory.d.ts.map +1 -0
  110. package/build/store/memory.js +0 -0
  111. package/build/store/memory.js.map +1 -0
  112. package/build/store/types.d.ts +82 -0
  113. package/build/store/types.d.ts.map +1 -0
  114. package/build/store/types.js +2 -0
  115. package/build/store/types.js.map +1 -0
  116. package/build/types.d.ts +46 -0
  117. package/build/types.d.ts.map +1 -0
  118. package/build/types.js +2 -0
  119. package/build/types.js.map +1 -0
  120. package/build/utils/guard.d.ts +19 -0
  121. package/build/utils/guard.d.ts.map +1 -0
  122. package/build/utils/guard.js +35 -0
  123. package/build/utils/guard.js.map +1 -0
  124. package/build/utils/index.d.ts +3 -0
  125. package/build/utils/index.d.ts.map +1 -0
  126. package/build/utils/index.js +3 -0
  127. package/build/utils/index.js.map +1 -0
  128. package/build/utils/scope.d.ts +20 -0
  129. package/build/utils/scope.d.ts.map +1 -0
  130. package/build/utils/scope.js +36 -0
  131. package/build/utils/scope.js.map +1 -0
  132. package/package.json +66 -0
  133. package/src/actions/cards.ts +52 -0
  134. package/src/actions/commit.ts +55 -0
  135. package/src/actions/execute.ts +47 -0
  136. package/src/actions/index.ts +8 -0
  137. package/src/actions/links.ts +14 -0
  138. package/src/actions/schemas.ts +15 -0
  139. package/src/actions/specs.ts +23 -0
  140. package/src/actions/transitions.ts +14 -0
  141. package/src/actions/watch.ts +52 -0
  142. package/src/consts.ts +22 -0
  143. package/src/executor/changes.ts +78 -0
  144. package/src/executor/code.ts +83 -0
  145. package/src/executor/receipt.ts +66 -0
  146. package/src/executor/resolve.ts +174 -0
  147. package/src/executor/validate.ts +253 -0
  148. package/src/executor.ts +149 -0
  149. package/src/facade.ts +162 -0
  150. package/src/helper.ts +44 -0
  151. package/src/index.ts +13 -0
  152. package/src/projection.ts +65 -0
  153. package/src/registry.ts +143 -0
  154. package/src/service.ts +157 -0
  155. package/src/store/commits.ts +174 -0
  156. package/src/store/composite.ts +171 -0
  157. package/src/store/fold.ts +257 -0
  158. package/src/store/index.ts +6 -0
  159. package/src/store/memory.ts +0 -0
  160. package/src/store/types.ts +94 -0
  161. package/src/types.ts +48 -0
  162. package/src/utils/guard.ts +40 -0
  163. package/src/utils/index.ts +2 -0
  164. package/src/utils/scope.ts +51 -0
  165. package/tests/commits.spec.ts +78 -0
  166. package/tests/context.ts +198 -0
  167. package/tests/entrypoints.spec.ts +72 -0
  168. package/tests/executor.spec.ts +69 -0
  169. package/tests/memory-store.spec.ts +84 -0
  170. package/tests/plugins.spec.ts +86 -0
  171. package/tests/specifications.spec.ts +61 -0
  172. package/tests/tsconfig.json +17 -0
  173. package/tests/validation.spec.ts +62 -0
  174. package/tsconfig.json +16 -0
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # @owlmeans/server-planning
2
+
3
+ The server half of `@owlmeans/planning`: the planning service and its plugin registry, the
4
+ transition executor every write goes through, the in-memory reference store, the commit hub and the
5
+ protocol handlers. A backend appends the service once, binds the handlers to the shared protocol
6
+ tree, and reads and writes through a scoped facade. A browser or Node client uses
7
+ `@owlmeans/client-planning` against the same tree; a durable store implements the ports and reuses
8
+ the `@owlmeans/server-planning/store` subpath.
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ bun add @owlmeans/server-planning@^0.1.18-rc.0 @owlmeans/planning@^0.1.18-rc.0 ajv
14
+ ```
15
+
16
+ ## Concepts
17
+
18
+ - **Facade** — `context.planning().for(scope)`: every read and write of one organization entity,
19
+ with no scope argument on any method.
20
+ - **Executor** — the only write path: normalize → idempotency → resolve → plugins' `before` →
21
+ validate → id → code → changes → seq CAS → append → project → receipt. Nothing is appended by a
22
+ refusal.
23
+ - **Plugin** — contributes types and flows, may own types with its own store, mint codes, refuse or
24
+ rewrite executions (`before`) and react to commits (`after`).
25
+ - **Commit** — a transition becomes visible when a store folds it; the folding process runs the
26
+ `after` chain exactly once and publishes a commit event.
27
+ - **Store** — `makeMemoryPlanningStore()` for tests and single-process tools; a durable store in
28
+ anything with more than one process.
29
+
30
+ ## Usage
31
+
32
+ Declare the tree once in the shared package:
33
+
34
+ ```ts
35
+ import { makePlanningProtocols } from '@owlmeans/planning'
36
+
37
+ export const planningProtocols = makePlanningProtocols({
38
+ base: { alias: 'app:planning', path: '/planning' },
39
+ guards: DEFAULT_GUARD,
40
+ })
41
+ ```
42
+
43
+ Wire the service and bind the handlers:
44
+
45
+ ```ts
46
+ import { appendPlanningService, servePlanningEntrypoints } from '@owlmeans/server-planning'
47
+
48
+ appendPlanningService(context, { plugins: [appTypesPlugin] })
49
+ context.registerEntrypoints(servePlanningEntrypoints(planningProtocols, {
50
+ scope: req => ({ channel: 'web' }),
51
+ }))
52
+ ```
53
+
54
+ Write through the facade and wait for the commit:
55
+
56
+ ```ts
57
+ import { TransitionAction, WorkcardKind } from '@owlmeans/planning'
58
+
59
+ const planning = context.planning().for({ entityId, profileId, channel: 'agent' })
60
+ const { card } = await planning.execute({
61
+ card: { kind: WorkcardKind.Card, type: 'app:task', parent: projectId, title: 'Ship it' },
62
+ action: TransitionAction.Create,
63
+ key: `import:${sourceId}`,
64
+ }, { wait: true })
65
+
66
+ await planning.execute({ card: card!.id!, action: TransitionAction.Transit, transition: 'start' }, { wait: true })
67
+ ```
68
+
69
+ A plugin with a rule and a reaction:
70
+
71
+ ```ts
72
+ import { ensurePlanningService } from '@owlmeans/server-planning'
73
+
74
+ ensurePlanningService(context).use({
75
+ name: 'app-rules',
76
+ order: 10,
77
+ before: async (exec, { card, facade }) => {
78
+ if (exec.transition === 'start' && await facade.cards.count({ parent: card?.parent, status: 'doing' }) > 0) {
79
+ throw new ProjectBusy(card!.parent!)
80
+ }
81
+ },
82
+ after: async (event, { transition }) => {
83
+ if (event.action === TransitionAction.Transit && transition?.actor.channel === 'web') {
84
+ await notifyWorker(event.card)
85
+ }
86
+ },
87
+ })
88
+ ```
89
+
90
+ Hand-written handler and the memory store in a test:
91
+
92
+ ```ts
93
+ import { appendPlanningService, planningFor } from '@owlmeans/server-planning'
94
+ import { makeMemoryPlanningStore } from '@owlmeans/server-planning/store'
95
+
96
+ appendPlanningService(testContext, { store: makeMemoryPlanningStore({ sync: false }), plugins: [fixtures] })
97
+ const cards = await planningFor(ctx, req, { channel: 'web' }).cards.list({ parent: projectId })
98
+ ```
99
+
100
+ ## API
101
+
102
+ - Service: `appendPlanningService`, `ensurePlanningService`, `makePlanningService`,
103
+ `planningServiceApi`, `makePluginRegistry`, `makeStoreFacade`, `executeTransition`,
104
+ `PlanningServiceOptions`, `PlanningHostService`, `PlanningRuntime`, `PluginRegistry`
105
+ - Handlers: `servePlanningEntrypoints`, `planningFor`, `listSchemas`, `listCards`,
106
+ `summarizeCards`, `getCard`, `listCardTransitions`, `listCardSpecifications`, `getSpecification`,
107
+ `listSpecificationRevisions`, `listLinks`, `getTransition`, `executePlanning`, `wireExecution`,
108
+ `executeOptionsOf`, `getCommit`, `watchCommits`, `PlanningHandlerOptions`, `PlanningScopeExtractor`
109
+ - Scope: `scopeOf`, `actorOf`, `handlerFacade`, `planningServiceOf`, `concealed`, `assertScope`,
110
+ `notFoundOf`, `clampSeconds`
111
+ - Projection: `makeProjectionProcessor`, `planningQueueHooks`, `ProjectionOptions`
112
+ - Constants: `DEFAULT_ALIAS`, `MEMORY_STORE_ALIAS`, `DEFAULT_COMMIT_MEMORY`, `COMMIT_POLL_LADDER`
113
+ - `@owlmeans/server-planning/store`: `makeMemoryPlanningStore`, `foldPending`, `failPending`,
114
+ `revisionsFromLog`, `commitEventOf`, `makeCommitHub`, `makeCompositeStore`, and the types
115
+ `BindablePlanningStore`, `CommitListener`, `CommitHub`, `CommitHubOptions`, `FoldOptions`,
116
+ `FoldResult`, `MemoryPlanningStore`, `MemoryPlanningStoreOptions`, `StoreRoute`
117
+
118
+ ## Common pitfalls
119
+
120
+ - `after` hooks run where the transition is FOLDED, once per commit — register the same plugins in
121
+ every folding process, and never run hooks from a commit-bus subscription.
122
+ - The memory store is per-process heap: wrong for multiple processes or restarts.
123
+ - `actor` comes from the scope; a wire `actor` or `createdBy` is ignored.
124
+ - Another entity's record is `WorkcardNotFound`, never a permission error.
125
+ - Give retried writes a `key` — it is checked before validation, so a retry answers the first receipt.
126
+ - `expectSeq` compares against `head`; a stale one is `WorkcardConflict`, re-read and retry.
127
+ - Call `appendPlanningService` before any `ensurePlanningService`, or the appended host replaces
128
+ what was registered on the default one.
129
+
130
+ ## Related packages
131
+
132
+ - `@owlmeans/planning` — records, flows, fold, query language, protocol tree, models
133
+ - `@owlmeans/client-planning` — the remote facade and state mirror
134
+ - `@owlmeans/server-job`, `@owlmeans/queue` — job feeds and the projection queue
135
+
136
+ <!-- owlmeans:agent-guidance:start -->
137
+ ## Agent guidance
138
+
139
+ This package ships embedded agent skills under `agent-meta/`. After installing your
140
+ `@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
141
+ your project's skill store (`.agents/skills/`):
142
+
143
+ ```sh
144
+ npx @owlmeans/agent-skills@^0.1.18-rc.27
145
+ ```
146
+
147
+ The embedded files are version-matched to this package release. Do not edit them
148
+ directly — they are regenerated on each publish. To contribute guidance edits,
149
+ open a PR against the source monorepo.
150
+ <!-- owlmeans:agent-guidance:end -->
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "package": "@owlmeans/server-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": "server-planning",
11
+ "category": "package-specific",
12
+ "file": "skills/server-planning/SKILL.md",
13
+ "canonicalPath": ".agents/skills/server-planning/SKILL.md"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: server-planning
3
+ description: How to use @owlmeans/server-planning — appendPlanningService and the plugin registry, the transition executor and its refusal order, the in-memory store, servePlanningEntrypoints and the commit socket, and the ports a durable or foreign provider implements. Auto-invoked when wiring planning into a backend, writing a planning plugin, or diagnosing a transition that was refused or never committed.
4
+ user-invocable: false
5
+ ---
6
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
7
+
8
+ # @owlmeans/server-planning
9
+
10
+ **Layer:** Server
11
+ **Install:** `"@owlmeans/server-planning": "^0.1.18-rc.0"` in `dependencies` (`ajv` is a peer)
12
+
13
+ The general implementation of `@owlmeans/planning`: the planning service (a plugin host), the
14
+ scoped facade, the executor every write goes through, the in-memory reference store, the commit
15
+ hub and the protocol handlers. It holds no database code — a durable store implements the ports and
16
+ reuses the `./store` subpath (`foldPending`, `makeCommitHub`) without pulling fastify in.
17
+
18
+ ## Key exports
19
+
20
+ | Export | What it is |
21
+ |---|---|
22
+ | `appendPlanningService(ctx, opts?, alias?)` | Registers the lazy host service and `ctx.planning()` |
23
+ | `ensurePlanningService(ctx, alias?)` | Idempotent — what a plugin package calls before `use` |
24
+ | `makePlanningService(opts?, alias?)`, `planningServiceApi(opts, self)` | The service, and its body for a specialised service |
25
+ | `PlanningServiceOptions` | `{ store?, plugins?, schemas?, hooks?, ids?, now? }` |
26
+ | `makePluginRegistry`, `makeStoreFacade`, `executeTransition` | The pieces the service is made of |
27
+ | `servePlanningEntrypoints(protocols, opts?)` | One binding per protocol of a `makePlanningProtocols` tree |
28
+ | `planningFor(ctx, req, extra?)` | The request-scoped facade for a hand-written handler |
29
+ | `PlanningHandlerOptions` | `{ service?, event?, maxPoll?, scope?(req, ctx) }` |
30
+ | `listCards` … `executePlanning`, `getCommit`, `watchCommits` | The handlers, to bind one by hand |
31
+ | `scopeOf(req, extra?)`, `actorOf(req)`, `concealed(run)`, `clampSeconds` | Scope and security helpers |
32
+ | `makeProjectionProcessor(ctx, opts?)`, `planningQueueHooks(ctx, opts?)` | A generic projection job body and its `onJobDead` |
33
+ | `./store`: `makeMemoryPlanningStore`, `foldPending`, `failPending`, `revisionsFromLog`, `commitEventOf`, `makeCommitHub`, `makeCompositeStore` | What stores are built from |
34
+
35
+ ## Wiring
36
+
37
+ ```ts
38
+ import { appendPlanningService, servePlanningEntrypoints } from '@owlmeans/server-planning'
39
+ import { makePlanningProtocols } from '@owlmeans/planning'
40
+
41
+ // shared package
42
+ export const planningProtocols = makePlanningProtocols({
43
+ base: { alias: 'app:planning', path: '/planning', parent: appProtocols.api.base },
44
+ guards: DEFAULT_GUARD,
45
+ socketBase: appProtocols.updates.base,
46
+ })
47
+
48
+ // backend context
49
+ appendPlanningService(context, { store: durableStore, plugins: [appTypesPlugin] })
50
+
51
+ // API process
52
+ context.registerEntrypoints(servePlanningEntrypoints(planningProtocols, {
53
+ scope: req => ({ channel: req.auth?.type === AuthroizationType.AuthToken ? 'connect' : 'web' }),
54
+ }))
55
+ ```
56
+
57
+ - Without `store` the service builds a memory store. The host is a LAZY service so
58
+ `ensurePlanningService(ctx).use(plugin)` works from `makeContext`, before `init()`.
59
+ - Call `appendPlanningService` BEFORE any `ensurePlanningService`: `ensure` registers a default
60
+ host when none exists, and a later `append` replaces it together with whatever was `use`d on it.
61
+ - In code: `ctx.planning().for({ entityId, profileId?, channel?, actor? })` → a `PlanningFacade`
62
+ with no scope argument on any method.
63
+
64
+ ## The executor, step by step
65
+
66
+ Nothing is appended before step 11 — every refusal leaves the log untouched.
67
+
68
+ 1. **Normalize** — deep copy, trimmed text, `parents ∋ parent` (first).
69
+ 2. **Scope** — `entityId` must be present.
70
+ 3. **Idempotency** — a `key` already in the entity's log answers ITS receipt, before any
71
+ validation or middleware.
72
+ 4. **Resolve** — the card (`WorkcardNotFound`; another entity's card is `PlanningScopeMismatch`),
73
+ its type and flow, every draft parent (`ParentNotFound`) and the parent project's
74
+ `cardTypes`/`projectTypes` (`CardTypeNotAllowed`), the specification slot.
75
+ 5. **`before` chain** — in plugin order; re-resolved once when it changed the card, type or parent.
76
+ 6. **Validate** — shape (`planning:malformed:*`), the flow rule (`IllegalTransition`), immutables
77
+ (`planning:immutable:*`), merged `fields` (`FieldsInvalid`), labels (`LabelNotAllowed`), a moved
78
+ parent, the slot (`SpecificationSlotUnknown`, `SpecificationRevisionConflict`, JSON body schema
79
+ → `FieldsInvalid`), relationships (`RelationshipRefused`: undeclared type, `from` not the card,
80
+ missing target, type constraints, `single`, unlinking an absent edge).
81
+ 7. **Card id** — `store.newId?.() ?? options.ids?.() ?? uuid()`. Ids are the store's.
82
+ 8. **Code** — a supplied code is checked; else the plugins' `mintCode` (first answer wins, checked);
83
+ else the type's `CodePolicy` (a slug policy derives from the title) → `CodeTaken`.
84
+ 9. **Changes** — `computeChanges`. An `update` that changes nothing answers the card's latest
85
+ receipt and appends nothing.
86
+ 10. **Seq** — `transitions.nextSeq(card, expectSeq)`, a CAS against `head` (`WorkcardConflict`).
87
+ 11. **Append** — `commit: pending`. A lost race on the key answers the winner's receipt.
88
+ 12. **Project** — `store.cards.project(card)`: a sync store folds now, a queued one enqueues.
89
+ 13. **Receipt** — `{ transition, card? (once committed), committed(opts) }`.
90
+ 14. **`wait: true`** — `committed()` before returning (`CommitTimeout`, `CommitFailed`).
91
+
92
+ `actor` is the SCOPE's: `profileId`/`userId`/`service`/`channel` come from the scope only; an
93
+ in-process caller may add `agent`/`runId` on the execution. A handler never passes a wire `actor`.
94
+
95
+ ## Idempotency and `expectSeq`
96
+
97
+ - Give every retried or replayed write a `key` (`import:<source>:<n>`). The key is unique per entity
98
+ and checked first, so a retry never re-validates against a card its first attempt already moved.
99
+ - `expectSeq` compares against `head`, not `seq`: two writers racing while a fold is pending both
100
+ see the same `seq` but only one wins the head. `null` or omitted opts out; a model fills it from
101
+ its record.
102
+
103
+ ## The plugin seam
104
+
105
+ | Concern | Rule |
106
+ |---|---|
107
+ | Registration | `use(plugin)` replaces by `name`; schemas are contributed at `use` time (later type/flow id wins) |
108
+ | Order | `order ?? 50` ascending, registration order among equals |
109
+ | Store routing | `store(type)` = the first plugin whose `owns(type)` is true AND that supplies a `store`; else the default store. A store factory is resolved once |
110
+ | Reads | By id: the default store, then each owned store. A list naming one owned type (or several owned by one store) goes there; any other list — a cross-type query — is the default store's alone |
111
+ | `mintCode` | First non-`undefined` answer wins |
112
+ | `before` | Every plugin in order; may return a replaced execution; a `ResilientError` passes through, anything else becomes `PlanningRefused('<plugin>:<message>')` |
113
+ | `after` | Every plugin in order, once per COMMITTED transition; errors logged, never rethrown |
114
+
115
+ **Where `after` runs.** The service's `committed(event)` runs the chain, and it is called by the
116
+ process that FOLDS the transition — the memory store inside `project()`, a queued store's projection
117
+ job after marking the commit. Never subscribe a process to the commit bus to run hooks: that fires
118
+ once per subscribed process. Every process that folds registers the same plugins; each commit is
119
+ folded by one of them, so each hook fires once. `hooks: false` makes a process fold without running
120
+ them. A hook sees `ctx.transition` (read back, so `actor.channel` is available) and a facade scoped
121
+ to the event's entity acting as this process's `cfg.service`.
122
+
123
+ The service hands `committed` to a store through `store.bind?.(listener)` the first time it resolves
124
+ the store; `bind` replaces the listener, so a store has exactly one.
125
+
126
+ ## The memory store
127
+
128
+ `makeMemoryPlanningStore({ sync?, ids?, now?, seed?, onCommitted?, alias? })`
129
+
130
+ - Cards and projects in one map, specifications in their own, relationships in a third; every read
131
+ goes through the `@owlmeans/resource` query engine. A card list includes specifications only when
132
+ the criteria asks for `kind: specification` or a `category`.
133
+ - `sync` (default) folds, publishes and runs `committed` inside `project()`, serialized per card.
134
+ `sync: false` folds nothing until `flush(card?)` — a queued store's shape, for tests that need a
135
+ commit to stay pending.
136
+ - A project `delete` purges the project, everything whose `parents` reach it, their specifications,
137
+ links and log; the commit hub remembers the settled event so the waiter still gets its answer.
138
+ - **It is wrong for more than one process or a restart** — the log, the cards and the commit feed
139
+ are this process's heap. Use it for tests, tools and single-process demos.
140
+
141
+ ## Writing a durable store
142
+
143
+ - Implement `PlanningStore` (`transitions`, `cards`, `specs`, `links`, `commits`, `newId`). `newId`
144
+ answers the id format the host's references need (ObjectId hex for Mongo).
145
+ - `transitions.list` treats `sinceSeq` as `seq > sinceSeq`; `nextSeq` is the CAS against `head` and
146
+ answers 1 for a card that has no log.
147
+ - The projection body is `foldPending(store, cardId, entityId, { onCommitted, publish, touch, limit })`:
148
+ it applies pending transitions with `applyTransition` only, marks each committed or failed,
149
+ publishes, then runs `onCommitted`. The caller owns single flight per card. Strip `record` from
150
+ the event before a cross-process bus.
151
+ - **A failed transition never blocks the card.** The fold marks it `failed`, publishes a failed
152
+ event, and goes PAST it: the card's `seq` advances to that transition with every other value as
153
+ it was, and the next transition folds normally. Without that, every later transition fails as
154
+ out of order and the card is stuck for good. Only two cases stop short: a failed `create` leaves
155
+ no card to advance, and a store that refuses the advancing write stops the fold with `followUp`,
156
+ leaving the rest pending for a retry. `revisionsFromLog` replays past failed transitions the same
157
+ way.
158
+ - `failPending` is what a dead projection job must do, or waiters hang to their timeout.
159
+ - `makeCommitHub({ status, remember?, ladder? })` is the `CommitSource`: feed `publish` from the bus,
160
+ answer `status` from transition rows. `wait` subscribes first, polls once, then climbs the ladder.
161
+ - `revisionsFromLog(transitions, limit?)` answers `specs.revisions`.
162
+ - `makeProjectionProcessor(ctx)` + `planningQueueHooks(ctx)` are the store-agnostic job body and
163
+ `onJobDead`; a store with an admission CAS wraps its own around them.
164
+
165
+ ## A foreign provider
166
+
167
+ Only `cards` is required. A plugin that `owns` its types and supplies a store without
168
+ `transitions` can be read through the facade; writes to it refuse with `PlanningUnsupported` —
169
+ mapping external records (`mappers`) belongs to that plugin's own store adapter.
170
+
171
+ ## The commit feed
172
+
173
+ - **Poll** — `commit.get` answers the status; with `?wait=<s>` (clamped to `maxPoll`, default 55)
174
+ it subscribes, re-polls, and holds until the commit settles or the time is up, answering `pending`
175
+ then — never an error.
176
+ - **Notify** — `commit.events` pushes `planning-commit` frames (or `opts.event`) filtered by the
177
+ authenticated entity and optional `project`/`card` query values; a frame without `record` gets the
178
+ card as it reads now. The subscription is released on the socket's `close` frame.
179
+ - `execute` with `wait: true` holds at most `maxPoll` seconds whatever `timeout` the body asked for.
180
+
181
+ ## Scope and security
182
+
183
+ - `entityId` is `requireEntityKey(req)`; `opts.scope(req, ctx)` adds a `channel` or `service` and
184
+ can never replace the entity. `actor` and a create's `createdBy` come from the request, never the
185
+ body.
186
+ - Another entity's card, specification or transition answers `WorkcardNotFound` — the same as an
187
+ absent id — on reads and writes alike (`concealed`).
188
+ - Queries arrive in their wire form and are decoded with `decode*Query` before `criteriaOf`, which
189
+ always adds the scope's `entityId`.
190
+
191
+ ## Testing
192
+
193
+ Category A. Build one real context with `makeBasicContext` + `appendPlanningService({ store:
194
+ makeMemoryPlanningStore({ now }), plugins: [fixtures] })`, and run a handler through
195
+ `handler.bind({ ref: { ctx } })(req, res)` with a request carrying `auth` and `entity`. A monotonic
196
+ `now` keeps log order deterministic; `sync: false` + `flush()` pins pending and hook-count cases.
197
+
198
+ ## Related
199
+
200
+ - `planning` — records, flows, the fold, the query language, the protocol tree, the models
201
+ - `client-planning` — the remote facade and the state mirror that talk to these handlers
202
+ - `server-job`, `queue` — the declare/serve/feed pattern and the projection queue
203
+ - `resource` — the criteria engine every memory read goes through
@@ -0,0 +1,16 @@
1
+ import { handlers } from '@owlmeans/server-api';
2
+ import type { PlanningProtocols } from '@owlmeans/planning';
3
+ import type { Context, PlanningHandlerOptions } from '../types.js';
4
+ type RequestHandler = ReturnType<ReturnType<typeof handlers<Context>>['request']>;
5
+ /** Cards of the caller's entity; the query arrives in its wire shape and is decoded here. */
6
+ export declare const listCards: (protocol: PlanningProtocols['card']['list'], opts?: PlanningHandlerOptions) => RequestHandler;
7
+ /** Intrinsic counts of DIRECT children per parent. */
8
+ export declare const summarizeCards: (protocol: PlanningProtocols['card']['summary'], opts?: PlanningHandlerOptions) => RequestHandler;
9
+ /** @throws {WorkcardNotFound} for an absent card and for another entity's alike */
10
+ export declare const getCard: (protocol: PlanningProtocols['card']['get'], opts?: PlanningHandlerOptions) => RequestHandler;
11
+ /** One card's log. */
12
+ export declare const listCardTransitions: (protocol: PlanningProtocols['card']['transitions'], opts?: PlanningHandlerOptions) => RequestHandler;
13
+ /** One card's specifications — the current document per category unless `all`. */
14
+ export declare const listCardSpecifications: (protocol: PlanningProtocols['card']['specifications'], opts?: PlanningHandlerOptions) => RequestHandler;
15
+ export {};
16
+ //# sourceMappingURL=cards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../../src/actions/cards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAI/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlE,KAAK,cAAc,GAAG,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;AAEjF,6FAA6F;AAC7F,eAAO,MAAM,SAAS,aAAc,iBAAiB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,SAAS,sBAAsB,KAAG,cAKlG,CAAA;AAEL,sDAAsD;AACtD,eAAO,MAAM,cAAc,aAAc,iBAAiB,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,SAAS,sBAAsB,KAAG,cAM1G,CAAA;AAEL,mFAAmF;AACnF,eAAO,MAAM,OAAO,aAAc,iBAAiB,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,SAAS,sBAAsB,KAAG,cAK/F,CAAA;AAEL,sBAAsB;AACtB,eAAO,MAAM,mBAAmB,aAAc,iBAAiB,CAAC,MAAM,CAAC,CAAC,aAAa,CAAC,SAAS,sBAAsB,KAAG,cAMnH,CAAA;AAEL,kFAAkF;AAClF,eAAO,MAAM,sBAAsB,aAAc,iBAAiB,CAAC,MAAM,CAAC,CAAC,gBAAgB,CAAC,SAAS,sBAAsB,KAAG,cAMzH,CAAA"}
@@ -0,0 +1,32 @@
1
+ import { handlers } from '@owlmeans/server-api';
2
+ import { decodeSpecificationQuery, decodeSummaryQuery, decodeTransitionQuery, decodeWorkcardQuery, } from '@owlmeans/planning';
3
+ import { concealed, handlerFacade } from '../utils/index.js';
4
+ /** Cards of the caller's entity; the query arrives in its wire shape and is decoded here. */
5
+ export const listCards = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
6
+ const facade = await handlerFacade(ctx, req, opts);
7
+ return await facade.cards.list(decodeWorkcardQuery(req.query));
8
+ }));
9
+ /** Intrinsic counts of DIRECT children per parent. */
10
+ export const summarizeCards = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
11
+ const facade = await handlerFacade(ctx, req, opts);
12
+ const { parents, ...query } = decodeSummaryQuery(req.query);
13
+ return await facade.cards.summary(parents, query);
14
+ }));
15
+ /** @throws {WorkcardNotFound} for an absent card and for another entity's alike */
16
+ export const getCard = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
17
+ const facade = await handlerFacade(ctx, req, opts);
18
+ return await facade.cards.get(`${req.params.id}`);
19
+ }));
20
+ /** One card's log. */
21
+ export const listCardTransitions = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
22
+ const facade = await handlerFacade(ctx, req, opts);
23
+ const card = await facade.cards.get(`${req.params.id}`);
24
+ return await facade.transitions.list({ ...decodeTransitionQuery(req.query), card: card.id });
25
+ }));
26
+ /** One card's specifications — the current document per category unless `all`. */
27
+ export const listCardSpecifications = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
28
+ const facade = await handlerFacade(ctx, req, opts);
29
+ const card = await facade.cards.get(`${req.params.id}`);
30
+ return await facade.specifications.list(card.id, decodeSpecificationQuery(req.query));
31
+ }));
32
+ //# sourceMappingURL=cards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cards.js","sourceRoot":"","sources":["../../src/actions/cards.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { handlers } from '@owlmeans/server-api';
2
+ import type { PlanningProtocols } from '@owlmeans/planning';
3
+ import type { Context, PlanningHandlerOptions } from '../types.js';
4
+ /**
5
+ * The poll tool: a commit's status, held open up to `wait` seconds (clamped to `maxPoll`).
6
+ *
7
+ * status → settled or no wait: answer → subscribe → poll again (a commit landing between the two
8
+ * reads is caught by the subscription) → race the subscription against the timer → answer the
9
+ * status as it is then, still `pending` when nothing landed.
10
+ *
11
+ * @throws {WorkcardNotFound} for another entity's transition
12
+ */
13
+ export declare const getCommit: (protocol: PlanningProtocols['commit']['get'], opts?: PlanningHandlerOptions) => ReturnType<ReturnType<typeof handlers<Context>>['request']>;
14
+ //# sourceMappingURL=commit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit.d.ts","sourceRoot":"","sources":["../../src/actions/commit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/C,OAAO,KAAK,EAAgB,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACzE,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlE;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,aACV,iBAAiB,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,SAAS,sBAAsB,KAC1E,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAqCxD,CAAA"}
@@ -0,0 +1,48 @@
1
+ import { handlers } from '@owlmeans/server-api';
2
+ import { CommitState, MAX_COMMIT_POLL } from '@owlmeans/planning';
3
+ import { clampSeconds, concealed, handlerFacade } from '../utils/index.js';
4
+ /**
5
+ * The poll tool: a commit's status, held open up to `wait` seconds (clamped to `maxPoll`).
6
+ *
7
+ * status → settled or no wait: answer → subscribe → poll again (a commit landing between the two
8
+ * reads is caught by the subscription) → race the subscription against the timer → answer the
9
+ * status as it is then, still `pending` when nothing landed.
10
+ *
11
+ * @throws {WorkcardNotFound} for another entity's transition
12
+ */
13
+ export const getCommit = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
14
+ const facade = await handlerFacade(ctx, req, opts);
15
+ const transition = `${req.params.transition}`;
16
+ let status = await facade.commits.status(transition);
17
+ const wait = clampSeconds(req.query?.wait, opts?.maxPoll ?? MAX_COMMIT_POLL);
18
+ if (status.state !== CommitState.Pending || wait <= 0) {
19
+ return status;
20
+ }
21
+ let landed = false;
22
+ let wake = () => { landed = true; };
23
+ const unsubscribe = await facade.commits.subscribe(event => {
24
+ if (event.transition === transition && event.state !== CommitState.Pending) {
25
+ wake();
26
+ }
27
+ });
28
+ try {
29
+ status = await facade.commits.status(transition);
30
+ if (status.state === CommitState.Pending) {
31
+ if (!landed) {
32
+ await new Promise(resolve => {
33
+ const timer = setTimeout(resolve, wait * 1000);
34
+ wake = () => {
35
+ clearTimeout(timer);
36
+ resolve();
37
+ };
38
+ });
39
+ }
40
+ status = await facade.commits.status(transition);
41
+ }
42
+ }
43
+ finally {
44
+ unsubscribe();
45
+ }
46
+ return status;
47
+ }));
48
+ //# sourceMappingURL=commit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit.js","sourceRoot":"","sources":["../../src/actions/commit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAGjE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAE1E;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,QAA4C,EAAE,IAA6B,EACd,EAAE,CAC/D,QAAQ,EAAW,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,IAA2B,EAAE;IACpG,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;IAClD,MAAM,UAAU,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,CAAA;IAE7C,IAAI,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACpD,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,IAAI,eAAe,CAAC,CAAA;IAC5E,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW,CAAC,OAAO,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;QACtD,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,MAAM,GAAG,KAAK,CAAA;IAClB,IAAI,IAAI,GAAe,GAAG,EAAE,GAAG,MAAM,GAAG,IAAI,CAAA,CAAC,CAAC,CAAA;IAC9C,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE;QACzD,IAAI,KAAK,CAAC,UAAU,KAAK,UAAU,IAAI,KAAK,CAAC,KAAK,KAAK,WAAW,CAAC,OAAO,EAAE,CAAC;YAC3E,IAAI,EAAE,CAAA;QACR,CAAC;IACH,CAAC,CAAC,CAAA;IACF,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;QAChD,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW,CAAC,OAAO,EAAE,CAAC;YACzC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;oBAChC,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC,CAAA;oBAC9C,IAAI,GAAG,GAAG,EAAE;wBACV,YAAY,CAAC,KAAK,CAAC,CAAA;wBACnB,OAAO,EAAE,CAAA;oBACX,CAAC,CAAA;gBACH,CAAC,CAAC,CAAA;YACJ,CAAC;YACD,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;QAClD,CAAC;IACH,CAAC;YAAS,CAAC;QACT,WAAW,EAAE,CAAA;IACf,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC,CAAC,CAAC,CAAA"}
@@ -0,0 +1,13 @@
1
+ import { handlers } from '@owlmeans/server-api';
2
+ import type { ExecuteOptions, ExecuteRequest, PlanningProtocols, PlanningScope, TransitionExecution } from '@owlmeans/planning';
3
+ import type { Context, PlanningHandlerOptions } from '../types.js';
4
+ /**
5
+ * What a wire execution may say. `actor` is dropped — the scope names the writer — and a create's
6
+ * `createdBy` is the authenticated subject, whatever the body claimed.
7
+ */
8
+ export declare const wireExecution: (body: ExecuteRequest, scope: PlanningScope) => TransitionExecution;
9
+ /** `wait`/`timeout` as a server grants them: the timeout never holds a request past `maxPoll`. */
10
+ export declare const executeOptionsOf: (body: Pick<ExecuteRequest, 'wait' | 'timeout'>, maxPoll: number) => ExecuteOptions;
11
+ /** The one write route. */
12
+ export declare const executePlanning: (protocol: PlanningProtocols['execute'], opts?: PlanningHandlerOptions) => ReturnType<ReturnType<typeof handlers<Context>>['request']>;
13
+ //# sourceMappingURL=execute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/actions/execute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/C,OAAO,KAAK,EACV,cAAc,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EACtF,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlE;;;GAGG;AACH,eAAO,MAAM,aAAa,SAAU,cAAc,SAAS,aAAa,KAAG,mBAQ1E,CAAA;AAED,kGAAkG;AAClG,eAAO,MAAM,gBAAgB,SAAU,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC,WAAW,MAAM,KAAG,cAOlG,CAAA;AAED,2BAA2B;AAC3B,eAAO,MAAM,eAAe,aAChB,iBAAiB,CAAC,SAAS,CAAC,SAAS,sBAAsB,KACpE,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAWxD,CAAA"}
@@ -0,0 +1,35 @@
1
+ import { handlers } from '@owlmeans/server-api';
2
+ import { DEFAULT_COMMIT_TIMEOUT, MAX_COMMIT_POLL, TransitionAction } from '@owlmeans/planning';
3
+ import { concealed, handlerFacade } from '../utils/index.js';
4
+ /**
5
+ * What a wire execution may say. `actor` is dropped — the scope names the writer — and a create's
6
+ * `createdBy` is the authenticated subject, whatever the body claimed.
7
+ */
8
+ export const wireExecution = (body, scope) => {
9
+ const { wait: _wait, timeout: _timeout, actor: _actor, ...exec } = body;
10
+ if (exec.action === TransitionAction.Create && exec.card != null && typeof exec.card === 'object') {
11
+ const { createdBy: _createdBy, ...draft } = exec.card;
12
+ const subject = scope.profileId ?? scope.userId;
13
+ exec.card = subject != null ? { ...draft, createdBy: subject } : draft;
14
+ }
15
+ return exec;
16
+ };
17
+ /** `wait`/`timeout` as a server grants them: the timeout never holds a request past `maxPoll`. */
18
+ export const executeOptionsOf = (body, maxPoll) => {
19
+ const ceiling = maxPoll * 1000;
20
+ if (body.wait !== true) {
21
+ return {};
22
+ }
23
+ const asked = Number(body.timeout ?? DEFAULT_COMMIT_TIMEOUT);
24
+ return { wait: true, timeout: Math.min(Number.isFinite(asked) && asked > 0 ? asked : DEFAULT_COMMIT_TIMEOUT, ceiling) };
25
+ };
26
+ /** The one write route. */
27
+ export const executePlanning = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
28
+ const facade = await handlerFacade(ctx, req, opts);
29
+ const body = (req.body ?? {});
30
+ const receipt = await facade.execute(wireExecution(body, facade.scope), executeOptionsOf(body, opts?.maxPoll ?? MAX_COMMIT_POLL));
31
+ return receipt.card !== undefined
32
+ ? { transition: receipt.transition, card: receipt.card }
33
+ : { transition: receipt.transition };
34
+ }));
35
+ //# sourceMappingURL=execute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/actions/execute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAK9F,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAE5D;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAoB,EAAE,KAAoB,EAAuB,EAAE;IAC/F,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAA;IACvE,IAAI,IAAI,CAAC,MAAM,KAAK,gBAAgB,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAClG,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,IAAI,CAAA;QACrD,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,MAAM,CAAA;QAC/C,IAAI,CAAC,IAAI,GAAG,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;IACxE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED,kGAAkG;AAClG,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAA8C,EAAE,OAAe,EAAkB,EAAE;IAClH,MAAM,OAAO,GAAG,OAAO,GAAG,IAAI,CAAA;IAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QACvB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,sBAAsB,CAAC,CAAA;IAC5D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,sBAAsB,EAAE,OAAO,CAAC,EAAE,CAAA;AACzH,CAAC,CAAA;AAED,2BAA2B;AAC3B,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,QAAsC,EAAE,IAA6B,EACR,EAAE,CAC/D,QAAQ,EAAW,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,IAAoC,EAAE;IAC7G,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;IAClD,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAmB,CAAA;IAC/C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,OAAO,CAClC,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,IAAI,eAAe,CAAC,CAC5F,CAAA;IAED,OAAO,OAAO,CAAC,IAAI,KAAK,SAAS;QAC/B,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE;QACxD,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAA;AACxC,CAAC,CAAC,CAAC,CAAA"}
@@ -0,0 +1,9 @@
1
+ export * from './cards.js';
2
+ export * from './commit.js';
3
+ export * from './execute.js';
4
+ export * from './links.js';
5
+ export * from './schemas.js';
6
+ export * from './specs.js';
7
+ export * from './transitions.js';
8
+ export * from './watch.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA"}
@@ -0,0 +1,9 @@
1
+ export * from './cards.js';
2
+ export * from './commit.js';
3
+ export * from './execute.js';
4
+ export * from './links.js';
5
+ export * from './schemas.js';
6
+ export * from './specs.js';
7
+ export * from './transitions.js';
8
+ export * from './watch.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA"}
@@ -0,0 +1,5 @@
1
+ import { handlers } from '@owlmeans/server-api';
2
+ import type { PlanningProtocols } from '@owlmeans/planning';
3
+ import type { Context, PlanningHandlerOptions } from '../types.js';
4
+ export declare const listLinks: (protocol: PlanningProtocols['link']['list'], opts?: PlanningHandlerOptions) => ReturnType<ReturnType<typeof handlers<Context>>['request']>;
5
+ //# sourceMappingURL=links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/actions/links.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlE,eAAO,MAAM,SAAS,aACV,iBAAiB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,SAAS,sBAAsB,KACzE,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAKxD,CAAA"}
@@ -0,0 +1,8 @@
1
+ import { handlers } from '@owlmeans/server-api';
2
+ import { decodeRelationshipQuery } from '@owlmeans/planning';
3
+ import { concealed, handlerFacade } from '../utils/index.js';
4
+ export const listLinks = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
5
+ const facade = await handlerFacade(ctx, req, opts);
6
+ return await facade.relationships.list(decodeRelationshipQuery(req.query));
7
+ }));
8
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/actions/links.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAG5D,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAE5D,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,QAA2C,EAAE,IAA6B,EACb,EAAE,CAC/D,QAAQ,EAAW,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE;IAC7E,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;IAElD,OAAO,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;AAC5E,CAAC,CAAC,CAAC,CAAA"}
@@ -0,0 +1,6 @@
1
+ import { handlers } from '@owlmeans/server-api';
2
+ import type { PlanningProtocols } from '@owlmeans/planning';
3
+ import type { Context, PlanningHandlerOptions } from '../types.js';
4
+ /** The schema bundle the service holds — what a client loads to answer `can()` with no round trip. */
5
+ export declare const listSchemas: (protocol: PlanningProtocols['schema']['list'], opts?: PlanningHandlerOptions) => ReturnType<ReturnType<typeof handlers<Context>>['request']>;
6
+ //# sourceMappingURL=schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/actions/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlE,sGAAsG;AACtG,eAAO,MAAM,WAAW,aACZ,iBAAiB,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,sBAAsB,KAC3E,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAMzD,CAAA"}