@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,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,14 @@
|
|
|
1
|
+
import type { CommitEvent, CommitFilter } from '@owlmeans/planning'
|
|
2
|
+
import type { Connection } from '@owlmeans/socket'
|
|
3
|
+
import type { PlanningOperation } from './lifecycle/types.js'
|
|
4
|
+
|
|
5
|
+
export interface Subscription {
|
|
6
|
+
operation: PlanningOperation
|
|
7
|
+
listener: (event: CommitEvent) => void | Promise<void>
|
|
8
|
+
filter?: CommitFilter
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CommitSocketOpening {
|
|
12
|
+
operation: PlanningOperation
|
|
13
|
+
promise: Promise<Connection | null>
|
|
14
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -2,13 +2,14 @@ import type { ClientConfig, ClientContext } from '@owlmeans/client-context'
|
|
|
2
2
|
import type { LazyService } from '@owlmeans/context'
|
|
3
3
|
import type { AbstractRequest, EntrypointProtocolDeclaration } from '@owlmeans/entrypoint'
|
|
4
4
|
import type {
|
|
5
|
-
CommitFilter, CommitSource, CommitState, PlanningFacade, PlanningProtocols,
|
|
6
|
-
PlanningScope, PlanningService, Relationship, RelationshipQuery, TransitionAction,
|
|
7
|
-
WorkcardKind, WorkcardQuery,
|
|
5
|
+
CommitFilter, CommitSource, CommitState, PlanningDefinitions, PlanningFacade, PlanningProtocols,
|
|
6
|
+
PlanningSchemaRegistry, PlanningScope, PlanningService, Relationship, RelationshipQuery, TransitionAction,
|
|
7
|
+
Workcard, WorkcardKind, WorkcardQuery,
|
|
8
8
|
} from '@owlmeans/planning'
|
|
9
9
|
import type { Criteria, ResourceRecord } from '@owlmeans/resource'
|
|
10
10
|
import type { Connection } from '@owlmeans/socket'
|
|
11
11
|
import type { StateAlias, StateResource } from '@owlmeans/state'
|
|
12
|
+
import type { PlanningClientLifecycle } from './lifecycle/types.js'
|
|
12
13
|
|
|
13
14
|
export interface Config extends ClientConfig { }
|
|
14
15
|
|
|
@@ -24,6 +25,8 @@ export interface PlanningSocketOpener {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface PlanningClientOptions {
|
|
28
|
+
/** Current organization slug and authenticated session identity; never an organization record id on the wire. */
|
|
29
|
+
scopeKey?: () => string | undefined
|
|
27
30
|
/** The tree the server mounted — the same `makePlanningProtocols` call, alias for alias. */
|
|
28
31
|
protocols: PlanningProtocols
|
|
29
32
|
/**
|
|
@@ -49,13 +52,34 @@ export interface PlanningClientOptions {
|
|
|
49
52
|
|
|
50
53
|
/** What `appendPlanningClient` registers under `PLANNING_SERVICE`. */
|
|
51
54
|
export interface PlanningClientService extends PlanningService, LazyService {
|
|
55
|
+
/** Shared by stock facades and feeds; local mutations never cross its close boundary. */
|
|
56
|
+
lifecycle: PlanningClientLifecycle
|
|
57
|
+
scopeKey: () => string
|
|
58
|
+
registerFeed: (feed: PlanningFeed) => import('@owlmeans/planning').Unsubscribe
|
|
52
59
|
/** A facade over the remote tree. The scope is advisory (see {@link PlanningClientOptions.scope}). */
|
|
53
60
|
for: (scope?: Partial<PlanningScope>) => PlanningFacade
|
|
54
61
|
commits: RemoteCommitSource
|
|
55
62
|
/** The registry, loaded from the server's bundle — once, unless `force`d. */
|
|
56
63
|
loadSchemas: (opts?: { force?: boolean }) => Promise<PlanningSchemaRegistry>
|
|
57
|
-
/**
|
|
64
|
+
/** Cancel old requests and feeds, drain local mutations, close the socket and clear mirrors. */
|
|
58
65
|
close: () => Promise<void>
|
|
66
|
+
/** Data-defined types and flows — present when the tree was declared with `definitions: true`. */
|
|
67
|
+
definitions?: RemoteDefinitions
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The client half of `PlanningDefinitions`: layers cached per project, dropped on any write. */
|
|
71
|
+
export interface RemoteDefinitions extends PlanningDefinitions {
|
|
72
|
+
/** Forget every cached layer — the next read asks the server. */
|
|
73
|
+
invalidate: () => void
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RemoteDefinitionsOptions {
|
|
77
|
+
lifecycle?: PlanningClientLifecycle
|
|
78
|
+
scopeKey?: () => string
|
|
79
|
+
/** Milliseconds, for every definitions call. */
|
|
80
|
+
timeout?: number
|
|
81
|
+
/** Called after every write (the service reloads its own bundle on the next `model()`). */
|
|
82
|
+
onWrite?: () => void
|
|
59
83
|
}
|
|
60
84
|
|
|
61
85
|
export interface WithPlanningClient {
|
|
@@ -65,11 +89,13 @@ export interface WithPlanningClient {
|
|
|
65
89
|
export interface RemoteCommitSource extends CommitSource {
|
|
66
90
|
/** A socket is open under this source. */
|
|
67
91
|
connected: () => boolean
|
|
68
|
-
/**
|
|
92
|
+
/** Cancel waits and subscriptions and release the socket, including a late-opening carrier. */
|
|
69
93
|
close: () => Promise<void>
|
|
70
94
|
}
|
|
71
95
|
|
|
72
96
|
export interface RemoteCommitSourceOptions {
|
|
97
|
+
lifecycle?: PlanningClientLifecycle
|
|
98
|
+
scopeKey?: () => string
|
|
73
99
|
socket?: PlanningSocketOpener
|
|
74
100
|
/** Seconds. */
|
|
75
101
|
poll?: number
|
|
@@ -80,12 +106,16 @@ export interface RemoteCommitSourceOptions {
|
|
|
80
106
|
}
|
|
81
107
|
|
|
82
108
|
export interface RemoteFacadeOptions {
|
|
109
|
+
lifecycle?: PlanningClientLifecycle
|
|
110
|
+
scopeKey?: () => string
|
|
83
111
|
commits: RemoteCommitSource
|
|
84
112
|
schemas: PlanningSchemaRegistry
|
|
85
113
|
/** Resolves the registry before a model is built. */
|
|
86
114
|
loadSchemas?: () => Promise<PlanningSchemaRegistry>
|
|
87
115
|
timeout?: number
|
|
88
116
|
stores?: () => PlanningStores | null
|
|
117
|
+
/** The data-defined schema surface, shared by every facade of the service. */
|
|
118
|
+
definitions?: RemoteDefinitions
|
|
89
119
|
}
|
|
90
120
|
|
|
91
121
|
/** One row per transition the client wrote or saw settle. */
|
|
@@ -105,12 +135,20 @@ export interface PlanningCommitRecord extends ResourceRecord {
|
|
|
105
135
|
}
|
|
106
136
|
|
|
107
137
|
export interface PlanningStoreAliases {
|
|
138
|
+
mentions: StateAlias<import('@owlmeans/planning').CommentMention>
|
|
139
|
+
comments: StateAlias<import('@owlmeans/planning').Comment>
|
|
140
|
+
teams: StateAlias<import('@owlmeans/planning').Team>
|
|
141
|
+
assignees: StateAlias<import('@owlmeans/planning').Assignee>
|
|
108
142
|
cards: StateAlias<Workcard>
|
|
109
143
|
links: StateAlias<Relationship>
|
|
110
144
|
commits: StateAlias<PlanningCommitRecord>
|
|
111
145
|
}
|
|
112
146
|
|
|
113
147
|
export interface PlanningStores {
|
|
148
|
+
mentions: StateResource<import('@owlmeans/planning').CommentMention>
|
|
149
|
+
comments: StateResource<import('@owlmeans/planning').Comment>
|
|
150
|
+
teams: StateResource<import('@owlmeans/planning').Team>
|
|
151
|
+
assignees: StateResource<import('@owlmeans/planning').Assignee>
|
|
114
152
|
cards: StateResource<Workcard>
|
|
115
153
|
links: StateResource<Relationship>
|
|
116
154
|
commits: StateResource<PlanningCommitRecord>
|
|
@@ -161,3 +199,13 @@ export interface PlanningFeed extends Readonly<PlanningFeedState> {
|
|
|
161
199
|
/** Stop folding frames and refreshing. The shared socket stays open. */
|
|
162
200
|
stop: () => Promise<void>
|
|
163
201
|
}
|
|
202
|
+
|
|
203
|
+
export interface PlanningResourceFeedOptions {
|
|
204
|
+
assignees?: import('@owlmeans/planning').AssigneeQuery
|
|
205
|
+
teams?: import('@owlmeans/planning').TeamQuery
|
|
206
|
+
comments?: import('@owlmeans/planning').CommentQuery
|
|
207
|
+
mentions?: import('@owlmeans/planning').MentionQuery
|
|
208
|
+
refresh?: number
|
|
209
|
+
scope?: Partial<PlanningScope>
|
|
210
|
+
onChange?: (state: PlanningFeedState) => void
|
|
211
|
+
}
|
|
@@ -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
|
+
}
|
package/src/utils/record.ts
CHANGED
|
@@ -2,90 +2,87 @@ import type { PlanningCommitRecord, PlanningStores } from '../types.js'
|
|
|
2
2
|
import type { CommitState, Relationship, Workcard } from '@owlmeans/planning'
|
|
3
3
|
import type { ResourceRecord } from '@owlmeans/resource'
|
|
4
4
|
import type { StateResource } from '@owlmeans/state'
|
|
5
|
+
import type { RecordUtils } from './record/types.js'
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
export const createRecordUtils = (): RecordUtils => {
|
|
8
|
+
const stable = (value: unknown): unknown => {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.map(stable)
|
|
11
|
+
}
|
|
12
|
+
if (value != null && typeof value === 'object') {
|
|
13
|
+
return Object.fromEntries(Object.entries(value as Record<string, unknown>)
|
|
14
|
+
.filter(([, entry]) => entry !== undefined)
|
|
15
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
16
|
+
.map(([key, entry]) => [key, stable(entry)]))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return value
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
const sameRecord = (left: unknown, right: unknown): boolean =>
|
|
23
|
+
JSON.stringify(stable(left)) === JSON.stringify(stable(right))
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
const putCard = async (store: StateResource<Workcard>, card: Workcard): Promise<Workcard> => {
|
|
26
|
+
if (card.id == null) {
|
|
27
|
+
return card
|
|
28
|
+
}
|
|
29
|
+
const known = await store.load(card.id)
|
|
30
|
+
if (known != null && (known.seq ?? 0) > (card.seq ?? 0)) {
|
|
31
|
+
return known
|
|
32
|
+
}
|
|
33
|
+
const head = Math.max(card.head ?? card.seq ?? 0, known?.head ?? 0)
|
|
34
|
+
const next = head > (card.head ?? card.seq ?? 0) ? { ...card, head } : card
|
|
35
|
+
if (known != null && sameRecord(known, next)) {
|
|
36
|
+
return known
|
|
37
|
+
}
|
|
38
|
+
await store.save(next)
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
* Write one card unless the store already holds a NEWER fold of it.
|
|
26
|
-
*
|
|
27
|
-
* `seq` is the last folded transition, so a lower one is an older picture — a list fetched before a
|
|
28
|
-
* commit landed must not undo that commit. The head only ever grows: a head this client allocated
|
|
29
|
-
* (`applyReceipt`) stays until a fold catches up with it.
|
|
30
|
-
*/
|
|
31
|
-
export const putCard = async (store: StateResource<Workcard>, card: Workcard): Promise<Workcard> => {
|
|
32
|
-
if (card.id == null) {
|
|
33
|
-
return card
|
|
40
|
+
return next
|
|
34
41
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
|
|
43
|
+
const putLink = async (store: StateResource<Relationship>, link: Relationship): Promise<void> => {
|
|
44
|
+
if (link.id == null) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
const known = await store.load(link.id)
|
|
48
|
+
if (known != null && sameRecord(known, link)) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
await store.save(link)
|
|
38
52
|
}
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
|
|
54
|
+
const dropCard = async (stores: PlanningStores, id: string, project: boolean): Promise<void> => {
|
|
55
|
+
const ids = [id]
|
|
56
|
+
if (project) {
|
|
57
|
+
const children = await stores.cards.list({ parents: { $contains: [id] } })
|
|
58
|
+
ids.push(...children.items.map(child => child.id).filter((child): child is string => child != null))
|
|
59
|
+
}
|
|
60
|
+
await stores.cards.purge({ id: { $in: ids } })
|
|
61
|
+
if (await stores.links.count({ $or: [{ from: { $in: ids } }, { to: { $in: ids } }] }) > 0) {
|
|
62
|
+
await stores.links.purge({ $or: [{ from: { $in: ids } }, { to: { $in: ids } }] })
|
|
63
|
+
}
|
|
43
64
|
}
|
|
44
|
-
await store.save(next)
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
}
|
|
66
|
+
const putCommit = async (
|
|
67
|
+
store: StateResource<PlanningCommitRecord>, record: Partial<PlanningCommitRecord> & { id: string, state: CommitState }
|
|
68
|
+
): Promise<PlanningCommitRecord> => {
|
|
69
|
+
const known = await store.load(record.id)
|
|
70
|
+
const next = { ...known, ...clean(record) } as PlanningCommitRecord
|
|
71
|
+
if (known != null && sameRecord(known, next)) {
|
|
72
|
+
return known
|
|
73
|
+
}
|
|
74
|
+
await store.save(next)
|
|
48
75
|
|
|
49
|
-
|
|
50
|
-
if (link.id == null) {
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
const known = await store.load(link.id)
|
|
54
|
-
if (known != null && sameRecord(known, link)) {
|
|
55
|
-
return
|
|
76
|
+
return next
|
|
56
77
|
}
|
|
57
|
-
await store.save(link)
|
|
58
|
-
}
|
|
59
78
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const ids = [id]
|
|
63
|
-
if (project) {
|
|
64
|
-
const children = await stores.cards.list({ parents: { $contains: [id] } })
|
|
65
|
-
ids.push(...children.items.map(child => child.id).filter((child): child is string => child != null))
|
|
66
|
-
}
|
|
67
|
-
await stores.cards.purge({ id: { $in: ids } })
|
|
68
|
-
if (await stores.links.count({ $or: [{ from: { $in: ids } }, { to: { $in: ids } }] }) > 0) {
|
|
69
|
-
await stores.links.purge({ $or: [{ from: { $in: ids } }, { to: { $in: ids } }] })
|
|
70
|
-
}
|
|
71
|
-
}
|
|
79
|
+
const clean = <T extends object>(record: T): T =>
|
|
80
|
+
Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)) as T
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
store: StateResource<PlanningCommitRecord>, record: Partial<PlanningCommitRecord> & { id: string, state: CommitState }
|
|
76
|
-
): Promise<PlanningCommitRecord> => {
|
|
77
|
-
const known = await store.load(record.id)
|
|
78
|
-
const next = { ...known, ...clean(record) } as PlanningCommitRecord
|
|
79
|
-
if (known != null && sameRecord(known, next)) {
|
|
80
|
-
return known
|
|
81
|
-
}
|
|
82
|
-
await store.save(next)
|
|
82
|
+
const idsOf = <T extends ResourceRecord>(records: T[]): string[] =>
|
|
83
|
+
records.map(record => record.id).filter((id): id is string => id != null)
|
|
83
84
|
|
|
84
|
-
return
|
|
85
|
+
return { sameRecord, putCard, putLink, dropCard, putCommit, clean, idsOf }
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
export const
|
|
88
|
-
Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)) as T
|
|
89
|
-
|
|
90
|
-
export const idsOf = <T extends ResourceRecord>(records: T[]): string[] =>
|
|
91
|
-
records.map(record => record.id).filter((id): id is string => id != null)
|
|
88
|
+
export const recordUtils = createRecordUtils()
|