@owlmeans/client-planning 0.1.18-rc.21 → 0.1.18-rc.22
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 +13 -8
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client-planning/SKILL.md +24 -23
- package/build/client.js +2 -2
- package/build/client.js.map +1 -1
- package/build/commits.js +4 -4
- package/build/commits.js.map +1 -1
- package/build/consts.d.ts +12 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +15 -0
- package/build/consts.js.map +1 -1
- package/build/definitions.js +2 -2
- package/build/definitions.js.map +1 -1
- package/build/events/types.d.ts +29 -0
- package/build/events/types.d.ts.map +1 -0
- package/build/events/types.js.map +1 -0
- package/build/events.d.ts +9 -20
- package/build/events.d.ts.map +1 -1
- package/build/events.js +97 -103
- package/build/events.js.map +1 -1
- package/build/facade.d.ts.map +1 -1
- package/build/facade.js +10 -10
- 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 +17 -15
- 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 +4 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/stores.d.ts +3 -10
- package/build/stores.d.ts.map +1 -1
- package/build/stores.js +9 -36
- 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/{utils → sync}/types.js.map +1 -1
- 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/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 -43
- package/build/utils/record.d.ts.map +1 -1
- package/build/utils/record.js +64 -72
- package/build/utils/record.js.map +1 -1
- package/package.json +15 -15
- package/src/client.ts +2 -2
- package/src/commits.ts +4 -4
- package/src/consts.ts +23 -0
- package/src/definitions.ts +2 -2
- package/src/events/types.ts +29 -0
- package/src/events.ts +105 -103
- package/src/facade.ts +11 -12
- package/src/feed.ts +17 -15
- package/src/helper/types.ts +15 -0
- package/src/helper.ts +27 -10
- package/src/index.ts +4 -1
- package/src/stores.ts +9 -39
- package/src/sync/types.ts +18 -0
- package/src/sync.ts +40 -0
- package/src/utils/record/types.ts +27 -0
- package/src/utils/record.ts +68 -75
- package/tests/events.spec.ts +12 -8
- package/tests/stores.spec.ts +13 -11
- package/build/utils/types.d.ts +0 -3
- package/build/utils/types.d.ts.map +0 -1
- package/build/utils.d.ts +0 -13
- package/build/utils.d.ts.map +0 -1
- package/build/utils.js +0 -15
- package/build/utils.js.map +0 -1
- package/src/utils/types.ts +0 -3
- package/src/utils.ts +0 -20
- /package/build/{utils → events}/types.js +0 -0
package/src/events.ts
CHANGED
|
@@ -1,120 +1,122 @@
|
|
|
1
|
+
import { memoHelper } from '@owlmeans/context'
|
|
1
2
|
import { CommitState, TransitionAction, WorkcardKind, type CommitEvent, type PlanningFacade, type TransitionReceiptView, type Workcard } from '@owlmeans/planning'
|
|
2
|
-
import {
|
|
3
|
+
import type { PlanningMirror } from './events/types.js'
|
|
4
|
+
import { syncHelper } from './sync.js'
|
|
3
5
|
import type { PlanningStores } from './types.js'
|
|
4
|
-
import {
|
|
6
|
+
import { recordUtils } from './utils/record.js'
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* - An OLDER `seq` never overwrites a newer record — frames and list answers race.
|
|
10
|
-
* - A committed `delete` REMOVES the row and every link touching it (and, for a project, the rows
|
|
11
|
-
* under it — the server purged them in the same fold).
|
|
12
|
-
* - A frame for an id the store never saw still writes a row: a card created in another tab
|
|
13
|
-
* appears here without waiting for the next list.
|
|
14
|
-
* - A `failed` commit leaves the card alone; the reason is recorded in the commit store.
|
|
15
|
-
*
|
|
16
|
-
* A frame from a cross-process bus carries ids only. When `record` is absent the card is re-read
|
|
17
|
-
* through `facade`; without one there is nothing to write but the commit itself.
|
|
18
|
-
*/
|
|
19
|
-
export const applyCommitEvent = async (
|
|
20
|
-
stores: PlanningStores, event: CommitEvent, facade?: PlanningFacade
|
|
21
|
-
): Promise<void> => {
|
|
22
|
-
const known = await stores.cards.load(event.card)
|
|
8
|
+
export const makePlanningMirror = (stores: PlanningStores): PlanningMirror => {
|
|
9
|
+
const applyCommitEvent = async (event: CommitEvent, facade?: PlanningFacade): Promise<void> => {
|
|
10
|
+
const known = await stores.cards.load(event.card)
|
|
23
11
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
12
|
+
await recordUtils.putCommit(stores.commits, {
|
|
13
|
+
id: event.transition,
|
|
14
|
+
card: event.card,
|
|
15
|
+
entityId: event.entityId,
|
|
16
|
+
project: event.project,
|
|
17
|
+
kind: event.kind,
|
|
18
|
+
type: event.type,
|
|
19
|
+
seq: event.seq,
|
|
20
|
+
action: event.action,
|
|
21
|
+
state: event.state,
|
|
22
|
+
at: event.at,
|
|
23
|
+
error: event.error,
|
|
24
|
+
})
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
if (event.state === CommitState.Failed) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
if (event.state === CommitState.Pending) {
|
|
30
|
+
if (known != null && (known.head ?? known.seq) < event.seq) {
|
|
31
|
+
await stores.cards.save({ ...known, head: event.seq })
|
|
32
|
+
}
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
if (known != null && known.seq > event.seq) {
|
|
36
|
+
return
|
|
44
37
|
}
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
if (known != null && known.seq > event.seq) {
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
38
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
if (event.action === TransitionAction.Delete) {
|
|
40
|
+
await recordUtils.dropCard(stores, event.card, event.kind === WorkcardKind.Project)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
55
43
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
44
|
+
const record: Workcard | null | undefined = event.record !== undefined
|
|
45
|
+
? event.record
|
|
46
|
+
: facade != null ? await facade.cards.load(event.card) : undefined
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
48
|
+
if (record === null) {
|
|
49
|
+
// Committed, and gone by the time it was read: a later delete won the race.
|
|
50
|
+
await recordUtils.dropCard(stores, event.card, event.kind === WorkcardKind.Project)
|
|
51
|
+
} else if (record !== undefined) {
|
|
52
|
+
await recordUtils.putCard(stores.cards, record)
|
|
53
|
+
}
|
|
66
54
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
if (facade != null && (event.action === TransitionAction.Link || event.action === TransitionAction.Unlink)) {
|
|
56
|
+
const links = await facade.relationships.list({ from: event.card, size: 0 })
|
|
57
|
+
await syncHelper.syncLinks(stores.links, links.items, { from: event.card })
|
|
58
|
+
}
|
|
70
59
|
}
|
|
71
|
-
}
|
|
72
60
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
await recordHelper.dropCard(stores, transition.card, transition.kind === WorkcardKind.Project)
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
if (receipt.card != null) {
|
|
105
|
-
await recordHelper.putCard(stores.cards, receipt.card)
|
|
106
|
-
return
|
|
107
|
-
}
|
|
61
|
+
const applyReceipt = async (receipt: TransitionReceiptView): Promise<void> => {
|
|
62
|
+
const { transition } = receipt
|
|
63
|
+
if (transition.id != null) {
|
|
64
|
+
await recordUtils.putCommit(stores.commits, {
|
|
65
|
+
id: transition.id,
|
|
66
|
+
card: transition.card,
|
|
67
|
+
entityId: transition.entityId,
|
|
68
|
+
project: transition.project,
|
|
69
|
+
kind: transition.kind,
|
|
70
|
+
type: transition.type,
|
|
71
|
+
seq: transition.seq,
|
|
72
|
+
action: transition.action,
|
|
73
|
+
state: transition.commit.state,
|
|
74
|
+
at: transition.commit.at,
|
|
75
|
+
error: transition.commit.error,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
if (transition.commit.state === CommitState.Failed) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (transition.commit.state === CommitState.Committed && transition.action === TransitionAction.Delete) {
|
|
82
|
+
await recordUtils.dropCard(stores, transition.card, transition.kind === WorkcardKind.Project)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
if (receipt.card != null) {
|
|
86
|
+
await recordUtils.putCard(stores.cards, receipt.card)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
108
89
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
90
|
+
const known = await stores.cards.load(transition.card)
|
|
91
|
+
if (known != null && (known.head ?? known.seq) < transition.seq) {
|
|
92
|
+
await stores.cards.save({ ...known, head: transition.seq })
|
|
93
|
+
}
|
|
112
94
|
}
|
|
113
|
-
}
|
|
114
95
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
96
|
+
const applyCards = async (cards: Workcard[]): Promise<void> => {
|
|
97
|
+
for (const card of cards) {
|
|
98
|
+
await recordUtils.putCard(stores.cards, card)
|
|
99
|
+
}
|
|
119
100
|
}
|
|
101
|
+
|
|
102
|
+
return { applyCommitEvent, applyReceipt, applyCards }
|
|
120
103
|
}
|
|
104
|
+
|
|
105
|
+
/** The mirror of one set of stores — one per stores object. */
|
|
106
|
+
export const planningMirrorOf = memoHelper.oncePer(makePlanningMirror)
|
|
107
|
+
|
|
108
|
+
/** @deprecated compat:factory-refactor — use `planningMirrorOf(stores).applyCommitEvent(…)` */
|
|
109
|
+
export const applyCommitEvent = async (
|
|
110
|
+
stores: PlanningStores, event: CommitEvent, facade?: PlanningFacade
|
|
111
|
+
): Promise<void> => await planningMirrorOf(stores).applyCommitEvent(event, facade)
|
|
112
|
+
|
|
113
|
+
/** @deprecated compat:factory-refactor — use `planningMirrorOf(stores).applyReceipt(…)` */
|
|
114
|
+
export const applyReceipt = async (stores: PlanningStores, receipt: TransitionReceiptView): Promise<void> =>
|
|
115
|
+
await planningMirrorOf(stores).applyReceipt(receipt)
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @deprecated compat:factory-refactor — use `planningMirrorOf(stores).applyCards(…)`. Only `cards` is
|
|
119
|
+
* read, so a narrower stores object still works here.
|
|
120
|
+
*/
|
|
121
|
+
export const applyCards = async (stores: Pick<PlanningStores, 'cards'>, cards: Workcard[]): Promise<void> =>
|
|
122
|
+
await planningMirrorOf(stores as PlanningStores).applyCards(cards)
|
package/src/facade.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
2
2
|
import {
|
|
3
|
-
CommitFailed, CommitState, DEFAULT_COMMIT_TIMEOUT, PlanningError, PlanningUnsupported,
|
|
4
|
-
|
|
5
|
-
encodeWorkcardQuery, modelOf,
|
|
3
|
+
CommitFailed, CommitState, DEFAULT_COMMIT_TIMEOUT, modelOf, PlanningError, PlanningUnsupported, wireHelper,
|
|
4
|
+
WorkcardKind, WorkcardNotFound,
|
|
6
5
|
} from '@owlmeans/planning'
|
|
7
6
|
import type {
|
|
8
7
|
PlanningFacade, PlanningProtocols, PlanningScope, TransitionReceipt,
|
|
9
8
|
TransitionReceiptView, Workcard, WorkcardModel,
|
|
10
9
|
} from '@owlmeans/planning'
|
|
11
|
-
import {
|
|
10
|
+
import { planningMirrorOf } from './events.js'
|
|
12
11
|
import type { RemoteFacadeOptions } from './types.js'
|
|
13
12
|
|
|
14
13
|
/**
|
|
@@ -74,34 +73,34 @@ export const makeRemoteFacade = <C extends BasicConfig, T extends BasicContext<C
|
|
|
74
73
|
},
|
|
75
74
|
|
|
76
75
|
list: async query => await context.entrypoint(protocols.card.list).call({
|
|
77
|
-
query: encodeWorkcardQuery(query), timeout,
|
|
76
|
+
query: wireHelper.encodeWorkcardQuery(query), timeout,
|
|
78
77
|
}),
|
|
79
78
|
|
|
80
79
|
count: async query => {
|
|
81
80
|
const { page: _page, size: _size, sort: _sort, ...where } = query ?? {}
|
|
82
81
|
const answer = await context.entrypoint(protocols.card.list).call({
|
|
83
|
-
query: encodeWorkcardQuery({ ...where, page: 0, size: 1 }), timeout,
|
|
82
|
+
query: wireHelper.encodeWorkcardQuery({ ...where, page: 0, size: 1 }), timeout,
|
|
84
83
|
})
|
|
85
84
|
|
|
86
85
|
return answer.total
|
|
87
86
|
},
|
|
88
87
|
|
|
89
88
|
summary: async (parents, query) => await context.entrypoint(protocols.card.summary).call({
|
|
90
|
-
query: encodeSummaryQuery({ ...query, parents }), timeout,
|
|
89
|
+
query: wireHelper.encodeSummaryQuery({ ...query, parents }), timeout,
|
|
91
90
|
}),
|
|
92
91
|
},
|
|
93
92
|
|
|
94
93
|
specifications: {
|
|
95
94
|
current: async (parent, category) => {
|
|
96
95
|
const answer = await context.entrypoint(protocols.card.specifications).call({
|
|
97
|
-
params: { id: parent }, query: encodeSpecificationQuery({ category }), timeout,
|
|
96
|
+
params: { id: parent }, query: wireHelper.encodeSpecificationQuery({ category }), timeout,
|
|
98
97
|
})
|
|
99
98
|
|
|
100
99
|
return answer.items[0] ?? null
|
|
101
100
|
},
|
|
102
101
|
|
|
103
102
|
list: async (parent, query) => await context.entrypoint(protocols.card.specifications).call({
|
|
104
|
-
params: { id: parent }, query: encodeSpecificationQuery(query), timeout,
|
|
103
|
+
params: { id: parent }, query: wireHelper.encodeSpecificationQuery(query), timeout,
|
|
105
104
|
}),
|
|
106
105
|
|
|
107
106
|
get: async id => await context.entrypoint(protocols.spec.get).call({ params: { id }, timeout }),
|
|
@@ -117,7 +116,7 @@ export const makeRemoteFacade = <C extends BasicConfig, T extends BasicContext<C
|
|
|
117
116
|
|
|
118
117
|
relationships: {
|
|
119
118
|
list: async query => await context.entrypoint(protocols.link.list).call({
|
|
120
|
-
query: encodeRelationshipQuery(query), timeout,
|
|
119
|
+
query: wireHelper.encodeRelationshipQuery(query), timeout,
|
|
121
120
|
}),
|
|
122
121
|
},
|
|
123
122
|
|
|
@@ -133,7 +132,7 @@ export const makeRemoteFacade = <C extends BasicConfig, T extends BasicContext<C
|
|
|
133
132
|
}
|
|
134
133
|
|
|
135
134
|
return await context.entrypoint(protocols.card.transitions).call({
|
|
136
|
-
params: { id: query.card }, query: encodeTransitionQuery(query), timeout,
|
|
135
|
+
params: { id: query.card }, query: wireHelper.encodeTransitionQuery(query), timeout,
|
|
137
136
|
})
|
|
138
137
|
},
|
|
139
138
|
},
|
|
@@ -146,7 +145,7 @@ export const makeRemoteFacade = <C extends BasicConfig, T extends BasicContext<C
|
|
|
146
145
|
const view = await context.entrypoint(protocols.execute).call({ body, timeout })
|
|
147
146
|
const stores = opts.stores?.()
|
|
148
147
|
if (stores != null) {
|
|
149
|
-
await applyReceipt(
|
|
148
|
+
await planningMirrorOf(stores).applyReceipt(view)
|
|
150
149
|
}
|
|
151
150
|
const receipt = receiptOf(view)
|
|
152
151
|
if (executeOpts?.wait === true) {
|
package/src/feed.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
2
2
|
import { logger } from '@owlmeans/log'
|
|
3
|
-
import {
|
|
3
|
+
import { PLANNING_SERVICE, queryHelper, type Relationship, type RelationshipQuery, type Unsubscribe } from '@owlmeans/planning'
|
|
4
4
|
import type { Criteria } from '@owlmeans/resource'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { planningMirrorOf } from './events.js'
|
|
6
|
+
import { planningContextOf } from './helper.js'
|
|
7
|
+
import { syncHelper } from './sync.js'
|
|
7
8
|
import type { PlanningClientService, PlanningFeed, PlanningFeedOptions, PlanningFeedState } from './types.js'
|
|
8
|
-
import {
|
|
9
|
+
import { recordUtils } from './utils/record.js'
|
|
9
10
|
|
|
10
11
|
const log = logger('client-planning')
|
|
11
12
|
|
|
12
13
|
const linkCriteriaOf = (query: RelationshipQuery): Criteria<Relationship> =>
|
|
13
|
-
|
|
14
|
+
recordUtils.clean({ from: query.from, to: query.to, type: query.type }) as Criteria<Relationship>
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Keep the state mirror current: subscribe to commits FIRST, then seed from the list, then fold
|
|
17
18
|
* every frame — and, with `refresh`, re-seed on an interval as the authoritative backstop to a
|
|
18
19
|
* socket that can drop frames. React-free: a hook wraps it, a Node process uses it as it is.
|
|
19
20
|
*
|
|
20
|
-
* The seed is `syncCards` over `where` (default `criteriaOf(query)`), never
|
|
21
|
-
* holds every kind, and a board's list must not drop the projects beside it. A
|
|
22
|
-
* while the list was in flight is kept even when the list does not name it yet. A
|
|
23
|
-
* writes — a page cannot say what does not exist.
|
|
21
|
+
* The seed is `syncHelper.syncCards` over `where` (default `queryHelper.criteriaOf(query)`), never
|
|
22
|
+
* `replace()`: the store holds every kind, and a board's list must not drop the projects beside it. A
|
|
23
|
+
* card a frame wrote while the list was in flight is kept even when the list does not name it yet. A
|
|
24
|
+
* PAGED seed only writes — a page cannot say what does not exist.
|
|
24
25
|
*
|
|
25
26
|
* Without a socket opener the feed is a seed plus its refresh; `commits.wait` still long-polls.
|
|
26
27
|
*
|
|
@@ -29,12 +30,13 @@ const linkCriteriaOf = (query: RelationshipQuery): Criteria<Relationship> =>
|
|
|
29
30
|
export const makePlanningFeed = <C extends BasicConfig, T extends BasicContext<C>>(
|
|
30
31
|
context: T, opts: PlanningFeedOptions = {}
|
|
31
32
|
): PlanningFeed => {
|
|
32
|
-
const stores =
|
|
33
|
+
const stores = planningContextOf(context).stores()
|
|
33
34
|
if (stores == null) {
|
|
34
35
|
throw new SyntaxError('Planning feed needs appendPlanningStores(context)')
|
|
35
36
|
}
|
|
36
37
|
const service = context.service<PlanningClientService>(PLANNING_SERVICE)
|
|
37
38
|
const facade = service.for(opts.scope)
|
|
39
|
+
const mirror = planningMirrorOf(stores)
|
|
38
40
|
|
|
39
41
|
const state: PlanningFeedState = { connected: false, seeded: false, error: null }
|
|
40
42
|
const report = (patch: Partial<PlanningFeedState>): void => {
|
|
@@ -47,7 +49,7 @@ export const makePlanningFeed = <C extends BasicConfig, T extends BasicContext<C
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const paged = opts.query?.size != null && opts.query.size > 0
|
|
50
|
-
const where = opts.where ?? criteriaOf(opts.query)
|
|
52
|
+
const where = opts.where ?? queryHelper.criteriaOf(opts.query)
|
|
51
53
|
const touched = new Set<string>()
|
|
52
54
|
let seeding = 0
|
|
53
55
|
let stopped = false
|
|
@@ -65,14 +67,14 @@ export const makePlanningFeed = <C extends BasicConfig, T extends BasicContext<C
|
|
|
65
67
|
return
|
|
66
68
|
}
|
|
67
69
|
if (paged) {
|
|
68
|
-
await applyCards(
|
|
70
|
+
await mirror.applyCards(answer.items)
|
|
69
71
|
} else {
|
|
70
|
-
await syncCards(stores.cards, answer.items, where, { keep: touched })
|
|
72
|
+
await syncHelper.syncCards(stores.cards, answer.items, where, { keep: touched })
|
|
71
73
|
}
|
|
72
74
|
if (opts.links != null) {
|
|
73
75
|
const links = await facade.relationships.list({ ...opts.links, size: opts.links.size ?? 0 })
|
|
74
76
|
if (!stopped) {
|
|
75
|
-
await syncLinks(stores.links, links.items, linkCriteriaOf(opts.links))
|
|
77
|
+
await syncHelper.syncLinks(stores.links, links.items, linkCriteriaOf(opts.links))
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
report({ seeded: true, error: null })
|
|
@@ -95,7 +97,7 @@ export const makePlanningFeed = <C extends BasicConfig, T extends BasicContext<C
|
|
|
95
97
|
touched.add(event.card)
|
|
96
98
|
}
|
|
97
99
|
try {
|
|
98
|
-
await applyCommitEvent(
|
|
100
|
+
await mirror.applyCommitEvent(event, facade)
|
|
99
101
|
} catch (e) {
|
|
100
102
|
log.warn('Planning feed apply failed', { card: event.card, error: e })
|
|
101
103
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PlanningFacade, PlanningScope, Workcard, WorkcardModel } from '@owlmeans/planning'
|
|
2
|
+
import type { PlanningStores } from '../types.js'
|
|
3
|
+
|
|
4
|
+
/** The planning client of one context, without the `WithPlanningClient` mixin type. */
|
|
5
|
+
export interface PlanningContextHelper {
|
|
6
|
+
/** The facade of the context's planning client — `context.planning().for(scope)`. */
|
|
7
|
+
facade: (scope?: Partial<PlanningScope>) => PlanningFacade
|
|
8
|
+
/**
|
|
9
|
+
* The model of a card (read by id when given one), with the schema bundle loaded first so `can()` and
|
|
10
|
+
* `available()` answer from real flows. Built by `@owlmeans/planning`'s `modelOf(record, facade)`.
|
|
11
|
+
*/
|
|
12
|
+
model: <R extends Workcard = Workcard>(card: R | string, scope?: Partial<PlanningScope>) => Promise<WorkcardModel<R>>
|
|
13
|
+
/** The stores `appendPlanningStores` registered, or `null` on a context that has none. */
|
|
14
|
+
stores: () => PlanningStores | null
|
|
15
|
+
}
|
package/src/helper.ts
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { memoHelper, type BasicConfig, type BasicContext } from '@owlmeans/context'
|
|
2
2
|
import { PLANNING_SERVICE } from '@owlmeans/planning'
|
|
3
3
|
import type { PlanningFacade, PlanningScope, Workcard, WorkcardModel } from '@owlmeans/planning'
|
|
4
|
-
import type {
|
|
4
|
+
import type { PlanningContextHelper } from './helper/types.js'
|
|
5
|
+
import type { PlanningClientService, PlanningStores, WithPlanningStores } from './types.js'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
export const makePlanningContextHelper = (context: BasicContext<BasicConfig>): PlanningContextHelper => {
|
|
8
|
+
const facade = (scope?: Partial<PlanningScope>): PlanningFacade =>
|
|
9
|
+
context.service<PlanningClientService>(PLANNING_SERVICE).for(scope)
|
|
10
|
+
|
|
11
|
+
const model = async <R extends Workcard = Workcard>(
|
|
12
|
+
card: R | string, scope?: Partial<PlanningScope>
|
|
13
|
+
): Promise<WorkcardModel<R>> => await facade(scope).model<R>(card)
|
|
14
|
+
|
|
15
|
+
const stores = (): PlanningStores | null => {
|
|
16
|
+
const ctx = context as Partial<WithPlanningStores>
|
|
17
|
+
|
|
18
|
+
return typeof ctx.planningStores === 'function' ? ctx.planningStores() : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { facade, model, stores }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The planning helper of a context — one per context. */
|
|
25
|
+
export const planningContextOf = memoHelper.oncePer(makePlanningContextHelper)
|
|
26
|
+
|
|
27
|
+
/** @deprecated compat:factory-refactor — use `planningContextOf(context).facade(…)` */
|
|
7
28
|
export const planningOf = <C extends BasicConfig, T extends BasicContext<C>>(
|
|
8
29
|
context: T, scope?: Partial<PlanningScope>
|
|
9
|
-
): PlanningFacade => context
|
|
30
|
+
): PlanningFacade => planningContextOf(context).facade(scope)
|
|
10
31
|
|
|
11
|
-
/**
|
|
12
|
-
* The model of a card (read by id when given one), with the schema bundle loaded first so `can()` and
|
|
13
|
-
* `available()` answer from real flows. Named apart from `@owlmeans/planning`'s `modelOf(record,
|
|
14
|
-
* facade)`, which it calls.
|
|
15
|
-
*/
|
|
32
|
+
/** @deprecated compat:factory-refactor — use `planningContextOf(context).model(…)` */
|
|
16
33
|
export const planningModelOf = async <R extends Workcard = Workcard, C extends BasicConfig = BasicConfig, T extends BasicContext<C> = BasicContext<C>>(
|
|
17
34
|
context: T, card: R | string, scope?: Partial<PlanningScope>
|
|
18
|
-
): Promise<WorkcardModel<R>> => await
|
|
35
|
+
): Promise<WorkcardModel<R>> => await planningContextOf(context).model<R>(card, scope)
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export type * from './types.js'
|
|
2
|
+
export type * from './events/types.js'
|
|
3
|
+
export type * from './helper/types.js'
|
|
4
|
+
export type * from './sync/types.js'
|
|
2
5
|
|
|
3
6
|
export * from './consts.js'
|
|
4
|
-
export * from './utils.js'
|
|
5
7
|
export * from './client.js'
|
|
6
8
|
export * from './facade.js'
|
|
7
9
|
export * from './definitions.js'
|
|
8
10
|
export * from './commits.js'
|
|
9
11
|
export * from './stores.js'
|
|
12
|
+
export * from './sync.js'
|
|
10
13
|
export * from './events.js'
|
|
11
14
|
export * from './feed.js'
|
|
12
15
|
export * from './helper.js'
|
package/src/stores.ts
CHANGED
|
@@ -2,9 +2,10 @@ import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
|
2
2
|
import type { Relationship, Workcard } from '@owlmeans/planning'
|
|
3
3
|
import type { Criteria } from '@owlmeans/resource'
|
|
4
4
|
import { appendStateResource, type StateResourceAppend } from '@owlmeans/state'
|
|
5
|
-
import { DEFAULT_STORE_ALIASES } from './
|
|
5
|
+
import { DEFAULT_STORE_ALIASES } from './consts.js'
|
|
6
|
+
import { planningContextOf } from './helper.js'
|
|
7
|
+
import { syncHelper } from './sync.js'
|
|
6
8
|
import type { PlanningStoreAliases, PlanningStores, SyncOptions, WithPlanningStores } from './types.js'
|
|
7
|
-
import { recordHelper } from './utils/record.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Register the planning state mirror: ONE card store for every kind, a link store and a commit
|
|
@@ -34,47 +35,16 @@ export const appendPlanningStores = <C extends BasicConfig, T extends BasicConte
|
|
|
34
35
|
return result
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
/** The stores `appendPlanningStores` registered, or `null` on a context that has none. */
|
|
38
|
-
export const planningStoresOf = (context: BasicContext<any>): PlanningStores | null => {
|
|
39
|
-
const ctx = context as Partial<WithPlanningStores>
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
/** @deprecated compat:factory-refactor — use `planningContextOf(context).stores()` */
|
|
40
|
+
export const planningStoresOf = (context: BasicContext<any>): PlanningStores | null => planningContextOf(context).stores()
|
|
43
41
|
|
|
44
|
-
/**
|
|
45
|
-
* Make the store agree with an authoritative list WITHIN a scope: every card given is written
|
|
46
|
-
* (unless the store holds a newer fold of it), and every card matching `where` that the list does
|
|
47
|
-
* not name is dropped. Cards outside `where` are left alone — which is why this, and never
|
|
48
|
-
* `replace()`, is how a list reaches the one shared card store.
|
|
49
|
-
*
|
|
50
|
-
* Unchanged cards are not rewritten, so a periodic re-seed wakes no subscriber.
|
|
51
|
-
*/
|
|
42
|
+
/** @deprecated compat:factory-refactor — use `syncHelper.syncCards(…)` */
|
|
52
43
|
export const syncCards = async (
|
|
53
44
|
store: PlanningStores['cards'], items: Workcard[], where?: Criteria<Workcard>, opts?: SyncOptions
|
|
54
|
-
): Promise<void> =>
|
|
55
|
-
await dropUnnamed(store, recordHelper.idsOf(items), where, opts)
|
|
56
|
-
for (const item of items) {
|
|
57
|
-
await recordHelper.putCard(store, item)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
45
|
+
): Promise<void> => await syncHelper.syncCards(store, items, where, opts)
|
|
60
46
|
|
|
61
|
-
/**
|
|
47
|
+
/** @deprecated compat:factory-refactor — use `syncHelper.syncLinks(…)` */
|
|
62
48
|
export const syncLinks = async (
|
|
63
49
|
store: PlanningStores['links'], items: Relationship[], where?: Criteria<Relationship>, opts?: SyncOptions
|
|
64
|
-
): Promise<void> =>
|
|
65
|
-
await dropUnnamed(store, recordHelper.idsOf(items), where, opts)
|
|
66
|
-
for (const item of items) {
|
|
67
|
-
await recordHelper.putLink(store, item)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const dropUnnamed = async <T extends Workcard | Relationship>(
|
|
72
|
-
store: PlanningStores['cards'] | PlanningStores['links'], named: string[], where?: Criteria<T>, opts?: SyncOptions
|
|
73
|
-
): Promise<void> => {
|
|
74
|
-
const keep = new Set([...named, ...(opts?.keep ?? [])])
|
|
75
|
-
const scoped = await (store as PlanningStores['cards']).list(where as Criteria<Workcard>)
|
|
76
|
-
const stale = recordHelper.idsOf(scoped.items).filter(id => !keep.has(id))
|
|
77
|
-
if (stale.length > 0) {
|
|
78
|
-
await store.purge({ id: { $in: stale } })
|
|
79
|
-
}
|
|
80
|
-
}
|
|
50
|
+
): Promise<void> => await syncHelper.syncLinks(store, items, where, opts)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Relationship, Workcard } from '@owlmeans/planning'
|
|
2
|
+
import type { Criteria } from '@owlmeans/resource'
|
|
3
|
+
import type { PlanningStores, SyncOptions } from '../types.js'
|
|
4
|
+
|
|
5
|
+
/** An authoritative list made true in one mirror store — within a scope, never beyond it. */
|
|
6
|
+
export interface SyncHelper {
|
|
7
|
+
/**
|
|
8
|
+
* Make the store agree with an authoritative list WITHIN a scope: every card given is written
|
|
9
|
+
* (unless the store holds a newer fold of it), and every card matching `where` that the list does
|
|
10
|
+
* not name is dropped. Cards outside `where` are left alone — which is why this, and never
|
|
11
|
+
* `replace()`, is how a list reaches the one shared card store.
|
|
12
|
+
*
|
|
13
|
+
* Unchanged cards are not rewritten, so a periodic re-seed wakes no subscriber.
|
|
14
|
+
*/
|
|
15
|
+
syncCards: (store: PlanningStores['cards'], items: Workcard[], where?: Criteria<Workcard>, opts?: SyncOptions) => Promise<void>
|
|
16
|
+
/** {@link SyncHelper.syncCards} for the link store. */
|
|
17
|
+
syncLinks: (store: PlanningStores['links'], items: Relationship[], where?: Criteria<Relationship>, opts?: SyncOptions) => Promise<void>
|
|
18
|
+
}
|
package/src/sync.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Relationship, Workcard } from '@owlmeans/planning'
|
|
2
|
+
import type { Criteria } from '@owlmeans/resource'
|
|
3
|
+
import type { SyncHelper } from './sync/types.js'
|
|
4
|
+
import type { PlanningStores, SyncOptions } from './types.js'
|
|
5
|
+
import { recordUtils } from './utils/record.js'
|
|
6
|
+
|
|
7
|
+
export const createSyncHelper = (): SyncHelper => {
|
|
8
|
+
const syncCards = async (
|
|
9
|
+
store: PlanningStores['cards'], items: Workcard[], where?: Criteria<Workcard>, opts?: SyncOptions
|
|
10
|
+
): Promise<void> => {
|
|
11
|
+
await dropUnnamed(store, recordUtils.idsOf(items), where, opts)
|
|
12
|
+
for (const item of items) {
|
|
13
|
+
await recordUtils.putCard(store, item)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const syncLinks = async (
|
|
18
|
+
store: PlanningStores['links'], items: Relationship[], where?: Criteria<Relationship>, opts?: SyncOptions
|
|
19
|
+
): Promise<void> => {
|
|
20
|
+
await dropUnnamed(store, recordUtils.idsOf(items), where, opts)
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
await recordUtils.putLink(store, item)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const dropUnnamed = async <T extends Workcard | Relationship>(
|
|
27
|
+
store: PlanningStores['cards'] | PlanningStores['links'], named: string[], where?: Criteria<T>, opts?: SyncOptions
|
|
28
|
+
): Promise<void> => {
|
|
29
|
+
const keep = new Set([...named, ...(opts?.keep ?? [])])
|
|
30
|
+
const scoped = await (store as PlanningStores['cards']).list(where as Criteria<Workcard>)
|
|
31
|
+
const stale = recordUtils.idsOf(scoped.items).filter(id => !keep.has(id))
|
|
32
|
+
if (stale.length > 0) {
|
|
33
|
+
await store.purge({ id: { $in: stale } })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { syncCards, syncLinks }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const syncHelper = createSyncHelper()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CommitState, Relationship, Workcard } from '@owlmeans/planning'
|
|
2
|
+
import type { ResourceRecord } from '@owlmeans/resource'
|
|
3
|
+
import type { StateResource } from '@owlmeans/state'
|
|
4
|
+
import type { PlanningCommitRecord, PlanningStores } from '../../types.js'
|
|
5
|
+
|
|
6
|
+
/** How a record is written into the state mirror: only when it is newer, and only when it changed. */
|
|
7
|
+
export interface RecordUtils {
|
|
8
|
+
/** Same content, whatever order the keys arrived in. */
|
|
9
|
+
sameRecord: (left: unknown, right: unknown) => boolean
|
|
10
|
+
/**
|
|
11
|
+
* Write one card unless the store already holds a NEWER fold of it.
|
|
12
|
+
*
|
|
13
|
+
* `seq` is the last folded transition, so a lower one is an older picture — a list fetched before a
|
|
14
|
+
* commit landed must not undo that commit. The head only ever grows: a head this client allocated
|
|
15
|
+
* (`applyReceipt`) stays until a fold catches up with it.
|
|
16
|
+
*/
|
|
17
|
+
putCard: (store: StateResource<Workcard>, card: Workcard) => Promise<Workcard>
|
|
18
|
+
putLink: (store: StateResource<Relationship>, link: Relationship) => Promise<void>
|
|
19
|
+
/** Drop a card, whatever lives under it when it is a project, and every link touching any of them. */
|
|
20
|
+
dropCard: (stores: PlanningStores, id: string, project: boolean) => Promise<void>
|
|
21
|
+
/** Record what is known about a transition, merged over what was known before. */
|
|
22
|
+
putCommit: (
|
|
23
|
+
store: StateResource<PlanningCommitRecord>, record: Partial<PlanningCommitRecord> & { id: string, state: CommitState }
|
|
24
|
+
) => Promise<PlanningCommitRecord>
|
|
25
|
+
clean: <T extends object>(record: T) => T
|
|
26
|
+
idsOf: <T extends ResourceRecord>(records: T[]) => string[]
|
|
27
|
+
}
|