@owlmeans/client-planning 0.1.18-rc.9 → 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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
2
|
+
import { planningReplyHelper, PlanningUnsupported, SchemaWriteMode, scopedSchemaHelper } from '@owlmeans/planning'
|
|
3
|
+
import type {
|
|
4
|
+
PlanningProtocols, SchemaDefineReply, SchemaDefineRequest, ScopedSchemaBundle, ScopedSchemaRegistry,
|
|
5
|
+
} from '@owlmeans/planning'
|
|
6
|
+
import type { RemoteDefinitions, RemoteDefinitionsOptions } from './types.js'
|
|
7
|
+
import { makePlanningClientLifecycle } from './lifecycle.js'
|
|
8
|
+
|
|
9
|
+
const clean = <T extends object>(record: T): T =>
|
|
10
|
+
Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)) as T
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Data-defined types and flows over a tree declared with `definitions: true`: a layer's registry is
|
|
14
|
+
* read through `schema.list` (with its `project`) and cached per project; every write goes through
|
|
15
|
+
* `schema.define` and drops the whole cache — an organization-wide write reaches every project's
|
|
16
|
+
* layer. The records of a layer are the server's alone (`records` refuses).
|
|
17
|
+
*
|
|
18
|
+
* @throws {PlanningUnsupported} for a tree declared without `definitions`
|
|
19
|
+
*/
|
|
20
|
+
export const makeRemoteDefinitions = <C extends BasicConfig, T extends BasicContext<C>>(
|
|
21
|
+
context: T, protocols: PlanningProtocols, opts: RemoteDefinitionsOptions = {}
|
|
22
|
+
): RemoteDefinitions => {
|
|
23
|
+
const lifecycle = opts.lifecycle ?? makePlanningClientLifecycle({ scopeKey: opts.scopeKey })
|
|
24
|
+
const define = protocols.schema.define
|
|
25
|
+
if (define == null) {
|
|
26
|
+
throw new PlanningUnsupported('client:definitions')
|
|
27
|
+
}
|
|
28
|
+
const cache = new Map<string, Promise<ScopedSchemaRegistry>>()
|
|
29
|
+
|
|
30
|
+
const invalidate = (): void => {
|
|
31
|
+
cache.clear()
|
|
32
|
+
opts.onWrite?.()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const registry = (project?: string): Promise<ScopedSchemaRegistry> => {
|
|
36
|
+
const identity = lifecycle.key()
|
|
37
|
+
const key = `${identity}\u0000${project ?? ''}`
|
|
38
|
+
let entry = cache.get(key)
|
|
39
|
+
if (entry == null) {
|
|
40
|
+
const loading = lifecycle.run(async operation => {
|
|
41
|
+
const bundle = planningReplyHelper.hydrate<ScopedSchemaBundle>(await operation.wait(context.entrypoint(protocols.schema.list).call({ query: project != null ? { project } : {}, timeout: opts.timeout, signal: operation.signal })))
|
|
42
|
+
return scopedSchemaHelper.scopedRegistryOf(bundle)
|
|
43
|
+
})
|
|
44
|
+
entry = loading
|
|
45
|
+
cache.set(key, loading)
|
|
46
|
+
// A failed read is forgotten, so the next caller asks again instead of inheriting the error.
|
|
47
|
+
loading.catch(() => {
|
|
48
|
+
if (cache.get(key) === loading) {
|
|
49
|
+
cache.delete(key)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
return entry
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const write = async (request: SchemaDefineRequest): Promise<SchemaDefineReply> => lifecycle.run(async operation => {
|
|
57
|
+
try {
|
|
58
|
+
return planningReplyHelper.hydrate<SchemaDefineReply>(await operation.wait(context.entrypoint(define).call({ body: clean(request), timeout: opts.timeout, signal: operation.signal })))
|
|
59
|
+
} finally {
|
|
60
|
+
if (operation.active()) invalidate()
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
bundle: async project => (await registry(project)).bundle(),
|
|
66
|
+
|
|
67
|
+
registry,
|
|
68
|
+
|
|
69
|
+
records: async () => {
|
|
70
|
+
throw new PlanningUnsupported('client:definitions:records')
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
putAssigneeType: async schema => (await write({ assigneeTypes: [schema], mode: SchemaWriteMode.Put })).records[0],
|
|
74
|
+
putType: async (type, layer) =>
|
|
75
|
+
(await write({ project: layer?.project, mode: SchemaWriteMode.Put, types: [type] })).records[0],
|
|
76
|
+
|
|
77
|
+
putFlow: async (flow, layer) =>
|
|
78
|
+
(await write({ project: layer?.project, mode: SchemaWriteMode.Put, flows: [flow] })).records[0],
|
|
79
|
+
|
|
80
|
+
define: async (declarations, layer) =>
|
|
81
|
+
(await write({ project: layer?.project, mode: SchemaWriteMode.Define, ...declarations })).records,
|
|
82
|
+
|
|
83
|
+
seed: async (declarations, layer) =>
|
|
84
|
+
(await write({ project: layer?.project, mode: SchemaWriteMode.Seed, ...declarations })).records,
|
|
85
|
+
|
|
86
|
+
retire: async (kind, key, layer) => {
|
|
87
|
+
const { records } = await write({ project: layer?.project, retire: [{ kind, key }] })
|
|
88
|
+
return records[records.length - 1]
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
invalidate,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CommitEvent, PlanningFacade, TransitionReceiptView, Workcard } from '@owlmeans/planning'
|
|
2
|
+
import type { PlanningClientLifecycle, PlanningOperation } from '../lifecycle/types.js'
|
|
3
|
+
|
|
4
|
+
export interface PlanningMirrorOptions {
|
|
5
|
+
lifecycle: PlanningClientLifecycle
|
|
6
|
+
operation: PlanningOperation
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** What one set of mirror stores learns from the server: commit frames, receipts and cards read. */
|
|
10
|
+
export interface PlanningMirror {
|
|
11
|
+
/**
|
|
12
|
+
* Fold one commit frame into the mirror.
|
|
13
|
+
*
|
|
14
|
+
* - An OLDER `seq` never overwrites a newer record — frames and list answers race.
|
|
15
|
+
* - A committed `delete` REMOVES the row and every link touching it (and, for a project, the rows
|
|
16
|
+
* under it — the server purged them in the same fold).
|
|
17
|
+
* - A frame for an id the store never saw still writes a row: a card created in another tab
|
|
18
|
+
* appears here without waiting for the next list.
|
|
19
|
+
* - A `failed` commit leaves the card alone; the reason is recorded in the commit store.
|
|
20
|
+
*
|
|
21
|
+
* A frame from a cross-process bus carries ids only. When `record` is absent the card is re-read
|
|
22
|
+
* through `facade`; without one there is nothing to write but the commit itself.
|
|
23
|
+
*/
|
|
24
|
+
applyCommitEvent: (event: CommitEvent, facade?: PlanningFacade, options?: PlanningMirrorOptions) => Promise<void>
|
|
25
|
+
/**
|
|
26
|
+
* Mark a receipt in the mirror the moment the server answers it.
|
|
27
|
+
*
|
|
28
|
+
* A pending transition raises the card's `head` to its `seq`, so `head > seq` — the model's
|
|
29
|
+
* `pending()` — is true before any frame arrives. A receipt that already carries the committed
|
|
30
|
+
* card writes it; a committed delete drops the row.
|
|
31
|
+
*/
|
|
32
|
+
applyReceipt: (receipt: TransitionReceiptView, options?: PlanningMirrorOptions) => Promise<void>
|
|
33
|
+
/** Write cards the caller read on its own, under the same newer-fold-wins rule as a commit. */
|
|
34
|
+
applyCards: (cards: Workcard[], options?: PlanningMirrorOptions) => Promise<void>
|
|
35
|
+
}
|
package/src/events.ts
CHANGED
|
@@ -1,121 +1,131 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
3
|
-
import {
|
|
1
|
+
import { memoHelper } from '@owlmeans/context'
|
|
2
|
+
import { CommitState, TransitionAction, WorkcardKind, type CommitEvent, type PlanningFacade, type TransitionReceiptView, type Workcard } from '@owlmeans/planning'
|
|
3
|
+
import type { PlanningMirror, PlanningMirrorOptions } from './events/types.js'
|
|
4
|
+
import { syncHelper } from './sync.js'
|
|
4
5
|
import type { PlanningStores } from './types.js'
|
|
5
|
-
import {
|
|
6
|
+
import { recordUtils } from './utils/record.js'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const known = await stores.cards.load(event.card)
|
|
8
|
+
export const makePlanningMirror = (stores: PlanningStores): PlanningMirror => {
|
|
9
|
+
const local = async (options: PlanningMirrorOptions | undefined, work: () => Promise<void>): Promise<void> =>
|
|
10
|
+
options == null ? await work() : await options.lifecycle.mutate(options.operation, work)
|
|
11
|
+
const read = async <T>(options: PlanningMirrorOptions | undefined, pending: Promise<T>): Promise<T> =>
|
|
12
|
+
options == null ? await pending : await options.operation.wait(pending)
|
|
13
|
+
const applyCommitEvent = async (event: CommitEvent, facade?: PlanningFacade, options?: PlanningMirrorOptions): Promise<void> => {
|
|
14
|
+
options?.operation.check()
|
|
15
|
+
// Resolve network references before entering the local mutation queue. Close must never
|
|
16
|
+
// wait for a lost card/link read, and sequence comparisons happen against the latest mirror.
|
|
17
|
+
const settled = event.state === CommitState.Committed && event.action !== TransitionAction.Delete
|
|
18
|
+
const record = event.record !== undefined ? event.record
|
|
19
|
+
: settled && facade != null ? await read(options, facade.cards.load(event.card)) : undefined
|
|
20
|
+
const links = settled && facade != null && (event.action === TransitionAction.Link || event.action === TransitionAction.Unlink)
|
|
21
|
+
? await read(options, facade.relationships.list({ from: event.card, size: 0 })) : undefined
|
|
22
|
+
await local(options, async () => {
|
|
23
|
+
const known = await stores.cards.load(event.card)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
await recordUtils.putCommit(stores.commits, {
|
|
26
|
+
id: event.transition,
|
|
27
|
+
card: event.card,
|
|
28
|
+
entityId: event.entityId,
|
|
29
|
+
project: event.project,
|
|
30
|
+
kind: event.kind,
|
|
31
|
+
type: event.type,
|
|
32
|
+
seq: event.seq,
|
|
33
|
+
action: event.action,
|
|
34
|
+
state: event.state,
|
|
35
|
+
at: event.at,
|
|
36
|
+
error: event.error,
|
|
37
|
+
})
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
if (event.state === CommitState.Failed) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
if (event.state === CommitState.Pending) {
|
|
43
|
+
if (known != null && (known.head ?? known.seq) < event.seq) {
|
|
44
|
+
await stores.cards.save({ ...known, head: event.seq })
|
|
45
|
+
}
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (known != null && known.seq > event.seq) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
if (event.action === TransitionAction.Delete) {
|
|
53
|
+
await recordUtils.dropCard(stores, event.card, event.kind === WorkcardKind.Project)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
if (record === null) {
|
|
58
|
+
// Committed, and gone by the time it was read: a later delete won the race.
|
|
59
|
+
await recordUtils.dropCard(stores, event.card, event.kind === WorkcardKind.Project)
|
|
60
|
+
} else if (record !== undefined) {
|
|
61
|
+
await recordUtils.putCard(stores.cards, record)
|
|
62
|
+
}
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
await putCard(stores.cards, record)
|
|
64
|
+
if (links != null) {
|
|
65
|
+
await syncHelper.syncLinks(stores.links, links.items, { from: event.card })
|
|
66
|
+
}
|
|
67
|
+
})
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
const applyReceipt = async (receipt: TransitionReceiptView, options?: PlanningMirrorOptions): Promise<void> => local(options, async () => {
|
|
71
|
+
const { transition } = receipt
|
|
72
|
+
if (transition.id != null) {
|
|
73
|
+
await recordUtils.putCommit(stores.commits, {
|
|
74
|
+
id: transition.id,
|
|
75
|
+
card: transition.card,
|
|
76
|
+
entityId: transition.entityId,
|
|
77
|
+
project: transition.project,
|
|
78
|
+
kind: transition.kind,
|
|
79
|
+
type: transition.type,
|
|
80
|
+
seq: transition.seq,
|
|
81
|
+
action: transition.action,
|
|
82
|
+
state: transition.commit.state,
|
|
83
|
+
at: transition.commit.at,
|
|
84
|
+
error: transition.commit.error,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
if (transition.commit.state === CommitState.Failed) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
if (transition.commit.state === CommitState.Committed && transition.action === TransitionAction.Delete) {
|
|
91
|
+
await recordUtils.dropCard(stores, transition.card, transition.kind === WorkcardKind.Project)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if (receipt.card != null) {
|
|
95
|
+
await recordUtils.putCard(stores.cards, receipt.card)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
73
98
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
* card writes it; a committed delete drops the row.
|
|
80
|
-
*/
|
|
81
|
-
export const applyReceipt = async (stores: PlanningStores, receipt: TransitionReceiptView): Promise<void> => {
|
|
82
|
-
const { transition } = receipt
|
|
83
|
-
if (transition.id != null) {
|
|
84
|
-
await putCommit(stores.commits, {
|
|
85
|
-
id: transition.id,
|
|
86
|
-
card: transition.card,
|
|
87
|
-
entityId: transition.entityId,
|
|
88
|
-
project: transition.project,
|
|
89
|
-
kind: transition.kind,
|
|
90
|
-
type: transition.type,
|
|
91
|
-
seq: transition.seq,
|
|
92
|
-
action: transition.action,
|
|
93
|
-
state: transition.commit.state,
|
|
94
|
-
at: transition.commit.at,
|
|
95
|
-
error: transition.commit.error,
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
if (transition.commit.state === CommitState.Failed) {
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
if (transition.commit.state === CommitState.Committed && transition.action === TransitionAction.Delete) {
|
|
102
|
-
await dropCard(stores, transition.card, transition.kind === WorkcardKind.Project)
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
if (receipt.card != null) {
|
|
106
|
-
await putCard(stores.cards, receipt.card)
|
|
107
|
-
return
|
|
108
|
-
}
|
|
99
|
+
const known = await stores.cards.load(transition.card)
|
|
100
|
+
if (known != null && (known.head ?? known.seq) < transition.seq) {
|
|
101
|
+
await stores.cards.save({ ...known, head: transition.seq })
|
|
102
|
+
}
|
|
103
|
+
})
|
|
109
104
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
105
|
+
const applyCards = async (cards: Workcard[], options?: PlanningMirrorOptions): Promise<void> => local(options, async () => {
|
|
106
|
+
for (const card of cards) {
|
|
107
|
+
await recordUtils.putCard(stores.cards, card)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
115
110
|
|
|
116
|
-
|
|
117
|
-
export const applyCards = async (stores: Pick<PlanningStores, 'cards'>, cards: Workcard[]): Promise<void> => {
|
|
118
|
-
for (const card of cards) {
|
|
119
|
-
await putCard(stores.cards, card)
|
|
120
|
-
}
|
|
111
|
+
return { applyCommitEvent, applyReceipt, applyCards }
|
|
121
112
|
}
|
|
113
|
+
|
|
114
|
+
/** The mirror of one set of stores — one per stores object. */
|
|
115
|
+
export const planningMirrorOf = memoHelper.oncePer(makePlanningMirror)
|
|
116
|
+
|
|
117
|
+
/** @deprecated compat:factory-refactor — use `planningMirrorOf(stores).applyCommitEvent(…)` */
|
|
118
|
+
export const applyCommitEvent = async (
|
|
119
|
+
stores: PlanningStores, event: CommitEvent, facade?: PlanningFacade
|
|
120
|
+
): Promise<void> => await planningMirrorOf(stores).applyCommitEvent(event, facade)
|
|
121
|
+
|
|
122
|
+
/** @deprecated compat:factory-refactor — use `planningMirrorOf(stores).applyReceipt(…)` */
|
|
123
|
+
export const applyReceipt = async (stores: PlanningStores, receipt: TransitionReceiptView): Promise<void> =>
|
|
124
|
+
await planningMirrorOf(stores).applyReceipt(receipt)
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @deprecated compat:factory-refactor — use `planningMirrorOf(stores).applyCards(…)`. Only `cards` is
|
|
128
|
+
* read, so a narrower stores object still works here.
|
|
129
|
+
*/
|
|
130
|
+
export const applyCards = async (stores: Pick<PlanningStores, 'cards'>, cards: Workcard[]): Promise<void> =>
|
|
131
|
+
await planningMirrorOf(stores as PlanningStores).applyCards(cards)
|
package/src/facade.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import type { ListResult } from '@owlmeans/resource'
|
|
1
2
|
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
2
3
|
import {
|
|
3
|
-
CommitFailed, CommitState, DEFAULT_COMMIT_TIMEOUT, PlanningError, PlanningUnsupported,
|
|
4
|
-
|
|
5
|
-
encodeWorkcardQuery, modelOf,
|
|
4
|
+
CommitFailed, CommitState, DEFAULT_COMMIT_TIMEOUT, modelOf, PlanningError, PlanningUnsupported, wireHelper,
|
|
5
|
+
WorkcardKind, WorkcardNotFound, planningReplyHelper,
|
|
6
6
|
} from '@owlmeans/planning'
|
|
7
7
|
import type {
|
|
8
|
-
PlanningFacade, PlanningProtocols, PlanningScope, TransitionReceipt,
|
|
9
|
-
TransitionReceiptView, Workcard, WorkcardModel,
|
|
8
|
+
PlanningFacade, PlanningProtocols, PlanningScope, PlanningReply, TransitionReceipt,
|
|
9
|
+
TransitionReceiptView, Workcard, WorkcardModel, Specification, Relationship, Transition, SpecificationRevisionList, SummaryView,
|
|
10
10
|
} from '@owlmeans/planning'
|
|
11
|
-
import {
|
|
11
|
+
import { makeRemoteResourceFacade } from './resources.js'
|
|
12
|
+
import { planningMirrorOf } from './events.js'
|
|
13
|
+
import { makePlanningClientLifecycle } from './lifecycle.js'
|
|
12
14
|
import type { RemoteFacadeOptions } from './types.js'
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -30,8 +32,12 @@ export const makeRemoteFacade = <C extends BasicConfig, T extends BasicContext<C
|
|
|
30
32
|
): PlanningFacade => {
|
|
31
33
|
const timeout = opts.timeout
|
|
32
34
|
const { commits } = opts
|
|
35
|
+
const lifecycle = opts.lifecycle ?? makePlanningClientLifecycle({ scopeKey: opts.scopeKey })
|
|
33
36
|
|
|
34
|
-
const
|
|
37
|
+
const scopedCall = async <R>(run: (signal: AbortSignal) => Promise<PlanningReply<R>>): Promise<R> =>
|
|
38
|
+
lifecycle.run(async operation => planningReplyHelper.hydrate<R>(await operation.wait(run(operation.signal))))
|
|
39
|
+
|
|
40
|
+
const receiptOf = (view: TransitionReceiptView, key: string): TransitionReceipt => {
|
|
35
41
|
const id = view.transition.id
|
|
36
42
|
if (id == null) {
|
|
37
43
|
throw new PlanningError('malformed:receipt-without-transition')
|
|
@@ -41,26 +47,30 @@ export const makeRemoteFacade = <C extends BasicConfig, T extends BasicContext<C
|
|
|
41
47
|
transition: view.transition,
|
|
42
48
|
...(view.card !== undefined ? { card: view.card } : {}),
|
|
43
49
|
committed: async waitOpts => {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
const operation = lifecycle.capture(key)
|
|
51
|
+
try {
|
|
52
|
+
const { state, error } = view.transition.commit
|
|
53
|
+
if (state === CommitState.Failed) {
|
|
54
|
+
throw new CommitFailed(error ?? id)
|
|
55
|
+
}
|
|
56
|
+
if (state === CommitState.Committed && view.card !== undefined) {
|
|
57
|
+
return view.card
|
|
58
|
+
}
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
return await operation.wait(commits.wait(id, { timeout: waitOpts?.timeout ?? DEFAULT_COMMIT_TIMEOUT }))
|
|
61
|
+
} finally { operation.release() }
|
|
53
62
|
},
|
|
54
63
|
}
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
const facade: PlanningFacade = {
|
|
67
|
+
...makeRemoteResourceFacade(context, protocols, { ...opts, lifecycle }),
|
|
58
68
|
scope: { ...scope, entityId: scope.entityId ?? '' },
|
|
59
69
|
|
|
60
70
|
schemas: opts.schemas,
|
|
61
71
|
|
|
62
72
|
cards: {
|
|
63
|
-
get: async id => await context.entrypoint(protocols.card.get).call({ params: { id }, timeout }),
|
|
73
|
+
get: async id => await scopedCall<Workcard>(signal => context.entrypoint(protocols.card.get).call({ params: { id }, timeout, signal })),
|
|
64
74
|
|
|
65
75
|
load: async id => {
|
|
66
76
|
try {
|
|
@@ -73,58 +83,58 @@ export const makeRemoteFacade = <C extends BasicConfig, T extends BasicContext<C
|
|
|
73
83
|
}
|
|
74
84
|
},
|
|
75
85
|
|
|
76
|
-
list: async query => await context.entrypoint(protocols.card.list).call({
|
|
77
|
-
query: encodeWorkcardQuery(query), timeout,
|
|
78
|
-
}),
|
|
86
|
+
list: async query => await scopedCall<ListResult<Workcard>>(signal => context.entrypoint(protocols.card.list).call({
|
|
87
|
+
query: wireHelper.encodeWorkcardQuery(query), timeout, signal,
|
|
88
|
+
})),
|
|
79
89
|
|
|
80
90
|
count: async query => {
|
|
81
91
|
const { page: _page, size: _size, sort: _sort, ...where } = query ?? {}
|
|
82
|
-
const answer = await context.entrypoint(protocols.card.list).call({
|
|
83
|
-
query: encodeWorkcardQuery({ ...where, page: 0, size: 1 }), timeout,
|
|
84
|
-
})
|
|
92
|
+
const answer = await scopedCall<ListResult<Workcard>>(signal => context.entrypoint(protocols.card.list).call({
|
|
93
|
+
query: wireHelper.encodeWorkcardQuery({ ...where, page: 0, size: 1 }), timeout, signal,
|
|
94
|
+
}))
|
|
85
95
|
|
|
86
96
|
return answer.total
|
|
87
97
|
},
|
|
88
98
|
|
|
89
|
-
summary: async (parents, query) => await context.entrypoint(protocols.card.summary).call({
|
|
90
|
-
query: encodeSummaryQuery({ ...query, parents }), timeout,
|
|
91
|
-
}),
|
|
99
|
+
summary: async (parents, query) => await scopedCall<SummaryView>(signal => context.entrypoint(protocols.card.summary).call({
|
|
100
|
+
query: wireHelper.encodeSummaryQuery({ ...query, parents }), timeout, signal,
|
|
101
|
+
})),
|
|
92
102
|
},
|
|
93
103
|
|
|
94
104
|
specifications: {
|
|
95
105
|
current: async (parent, category) => {
|
|
96
|
-
const answer = await context.entrypoint(protocols.card.specifications).call({
|
|
97
|
-
params: { id: parent }, query: encodeSpecificationQuery({ category }), timeout,
|
|
98
|
-
})
|
|
106
|
+
const answer = await scopedCall<ListResult<Specification>>(signal => context.entrypoint(protocols.card.specifications).call({
|
|
107
|
+
params: { id: parent }, query: wireHelper.encodeSpecificationQuery({ category }), timeout, signal,
|
|
108
|
+
}))
|
|
99
109
|
|
|
100
110
|
return answer.items[0] ?? null
|
|
101
111
|
},
|
|
102
112
|
|
|
103
|
-
list: async (parent, query) => await context.entrypoint(protocols.card.specifications).call({
|
|
104
|
-
params: { id: parent }, query: encodeSpecificationQuery(query), timeout,
|
|
105
|
-
}),
|
|
113
|
+
list: async (parent, query) => await scopedCall<ListResult<Specification>>(signal => context.entrypoint(protocols.card.specifications).call({
|
|
114
|
+
params: { id: parent }, query: wireHelper.encodeSpecificationQuery(query), timeout, signal,
|
|
115
|
+
})),
|
|
106
116
|
|
|
107
|
-
get: async id => await context.entrypoint(protocols.spec.get).call({ params: { id }, timeout }),
|
|
117
|
+
get: async id => await scopedCall<Specification>(signal => context.entrypoint(protocols.spec.get).call({ params: { id }, timeout, signal })),
|
|
108
118
|
|
|
109
119
|
revisions: async (id, limit) => {
|
|
110
|
-
const answer = await context.entrypoint(protocols.spec.revisions).call({
|
|
111
|
-
params: { id }, query: limit != null ? { limit } : {}, timeout,
|
|
112
|
-
})
|
|
120
|
+
const answer = await scopedCall<SpecificationRevisionList>(signal => context.entrypoint(protocols.spec.revisions).call({
|
|
121
|
+
params: { id }, query: limit != null ? { limit } : {}, timeout, signal,
|
|
122
|
+
}))
|
|
113
123
|
|
|
114
124
|
return answer.items
|
|
115
125
|
},
|
|
116
126
|
},
|
|
117
127
|
|
|
118
128
|
relationships: {
|
|
119
|
-
list: async query => await context.entrypoint(protocols.link.list).call({
|
|
120
|
-
query: encodeRelationshipQuery(query), timeout,
|
|
121
|
-
}),
|
|
129
|
+
list: async query => await scopedCall<ListResult<Relationship>>(signal => context.entrypoint(protocols.link.list).call({
|
|
130
|
+
query: wireHelper.encodeRelationshipQuery(query), timeout, signal,
|
|
131
|
+
})),
|
|
122
132
|
},
|
|
123
133
|
|
|
124
134
|
transitions: {
|
|
125
|
-
get: async id => await context.entrypoint(protocols.transition.get).call({
|
|
126
|
-
params: { transition: id }, timeout,
|
|
127
|
-
}),
|
|
135
|
+
get: async id => await scopedCall<Transition>(signal => context.entrypoint(protocols.transition.get).call({
|
|
136
|
+
params: { transition: id }, timeout, signal,
|
|
137
|
+
})),
|
|
128
138
|
|
|
129
139
|
list: async query => {
|
|
130
140
|
// The tree reads the log through ONE card; a project-wide read has no route to travel.
|
|
@@ -132,36 +142,58 @@ export const makeRemoteFacade = <C extends BasicConfig, T extends BasicContext<C
|
|
|
132
142
|
throw new PlanningUnsupported('client:transitions-without-card')
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
return await context.entrypoint(protocols.card.transitions).call({
|
|
136
|
-
params: { id: query.card }, query: encodeTransitionQuery(query), timeout,
|
|
137
|
-
})
|
|
145
|
+
return await scopedCall<ListResult<Transition>>(signal => context.entrypoint(protocols.card.transitions).call({
|
|
146
|
+
params: { id: query.card! }, query: wireHelper.encodeTransitionQuery(query), timeout, signal,
|
|
147
|
+
}))
|
|
138
148
|
},
|
|
139
149
|
},
|
|
140
150
|
|
|
141
151
|
commits,
|
|
142
152
|
|
|
143
|
-
execute: async (exec, executeOpts) => {
|
|
153
|
+
execute: async (exec, executeOpts) => lifecycle.run(async operation => {
|
|
144
154
|
// Neither the actor nor a hold travels: the server decides who wrote, and the wait is ours.
|
|
145
155
|
const { actor: _actor, ...body } = exec
|
|
146
|
-
const view = await context.entrypoint(protocols.execute).call({ body, timeout })
|
|
156
|
+
const view = planningReplyHelper.hydrate<TransitionReceiptView>(await operation.wait(context.entrypoint(protocols.execute).call({ body, timeout, signal: operation.signal })))
|
|
147
157
|
const stores = opts.stores?.()
|
|
148
158
|
if (stores != null) {
|
|
149
|
-
await applyReceipt(
|
|
159
|
+
await planningMirrorOf(stores).applyReceipt(view, { lifecycle, operation })
|
|
150
160
|
}
|
|
151
|
-
|
|
161
|
+
operation.check()
|
|
162
|
+
const receipt = receiptOf(view, operation.key)
|
|
152
163
|
if (executeOpts?.wait === true) {
|
|
153
|
-
receipt.card = await receipt.committed({ timeout: executeOpts.timeout })
|
|
164
|
+
receipt.card = await operation.wait(receipt.committed({ timeout: executeOpts.timeout }))
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
return receipt
|
|
157
|
-
},
|
|
168
|
+
}),
|
|
169
|
+
|
|
170
|
+
model: async <R extends Workcard = Workcard>(card: R | string): Promise<WorkcardModel<R>> => lifecycle.run(async operation => {
|
|
171
|
+
if (opts.loadSchemas != null) await operation.wait(opts.loadSchemas())
|
|
172
|
+
const record = typeof card === 'string' ? await operation.wait(facade.cards.get(card)) as R : card
|
|
173
|
+
// A card of a project resolves its types in that project's layer; the organization's layer
|
|
174
|
+
// is the service's own bundle, and a specification's type is always code's.
|
|
175
|
+
let project = opts.definitions == null ? undefined : record.kind === WorkcardKind.Project ? record.id : undefined
|
|
176
|
+
if (opts.definitions != null && record.kind === WorkcardKind.Card) {
|
|
177
|
+
let parent = record.parent
|
|
178
|
+
const seen = new Set<string>(record.id != null ? [record.id] : [])
|
|
179
|
+
while (parent != null) {
|
|
180
|
+
if (seen.has(parent)) throw new PlanningError('hierarchy:cycle')
|
|
181
|
+
seen.add(parent)
|
|
182
|
+
const ancestor = await operation.wait(facade.cards.get(parent))
|
|
183
|
+
if (ancestor.kind === WorkcardKind.Project) { project = ancestor.id; break }
|
|
184
|
+
parent = ancestor.parent
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (project == null) {
|
|
188
|
+
return modelOf<R>(record, facade)
|
|
189
|
+
}
|
|
158
190
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
191
|
+
return modelOf<R>(record, { ...facade, schemas: await operation.wait(opts.definitions!.registry(project)) })
|
|
192
|
+
}),
|
|
193
|
+
}
|
|
162
194
|
|
|
163
|
-
|
|
164
|
-
|
|
195
|
+
if (opts.definitions != null) {
|
|
196
|
+
facade.definitions = opts.definitions
|
|
165
197
|
}
|
|
166
198
|
|
|
167
199
|
return facade
|