@bud.sh/app 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -31
- package/dist/client/assets/bud/design.js +1 -1
- package/dist/client/assets/canvas-Bih9EdKJ.js +1 -0
- package/dist/client/assets/difference-worker-Bzg-rizy.js +8 -0
- package/dist/client/assets/dist-CdE5wBkg.js +5 -0
- package/dist/client/assets/files-1dLuoM2f.js +1 -0
- package/dist/client/assets/i18n-CkO6VFI0.js +1 -0
- package/dist/client/assets/index-Cn2mxroU.css +2 -0
- package/dist/client/assets/{index-ChANGtl-.js → index-DtISWZp9.js} +2 -2
- package/dist/client/assets/pages-BJ-tNJ4J.js +1 -0
- package/dist/client/assets/{project-branches-DO897zM6.js → project-branches-Ce3rXrkc.js} +1 -1
- package/dist/client/assets/project-comments-CPUONfs8.js +1 -0
- package/dist/client/assets/project-thread-links-C-TGkq59.js +1 -0
- package/dist/client/assets/{project-threads-Lay351p4.js → project-threads-Bv7IDBgi.js} +105 -105
- package/dist/client/assets/project-work-DAE9-olg.js +1 -0
- package/dist/client/assets/{task-design-Avb2RO4w.js → task-design-CwySIAOl.js} +3 -3
- package/dist/client/assets/tasks-KWsCLi_h.js +3 -0
- package/dist/client/assets/wasm-inline-DSUyFn1L.js +1 -0
- package/dist/client/assets/workspace-CVdcfaaB.js +2 -0
- package/dist/client/index.html +3 -3
- package/dist/hosted/assets/{canvas-V36732ER.js → canvas-AII16n9i.js} +97 -6
- package/dist/hosted/assets/{comment-data-CF2ZV_3k.js → comment-data-BXfMElfx.js} +2004 -402
- package/dist/hosted/assets/dist-BCSTeNf4.js +4785 -0
- package/dist/hosted/assets/{files-DjSCjfU7.js → files-D_1yeAVO.js} +10 -5
- package/dist/hosted/assets/{pages-BzjzeuPu.js → pages-CN_SbKWA.js} +3 -3
- package/dist/hosted/assets/{project-branches-BT97J9i7.js → project-branches-Bxp0DCwg.js} +1 -1
- package/dist/hosted/assets/{project-thread-links-BRLgByEe.js → project-thread-links-KqQoKBLE.js} +76 -4
- package/dist/{node-host/assets/screen-CZXWJEds.js → hosted/assets/screen-DpAcDUKR.js} +29 -1
- package/dist/hosted/assets/tasks-D66LlwxM.js +1423 -0
- package/dist/hosted/assets/wasm-inline-Dl9tqabx.js +4 -0
- package/dist/hosted/hosted.js +7885 -6219
- package/dist/node/assets/Stream-CAm1eJ2n.js +16604 -0
- package/dist/{server/assets/bridge-B_Blt55E.js → node/assets/bridge-DQSBYHmw.js} +124 -290
- package/dist/node/assets/config-9_jWwNBh.js +1067 -0
- package/dist/node/assets/load-config.server-CMVtRVmO.js +261 -0
- package/dist/node/assets/module-3GgtT4Xs.js +308 -0
- package/dist/node/assets/server-DTQFuoX2.js +846 -0
- package/dist/node/assets/src-D-kG1GxB.js +2 -0
- package/dist/node/assets/{src-CerrKjID.js → src-Dwe9APUx.js} +93 -3
- package/dist/node/assets/src-DysjfbM8.js +8592 -0
- package/dist/node/bridge.js +1 -845
- package/dist/node/cli.js +206 -0
- package/dist/node/design.js +811 -13
- package/dist/node/index.js +2 -739
- package/dist/node/plugins.js +5711 -0
- package/dist/node/server.js +1 -6746
- package/dist/node/vite.js +121 -0
- package/dist/{server/assets/canvas-Dc1mYCwQ.js → node-host/assets/canvas-BeDoufy1.js} +97 -7
- package/dist/node-host/assets/{comment-data-DyIZNpfG.js → comment-data-DE2T9yTj.js} +1980 -376
- package/dist/node-host/assets/dist-BCSTeNf4.js +4785 -0
- package/dist/node-host/assets/{files-Dh2TiQZq.js → files-Dl-PTS09.js} +11 -6
- package/dist/node-host/assets/{pages-BxpjbYhs.js → pages-DL2sk6_U.js} +3 -3
- package/dist/node-host/assets/{project-branches-B7HUGiVK.js → project-branches-DGp5ELct.js} +1 -1
- package/dist/{server/assets/project-thread-links-CBOpf6zk.js → node-host/assets/project-thread-links-CgBV3Jcm.js} +76 -5
- package/dist/{hosted/assets/screen-CKp2C7kx.js → node-host/assets/screen-B55yyVfb.js} +29 -1
- package/dist/node-host/assets/tasks-DZRNkoxe.js +1425 -0
- package/dist/node-host/assets/wasm-inline-Dl9tqabx.js +4 -0
- package/dist/node-host/node-host.js +6127 -4438
- package/dist/server/assets/{branch-previews-ClkoJ0lM.js → branch-previews-DedrpnVR.js} +1555 -673
- package/dist/{node-host/assets/canvas-TRUfxfL5.js → server/assets/canvas-CJWQ-jjv.js} +98 -6
- package/dist/server/assets/{comment-data-CsjukwAW.js → comment-data-Rg42ToTW.js} +1666 -47
- package/dist/server/assets/dist-BCSTeNf4.js +4785 -0
- package/dist/server/assets/{events-iHpLGr3k.js → events-BH_rAu7O.js} +155 -20
- package/dist/server/assets/{files-E9mSFeoc.js → files-CPJqKeMo.js} +12 -7
- package/dist/server/assets/{listener-DN5TyOjJ.js → listener-D61O6PzF.js} +708 -12
- package/dist/server/assets/{pages-BBBXyJWV.js → pages-KJY_Ba92.js} +4 -4
- package/dist/server/assets/{project-branches-B0YIK6Ur.js → project-branches-qG9ACZvo.js} +2 -2
- package/dist/{node-host/assets/project-thread-links-CauJF0Cz.js → server/assets/project-thread-links-CqK0lqCF.js} +77 -4
- package/dist/{node-host/assets/tasks-request-ZWmbc8fE.js → server/assets/project-work-DoIwxWMm.js} +87 -2
- package/dist/server/assets/run-BJ8r5jSh.js +572 -0
- package/dist/server/assets/{screen-BIQA2mr4.js → screen-CBrHPFug.js} +30 -2
- package/dist/server/assets/{server-DxK-UB_Z.js → server-Dkbp5stG.js} +1521 -793
- package/dist/server/assets/{services-BRrgXYlT.js → services-BTfig8A8.js} +234 -997
- package/dist/server/assets/tasks-Bgu5Lg9Y.js +1427 -0
- package/dist/server/assets/wasm-inline-Dl9tqabx.js +4 -0
- package/dist/server/capture.js +1 -2
- package/dist/server/migration.js +569 -12
- package/dist/server/runtime.js +7 -5
- package/dist/server/server.js +3 -3
- package/dist/types/assets/{config-DIC2cf_S.d.ts → config-CvcYmE2s.d.ts} +188 -50
- package/dist/types/assets/{contract-CAT-XFNq.d.ts → contract-B7yuuqSn.d.ts} +44 -2
- package/dist/types/assets/{items-CaVkByHk.d.ts → items-BQdaVksf.d.ts} +12 -1
- package/dist/types/assets/memory-state-BBW-FAVA.d.ts +236 -0
- package/dist/types/assets/picker-Be5Gx4WR.d.ts +9 -0
- package/dist/types/assets/{task-design-xJOhRNWh.d.ts → task-design-BDHKFHpp.d.ts} +3 -1
- package/dist/types/bridge.d.ts +1 -1
- package/dist/types/companion.d.ts +3 -3
- package/dist/types/design.d.ts +2 -2
- package/dist/types/hosted.d.ts +3 -3
- package/dist/types/index.d.ts +2 -2
- package/dist/types/node-host.d.ts +2 -2
- package/dist/types/plugins.d.ts +445 -0
- package/dist/types/screen.d.ts +2 -2
- package/dist/types/server.d.ts +2 -228
- package/dist/types/vite.d.ts +16 -0
- package/package.json +19 -3
- package/dist/client/assets/canvas-Cu6R6xxT.js +0 -1
- package/dist/client/assets/difference-worker-jfTSn-Tw.js +0 -8
- package/dist/client/assets/files-js6ElfkU.js +0 -1
- package/dist/client/assets/i18n-DxqFr2mi.js +0 -1
- package/dist/client/assets/index-CFFxP0gS.css +0 -2
- package/dist/client/assets/pages-6_lhiQmz.js +0 -1
- package/dist/client/assets/project-comments-DFFKVpia.js +0 -1
- package/dist/client/assets/project-thread-links-NLFWQRD2.js +0 -1
- package/dist/client/assets/project-work-du0Y-L5Y.js +0 -1
- package/dist/client/assets/tasks-CA-E8bbK.js +0 -1
- package/dist/client/assets/tasks-request-C55IeuiP.js +0 -1
- package/dist/client/assets/workspace-Cw5HvX5d.js +0 -2
- package/dist/hosted/assets/tasks-Dj7AyBJB.js +0 -649
- package/dist/hosted/assets/tasks-request-DZob623r.js +0 -113
- package/dist/node-host/assets/tasks-Bs1IOpeU.js +0 -651
- package/dist/server/assets/project-work-qSKFO5yj.js +0 -88
- package/dist/server/assets/tasks-CiXodsLN.js +0 -652
- package/dist/server/assets/tasks-request-DhwCzpRi.js +0 -115
- package/dist/types/assets/picker-D1Bw3ndx.d.ts +0 -14
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
import { A as parseWorkValue, B as statusCategory, C as parseWorkMutationResult, E as parseWorkSnapshot, F as parseMessage, G as record$1, H as identifier, I as parseMessageAuthor, K as strict, L as withoutMessages, M as workRunSummary, N as messageDeletionRefusal, P as messageDeletionRefusals, R as fieldByRole, S as parseWorkItemRef, T as parseWorkQuery, U as nonempty, V as WorkValidationError, W as parseWork, a as parseThreadDeleteInput, b as parseWorkDetail, c as parseThreadLinkInput, f as parseThreadStatusInput, g as parseWorkActionInput, h as parseSectionData, i as parseThreadCapabilities, j as valueByRole, k as parseWorkUpdate, l as parseThreadReplyInput, o as parseThreadFilter, r as parseThread, s as parseThreadInput, t as MAX_THREADS, u as parseThreadSnapshot, x as parseWorkItem, y as parseWorkCreate, z as parseWorkSchema } from "./src-Dwe9APUx.js";
|
|
2
|
+
import { F as offerUnsafe, Ht as sync, I as acquireRelease, Kt as try_, Ur as TaggedError, Yt as yieldNow, Zt as currentTimeMillis, _r as make, ft as gen, mr as add, n as callback, pn as effectContext, pr as Service } from "./Stream-CAm1eJ2n.js";
|
|
3
|
+
import { any } from "remix/data-schema";
|
|
4
|
+
import { isAbsolute, resolve } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
//#region ../work/src/server/errors.ts
|
|
7
|
+
var WorkErrorBase = TaggedError("WorkError");
|
|
8
|
+
/** Safe messages cross the HTTP boundary; diagnostics and credentials do not. */
|
|
9
|
+
var WorkError = class extends WorkErrorBase {};
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region ../work/src/server/services.ts
|
|
12
|
+
var WorkAdapterBase = Service()("@bud.sh/work/WorkAdapter");
|
|
13
|
+
/** A backend's work items. Run programs at a request boundary with its AbortSignal. */
|
|
14
|
+
var WorkAdapter = class extends WorkAdapterBase {};
|
|
15
|
+
var ThreadAdapterBase = Service()("@bud.sh/work/ThreadAdapter");
|
|
16
|
+
/** Discussions receive trusted server attribution separately from browser input. */
|
|
17
|
+
var ThreadAdapter = class extends ThreadAdapterBase {};
|
|
18
|
+
var ThreadImportSchema = strict({
|
|
19
|
+
source: identifier,
|
|
20
|
+
sourceId: nonempty(512),
|
|
21
|
+
thread: any()
|
|
22
|
+
});
|
|
23
|
+
function history(value) {
|
|
24
|
+
let input = parseWork(record$1, value, "import.thread");
|
|
25
|
+
if (!Array.isArray(input.messages) || input.messages.length === 0 || input.messages.length > 67108864 || input.messagesLoaded !== true || input.messagesTruncated !== false || input.messageCount !== input.messages.length) throw new WorkValidationError("import.thread.messages");
|
|
26
|
+
let encoder = new TextEncoder();
|
|
27
|
+
let bytes = 0;
|
|
28
|
+
let messages = Array.from(input.messages, (value) => {
|
|
29
|
+
let message = parseMessage(value);
|
|
30
|
+
bytes += encoder.encode(JSON.stringify(message)).byteLength + 1;
|
|
31
|
+
if (bytes > 67108864) throw new WorkValidationError("import.thread.bytes");
|
|
32
|
+
return message;
|
|
33
|
+
});
|
|
34
|
+
let parents = new Map(messages.map((message) => [message.id, message.parentId]));
|
|
35
|
+
if (parents.size !== messages.length) throw new WorkValidationError("import.thread.messages");
|
|
36
|
+
let complete = /* @__PURE__ */ new Set();
|
|
37
|
+
for (let message of messages) {
|
|
38
|
+
let path = /* @__PURE__ */ new Set();
|
|
39
|
+
let id = message.id;
|
|
40
|
+
while (id !== null && !complete.has(id)) {
|
|
41
|
+
if (path.has(id) || !parents.has(id)) throw new WorkValidationError("import.thread.messages.parentId");
|
|
42
|
+
path.add(id);
|
|
43
|
+
id = parents.get(id);
|
|
44
|
+
}
|
|
45
|
+
for (let id of path) complete.add(id);
|
|
46
|
+
}
|
|
47
|
+
let thread = parseThread({
|
|
48
|
+
...input,
|
|
49
|
+
messages: messages.slice(0, 200),
|
|
50
|
+
messagesTruncated: messages.length > 200
|
|
51
|
+
});
|
|
52
|
+
bytes += encoder.encode(JSON.stringify({
|
|
53
|
+
...thread,
|
|
54
|
+
messages: []
|
|
55
|
+
})).byteLength;
|
|
56
|
+
if (bytes > 67108864) throw new WorkValidationError("import.thread.bytes");
|
|
57
|
+
return {
|
|
58
|
+
...thread,
|
|
59
|
+
messages,
|
|
60
|
+
messagesTruncated: false
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validates complete source history before a server-side migration writes anything.
|
|
65
|
+
* @param value - The source identity and fully loaded discussion.
|
|
66
|
+
* @returns Complete, byte-bounded history, independent of the browser's message-count limit.
|
|
67
|
+
*/
|
|
68
|
+
function parseThreadImport(value) {
|
|
69
|
+
let input = parseWork(ThreadImportSchema, value, "import");
|
|
70
|
+
return {
|
|
71
|
+
...input,
|
|
72
|
+
thread: history(input.thread)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region ../work/src/server/memory-state.ts
|
|
77
|
+
var expectedRevision = nonempty(256);
|
|
78
|
+
var projectRoot = nonempty(4096);
|
|
79
|
+
/**
|
|
80
|
+
* Runs a parser over caller input, turning its failure into an `input` work error.
|
|
81
|
+
* @param parse - Parses the input.
|
|
82
|
+
* @returns The parsed input.
|
|
83
|
+
*/
|
|
84
|
+
function input(parse) {
|
|
85
|
+
try {
|
|
86
|
+
return parse();
|
|
87
|
+
} catch (cause) {
|
|
88
|
+
if (cause instanceof WorkError) throw cause;
|
|
89
|
+
throw new WorkError({
|
|
90
|
+
reason: "input",
|
|
91
|
+
message: "Check the supplied values.",
|
|
92
|
+
cause
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Runs a parser over stored data, turning its failure into a `storage` work error.
|
|
98
|
+
* @param parse - Parses the stored data.
|
|
99
|
+
* @returns The parsed data.
|
|
100
|
+
*/
|
|
101
|
+
function output(parse) {
|
|
102
|
+
try {
|
|
103
|
+
return parse();
|
|
104
|
+
} catch (cause) {
|
|
105
|
+
if (cause instanceof WorkError) throw cause;
|
|
106
|
+
throw new WorkError({
|
|
107
|
+
reason: "storage",
|
|
108
|
+
message: "The adapter returned invalid data.",
|
|
109
|
+
cause
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Throws a `refused` work error.
|
|
115
|
+
* @param message - The reason, safe to show.
|
|
116
|
+
*/
|
|
117
|
+
function refused(message) {
|
|
118
|
+
throw new WorkError({
|
|
119
|
+
reason: "refused",
|
|
120
|
+
message
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Throws a `conflict` work error unless a record is still at the revision the caller saw.
|
|
125
|
+
* @param current - The stored record.
|
|
126
|
+
* @param revision - The caller's expected revision.
|
|
127
|
+
*/
|
|
128
|
+
function expected(current, revision) {
|
|
129
|
+
input(() => parseWork(expectedRevision, revision, "expectedRevision"));
|
|
130
|
+
if (current.revision !== revision) throw new WorkError({
|
|
131
|
+
reason: "conflict",
|
|
132
|
+
message: "This record changed. Refresh and review your changes before saving.",
|
|
133
|
+
currentRevision: current.revision
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A loaded section, cut to its limit.
|
|
138
|
+
* @param items - The section's entries.
|
|
139
|
+
* @param max - The most entries to return.
|
|
140
|
+
* @param count - How many entries exist in all.
|
|
141
|
+
* @returns The available section, truncated when more exist than it holds.
|
|
142
|
+
*/
|
|
143
|
+
function available(items, max, count = items.length) {
|
|
144
|
+
return {
|
|
145
|
+
state: "available",
|
|
146
|
+
items: items.slice(0, max),
|
|
147
|
+
truncated: count > max
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A section the store does not provide.
|
|
152
|
+
* @returns The unsupported section.
|
|
153
|
+
*/
|
|
154
|
+
function unsupported() {
|
|
155
|
+
return {
|
|
156
|
+
state: "unsupported",
|
|
157
|
+
items: [],
|
|
158
|
+
truncated: false
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** The items, discussions and watchers behind the in-memory work and thread adapters. */
|
|
162
|
+
var MemoryStore = class {
|
|
163
|
+
identity = randomUUID();
|
|
164
|
+
schema;
|
|
165
|
+
author;
|
|
166
|
+
defaults;
|
|
167
|
+
capabilities;
|
|
168
|
+
options;
|
|
169
|
+
workspaces = /* @__PURE__ */ new Map();
|
|
170
|
+
#watchers = /* @__PURE__ */ new Map();
|
|
171
|
+
seed;
|
|
172
|
+
constructor(options) {
|
|
173
|
+
this.schema = parseWorkSchema(options.schema);
|
|
174
|
+
this.author = parseMessageAuthor(options.author ?? {
|
|
175
|
+
id: "memory",
|
|
176
|
+
name: "Memory adapter",
|
|
177
|
+
email: ""
|
|
178
|
+
});
|
|
179
|
+
this.defaults = Object.fromEntries(Object.entries(options.defaults ?? {}).map(([key, value]) => {
|
|
180
|
+
let field = this.schema.fields.find((field) => field.id === key);
|
|
181
|
+
if (!field) refused("A default refers to an undeclared field.");
|
|
182
|
+
return [key, parseWorkValue(value, field, this.schema)];
|
|
183
|
+
}));
|
|
184
|
+
this.options = {
|
|
185
|
+
...options,
|
|
186
|
+
actions: { ...options.actions },
|
|
187
|
+
sections: { ...options.sections }
|
|
188
|
+
};
|
|
189
|
+
this.capabilities = parseThreadCapabilities({
|
|
190
|
+
create: this.schema.capabilities.threads,
|
|
191
|
+
reply: this.schema.capabilities.threads,
|
|
192
|
+
status: this.schema.capabilities.threads,
|
|
193
|
+
link: this.schema.capabilities.threads,
|
|
194
|
+
delete: this.schema.capabilities.threads,
|
|
195
|
+
...options.threadCapabilities,
|
|
196
|
+
sync: false,
|
|
197
|
+
edit: false
|
|
198
|
+
});
|
|
199
|
+
if (this.schema.capabilities.sync) refused("Memory storage has no remote synchronization.");
|
|
200
|
+
for (let action of this.schema.actions) if (action.kind === "custom" && !this.options.actions?.[action.id]) refused("Declare an implementation for each custom memory action.");
|
|
201
|
+
for (let id of Object.keys(this.options.actions ?? {})) if (!this.schema.actions.some((action) => action.id === id)) refused("An action implementation has no declaration.");
|
|
202
|
+
for (let id of Object.keys(this.options.sections ?? {})) if (!this.schema.sections.some((section) => section.id === id)) refused("A section implementation has no declaration.");
|
|
203
|
+
this.seed = {
|
|
204
|
+
sequence: 0,
|
|
205
|
+
items: /* @__PURE__ */ new Map(),
|
|
206
|
+
threads: /* @__PURE__ */ new Map()
|
|
207
|
+
};
|
|
208
|
+
for (let value of options.items ?? []) {
|
|
209
|
+
let item = parseWorkItem(value, this.schema);
|
|
210
|
+
if (!item.contentLoaded || !item.revision || this.seed.items.has(item.id)) refused("Seed work items need complete content and unique identities with revisions.");
|
|
211
|
+
this.seed.items.set(item.id, {
|
|
212
|
+
item,
|
|
213
|
+
activity: [],
|
|
214
|
+
activityCount: 0,
|
|
215
|
+
history: [],
|
|
216
|
+
historyCount: 0
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
for (let value of options.threads ?? []) {
|
|
220
|
+
let thread = parseThread(value);
|
|
221
|
+
if (!thread.messagesLoaded || thread.messagesTruncated || !thread.revision || this.seed.threads.has(thread.id)) refused("Seed threads need complete messages and unique identities with revisions.");
|
|
222
|
+
for (let id of thread.itemIds) this.item(this.seed, id);
|
|
223
|
+
this.seed.threads.set(thread.id, thread);
|
|
224
|
+
}
|
|
225
|
+
for (let row of this.seed.items.values()) for (let relation of row.item.relations) this.item(this.seed, relation.item.id);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Signals every write to one workspace while the stream runs.
|
|
229
|
+
* @param root - The project directory.
|
|
230
|
+
* @returns One element per write.
|
|
231
|
+
*/
|
|
232
|
+
watch(root) {
|
|
233
|
+
return callback((queue) => acquireRelease(try_({
|
|
234
|
+
try: () => {
|
|
235
|
+
let key = input(() => resolve(root));
|
|
236
|
+
let notify = () => void offerUnsafe(queue, void 0);
|
|
237
|
+
let watchers = this.#watchers.get(key) ?? /* @__PURE__ */ new Set();
|
|
238
|
+
this.#watchers.set(key, watchers.add(notify));
|
|
239
|
+
return () => watchers.delete(notify);
|
|
240
|
+
},
|
|
241
|
+
catch: (cause) => cause instanceof WorkError ? cause : new WorkError({
|
|
242
|
+
reason: "input",
|
|
243
|
+
message: "Invalid project directory.",
|
|
244
|
+
cause
|
|
245
|
+
})
|
|
246
|
+
}), (stop) => sync(stop)));
|
|
247
|
+
}
|
|
248
|
+
run(root, write, operation) {
|
|
249
|
+
return gen({ self: this }, function* () {
|
|
250
|
+
yield* yieldNow;
|
|
251
|
+
let now = yield* currentTimeMillis;
|
|
252
|
+
return yield* try_({
|
|
253
|
+
try: () => {
|
|
254
|
+
let key = input(() => {
|
|
255
|
+
parseWork(projectRoot, root, "root");
|
|
256
|
+
if (!isAbsolute(root)) throw new TypeError("The project directory must be absolute.");
|
|
257
|
+
return resolve(root);
|
|
258
|
+
});
|
|
259
|
+
let current = this.workspaces.get(key) ?? structuredClone(this.seed);
|
|
260
|
+
let data = write ? structuredClone(current) : current;
|
|
261
|
+
let result = structuredClone(operation(data, new Date(now).toISOString()));
|
|
262
|
+
if (write) {
|
|
263
|
+
this.workspaces.set(key, data);
|
|
264
|
+
for (let notify of [...this.#watchers.get(key) ?? []]) notify();
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
},
|
|
268
|
+
catch: (cause) => cause instanceof WorkError ? cause : new WorkError({
|
|
269
|
+
reason: "storage",
|
|
270
|
+
message: "Memory storage failed.",
|
|
271
|
+
cause
|
|
272
|
+
})
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
next(data, prefix) {
|
|
277
|
+
return `${prefix}-${this.identity}-${++data.sequence}`;
|
|
278
|
+
}
|
|
279
|
+
item(data, id) {
|
|
280
|
+
input(() => parseWork(identifier, id, "id"));
|
|
281
|
+
let item = data.items.get(id);
|
|
282
|
+
if (!item) throw new WorkError({
|
|
283
|
+
reason: "not-found",
|
|
284
|
+
message: "This work item does not exist."
|
|
285
|
+
});
|
|
286
|
+
return item;
|
|
287
|
+
}
|
|
288
|
+
thread(data, id) {
|
|
289
|
+
input(() => parseWork(identifier, id, "id"));
|
|
290
|
+
let thread = data.threads.get(id);
|
|
291
|
+
if (!thread) throw new WorkError({
|
|
292
|
+
reason: "not-found",
|
|
293
|
+
message: "This discussion does not exist."
|
|
294
|
+
});
|
|
295
|
+
return thread;
|
|
296
|
+
}
|
|
297
|
+
done(item) {
|
|
298
|
+
let status = valueByRole(item, this.schema, "status");
|
|
299
|
+
return typeof status === "string" && ["done", "cancelled"].includes(statusCategory(this.schema, status) ?? "");
|
|
300
|
+
}
|
|
301
|
+
view(data, id) {
|
|
302
|
+
let item = this.item(data, id).item;
|
|
303
|
+
let children = [...data.items.values()].filter((row) => row.item.relations.some((relation) => relation.direction === "out" && relation.item.id === id && this.schema.relations.find((kind) => kind.type === relation.type)?.kind === "parent"));
|
|
304
|
+
let discussion = [...data.threads.values()].filter((thread) => thread.itemIds.includes(id)).reduce((count, thread) => count + thread.messageCount, 0);
|
|
305
|
+
let blocked = item.relations.some((relation) => relation.direction === "out" && this.schema.relations.find((kind) => kind.type === relation.type)?.kind === "blocks" && !this.done(this.item(data, relation.item.id).item));
|
|
306
|
+
return output(() => parseWorkItem({
|
|
307
|
+
...item,
|
|
308
|
+
counts: {
|
|
309
|
+
...this.schema.capabilities.discussion ? { discussion } : {},
|
|
310
|
+
children: children.length,
|
|
311
|
+
childrenDone: children.filter((row) => this.done(row.item)).length
|
|
312
|
+
},
|
|
313
|
+
...this.schema.capabilities.ready ? { ready: !this.done(item) && !blocked } : {}
|
|
314
|
+
}, this.schema));
|
|
315
|
+
}
|
|
316
|
+
save(data, value, at, summary) {
|
|
317
|
+
let prior = data.items.get(value.id);
|
|
318
|
+
let revision = this.next(data, "revision");
|
|
319
|
+
let values = { ...value.values };
|
|
320
|
+
for (let [role, entry] of [
|
|
321
|
+
["identifier", value.id],
|
|
322
|
+
["revision", revision],
|
|
323
|
+
["updated", at],
|
|
324
|
+
...!prior ? [["created", at]] : []
|
|
325
|
+
]) {
|
|
326
|
+
let field = this.schema.fields.find((field) => field.role === role);
|
|
327
|
+
if (field) values[field.id] = entry;
|
|
328
|
+
}
|
|
329
|
+
let item = output(() => parseWorkItem({
|
|
330
|
+
...value,
|
|
331
|
+
revision,
|
|
332
|
+
values
|
|
333
|
+
}, this.schema));
|
|
334
|
+
let row = prior ?? {
|
|
335
|
+
item,
|
|
336
|
+
history: [],
|
|
337
|
+
historyCount: 0,
|
|
338
|
+
activity: [],
|
|
339
|
+
activityCount: 0
|
|
340
|
+
};
|
|
341
|
+
row.item = item;
|
|
342
|
+
data.items.set(item.id, row);
|
|
343
|
+
item = this.view(data, item.id);
|
|
344
|
+
row.item = item;
|
|
345
|
+
row.history.unshift({
|
|
346
|
+
revision,
|
|
347
|
+
author: this.author.name,
|
|
348
|
+
at,
|
|
349
|
+
item: structuredClone(item)
|
|
350
|
+
});
|
|
351
|
+
row.history = row.history.slice(0, 50);
|
|
352
|
+
row.historyCount++;
|
|
353
|
+
row.activity.unshift({
|
|
354
|
+
id: this.next(data, "activity"),
|
|
355
|
+
actor: this.author.name,
|
|
356
|
+
at,
|
|
357
|
+
verb: prior ? "changed" : "created",
|
|
358
|
+
summary
|
|
359
|
+
});
|
|
360
|
+
row.activity = row.activity.slice(0, 200);
|
|
361
|
+
row.activityCount++;
|
|
362
|
+
return item;
|
|
363
|
+
}
|
|
364
|
+
touchThreads(data, ids, at) {
|
|
365
|
+
for (let id of new Set(ids)) this.save(data, this.item(data, id).item, at, "Discussion changed");
|
|
366
|
+
}
|
|
367
|
+
initialValues(id, at, supplied) {
|
|
368
|
+
let values = {
|
|
369
|
+
...this.defaults,
|
|
370
|
+
...supplied
|
|
371
|
+
};
|
|
372
|
+
let status = fieldByRole(this.schema, "status");
|
|
373
|
+
if (status && values[status.id] === void 0 && this.schema.statuses.length) values[status.id] = this.schema.statuses[0].value;
|
|
374
|
+
for (let role of [
|
|
375
|
+
"identifier",
|
|
376
|
+
"created",
|
|
377
|
+
"updated"
|
|
378
|
+
]) {
|
|
379
|
+
let field = fieldByRole(this.schema, role);
|
|
380
|
+
if (field) values[field.id] = role === "identifier" ? id : at;
|
|
381
|
+
}
|
|
382
|
+
return values;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region ../work/src/server/memory-threads.ts
|
|
387
|
+
/**
|
|
388
|
+
* The thread adapter over an in-memory store.
|
|
389
|
+
* @param store - The store shared with the work adapter.
|
|
390
|
+
* @returns The `ThreadAdapter` implementation.
|
|
391
|
+
*/
|
|
392
|
+
function memoryThreads(store) {
|
|
393
|
+
function view(thread, brief = false) {
|
|
394
|
+
let limit = brief ? 1 : 200;
|
|
395
|
+
return output(() => parseThread({
|
|
396
|
+
...thread,
|
|
397
|
+
messages: thread.messages.slice(0, limit),
|
|
398
|
+
messagesLoaded: !brief,
|
|
399
|
+
messagesTruncated: !brief && thread.messages.length > limit
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
function save(data, thread, at, oldIds = []) {
|
|
403
|
+
let changed = {
|
|
404
|
+
...thread,
|
|
405
|
+
revision: store.next(data, "thread-revision"),
|
|
406
|
+
updatedAt: new Date(Math.max(Date.parse(at), Date.parse(thread.updatedAt))).toISOString(),
|
|
407
|
+
messageCount: thread.messages.length
|
|
408
|
+
};
|
|
409
|
+
let result = view(changed);
|
|
410
|
+
data.threads.set(changed.id, changed);
|
|
411
|
+
store.touchThreads(data, [...oldIds, ...changed.itemIds], at);
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
function snapshot(data, filter) {
|
|
415
|
+
let threads = [...data.threads.values()].filter((thread) => {
|
|
416
|
+
if (filter.itemId && !thread.itemIds.includes(filter.itemId)) return false;
|
|
417
|
+
let kind = thread.anchor ? "type" in thread.anchor ? "code" : "canvas" : null;
|
|
418
|
+
if (filter.anchorKind && filter.anchorKind !== kind) return false;
|
|
419
|
+
return !filter.pagePath || thread.anchor && !("type" in thread.anchor) && thread.anchor.pagePath === filter.pagePath;
|
|
420
|
+
});
|
|
421
|
+
return output(() => parseThreadSnapshot({
|
|
422
|
+
connection: {
|
|
423
|
+
state: "ready",
|
|
424
|
+
adapter: store.schema.adapter
|
|
425
|
+
},
|
|
426
|
+
capabilities: store.capabilities,
|
|
427
|
+
threads: threads.slice(0, MAX_THREADS).map((thread) => view(thread, true)),
|
|
428
|
+
truncated: threads.length > MAX_THREADS
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
function link(root, id, ids, expectedRevision, remove) {
|
|
432
|
+
return store.run(root, true, (data, at) => {
|
|
433
|
+
if (!store.capabilities.link) refused("This adapter does not allow linking discussions.");
|
|
434
|
+
let request = input(() => parseThreadLinkInput({
|
|
435
|
+
itemIds: ids,
|
|
436
|
+
expectedRevision
|
|
437
|
+
}));
|
|
438
|
+
let thread = store.thread(data, id);
|
|
439
|
+
expected(thread, request.expectedRevision);
|
|
440
|
+
for (let id of request.itemIds) store.item(data, id);
|
|
441
|
+
let itemIds = remove ? thread.itemIds.filter((id) => !request.itemIds.includes(id)) : [.../* @__PURE__ */ new Set([...thread.itemIds, ...request.itemIds])];
|
|
442
|
+
if (itemIds.length > 100) refused("This discussion has too many linked work items.");
|
|
443
|
+
if (!thread.anchor && !itemIds.length) refused("An unanchored discussion needs a linked work item.");
|
|
444
|
+
return save(data, {
|
|
445
|
+
...thread,
|
|
446
|
+
itemIds
|
|
447
|
+
}, at, thread.itemIds);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
watch: (root) => store.watch(root),
|
|
452
|
+
list: (root, value) => store.run(root, false, (data) => snapshot(data, input(() => parseThreadFilter(value)))),
|
|
453
|
+
get: (root, id) => store.run(root, false, (data) => view(store.thread(data, id))),
|
|
454
|
+
create: (root, value, author) => store.run(root, true, (data, at) => {
|
|
455
|
+
if (!store.capabilities.create) refused("This adapter does not allow creating discussions.");
|
|
456
|
+
let request = input(() => parseThreadInput(value));
|
|
457
|
+
let trusted = input(() => parseMessageAuthor(author));
|
|
458
|
+
for (let id of request.itemIds) store.item(data, id);
|
|
459
|
+
return save(data, {
|
|
460
|
+
id: store.next(data, "thread"),
|
|
461
|
+
revision: null,
|
|
462
|
+
status: "open",
|
|
463
|
+
anchor: request.anchor,
|
|
464
|
+
itemIds: request.itemIds,
|
|
465
|
+
messages: [{
|
|
466
|
+
id: store.next(data, "message"),
|
|
467
|
+
body: request.body,
|
|
468
|
+
author: trusted,
|
|
469
|
+
createdAt: at,
|
|
470
|
+
parentId: null
|
|
471
|
+
}],
|
|
472
|
+
messageCount: 1,
|
|
473
|
+
messagesLoaded: true,
|
|
474
|
+
messagesTruncated: false,
|
|
475
|
+
createdAt: at,
|
|
476
|
+
updatedAt: at
|
|
477
|
+
}, at);
|
|
478
|
+
}),
|
|
479
|
+
reply: (root, id, value, author) => store.run(root, true, (data, at) => {
|
|
480
|
+
if (!store.capabilities.reply) refused("This adapter does not allow replying to discussions.");
|
|
481
|
+
let request = input(() => parseThreadReplyInput(value));
|
|
482
|
+
let trusted = input(() => parseMessageAuthor(author));
|
|
483
|
+
let thread = store.thread(data, id);
|
|
484
|
+
expected(thread, request.expectedRevision);
|
|
485
|
+
if (request.parentId && !thread.messages.some((message) => message.id === request.parentId)) refused("The reply parent must belong to this discussion.");
|
|
486
|
+
return save(data, {
|
|
487
|
+
...thread,
|
|
488
|
+
messages: [...thread.messages, {
|
|
489
|
+
id: store.next(data, "message"),
|
|
490
|
+
body: request.body,
|
|
491
|
+
author: trusted,
|
|
492
|
+
createdAt: at,
|
|
493
|
+
parentId: request.parentId
|
|
494
|
+
}]
|
|
495
|
+
}, at);
|
|
496
|
+
}),
|
|
497
|
+
setStatus: (root, id, status, expectedRevision) => store.run(root, true, (data, at) => {
|
|
498
|
+
if (!store.capabilities.status) refused("This adapter does not allow changing discussion status.");
|
|
499
|
+
let request = input(() => parseThreadStatusInput({
|
|
500
|
+
status,
|
|
501
|
+
expectedRevision
|
|
502
|
+
}));
|
|
503
|
+
let thread = store.thread(data, id);
|
|
504
|
+
expected(thread, request.expectedRevision);
|
|
505
|
+
return save(data, {
|
|
506
|
+
...thread,
|
|
507
|
+
status: request.status
|
|
508
|
+
}, at);
|
|
509
|
+
}),
|
|
510
|
+
deleteMessage: (root, id, value, author) => store.run(root, true, (data, at) => {
|
|
511
|
+
if (!store.capabilities.delete) refused("This adapter does not allow deleting messages.");
|
|
512
|
+
let request = input(() => parseThreadDeleteInput(value));
|
|
513
|
+
let trusted = input(() => parseMessageAuthor(author));
|
|
514
|
+
let thread = store.thread(data, id);
|
|
515
|
+
expected(thread, request.expectedRevision);
|
|
516
|
+
let refusal = messageDeletionRefusal(thread.messages, request.messageId, trusted.id);
|
|
517
|
+
if (refusal === "missing") throw new WorkError({
|
|
518
|
+
reason: "not-found",
|
|
519
|
+
message: messageDeletionRefusals.missing
|
|
520
|
+
});
|
|
521
|
+
if (refusal) refused(messageDeletionRefusals[refusal]);
|
|
522
|
+
if (thread.messages[0].id === request.messageId) {
|
|
523
|
+
data.threads.delete(id);
|
|
524
|
+
store.touchThreads(data, thread.itemIds, at);
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
return save(data, {
|
|
528
|
+
...thread,
|
|
529
|
+
messages: withoutMessages(thread.messages, [request.messageId])
|
|
530
|
+
}, at);
|
|
531
|
+
}),
|
|
532
|
+
link: (root, id, ids, revision) => link(root, id, ids, revision, false),
|
|
533
|
+
unlink: (root, id, ids, revision) => link(root, id, ids, revision, true)
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region ../work/src/server/memory-actions.ts
|
|
538
|
+
/**
|
|
539
|
+
* Computes what a declared action changes on an item, using its custom implementation or the
|
|
540
|
+
* built-in status, claim and relation behaviour.
|
|
541
|
+
* @param store - The memory store, with its schema, author and action implementations.
|
|
542
|
+
* @param item - The item the action runs on.
|
|
543
|
+
* @param action - The action's declaration.
|
|
544
|
+
* @param values - The action's validated input.
|
|
545
|
+
* @param at - When the action runs, for close timestamps.
|
|
546
|
+
* @returns The new values or outgoing relations.
|
|
547
|
+
*/
|
|
548
|
+
function actionChange(store, item, action, values, at) {
|
|
549
|
+
let custom = store.options.actions?.[action.id];
|
|
550
|
+
if (custom) return custom(structuredClone({
|
|
551
|
+
item,
|
|
552
|
+
input: values,
|
|
553
|
+
schema: store.schema,
|
|
554
|
+
author: store.author
|
|
555
|
+
}));
|
|
556
|
+
let patch = {};
|
|
557
|
+
let status = fieldByRole(store.schema, "status");
|
|
558
|
+
if (action.kind === "close" || action.kind === "reopen" || action.kind === "transition") {
|
|
559
|
+
if (!status) refused("This schema has no status field.");
|
|
560
|
+
let destination = action.kind === "transition" ? values[action.input?.find((field) => field.role === "status" || field.kind === "status")?.id ?? ""] : store.schema.statuses.find((status) => action.kind === "close" ? status.category === "done" : ["unstarted", "backlog"].includes(status.category))?.value;
|
|
561
|
+
if (typeof destination !== "string" || !store.schema.statuses.some((status) => status.value === destination)) refused("This action has no valid destination status.");
|
|
562
|
+
patch[status.id] = destination;
|
|
563
|
+
let closed = fieldByRole(store.schema, "closed");
|
|
564
|
+
if (closed && action.kind === "close") patch[closed.id] = at;
|
|
565
|
+
if (closed && !closed.required && action.kind === "reopen") patch[closed.id] = null;
|
|
566
|
+
let reason = fieldByRole(store.schema, "closeReason");
|
|
567
|
+
let reasonInput = action.input?.find((field) => field.role === "closeReason");
|
|
568
|
+
if (reason && reasonInput && values[reasonInput.id] !== void 0) patch[reason.id] = values[reasonInput.id];
|
|
569
|
+
return { values: patch };
|
|
570
|
+
}
|
|
571
|
+
if (action.kind === "claim" || action.kind === "unclaim") {
|
|
572
|
+
let assignee = fieldByRole(store.schema, "assignee");
|
|
573
|
+
if (!assignee) refused("This schema has no assignee field.");
|
|
574
|
+
let current = item.values[assignee.id];
|
|
575
|
+
if (action.kind === "claim" && current !== null && current !== void 0 && current !== "" && !(Array.isArray(current) && !current.length) && !(typeof current === "string" && current === store.author.name) && !(typeof current === "object" && !Array.isArray(current) && current.id === store.author.id)) refused("This work item is already assigned.");
|
|
576
|
+
let person = {
|
|
577
|
+
id: store.author.id,
|
|
578
|
+
name: store.author.name,
|
|
579
|
+
email: store.author.email
|
|
580
|
+
};
|
|
581
|
+
let claimed = action.kind === "claim";
|
|
582
|
+
if (assignee.kind === "person") patch[assignee.id] = claimed ? person : null;
|
|
583
|
+
else if (assignee.kind === "people") patch[assignee.id] = claimed ? [person] : [];
|
|
584
|
+
else if (assignee.kind === "text") patch[assignee.id] = claimed ? person.name : "";
|
|
585
|
+
else refused("This assignee field cannot represent a claim.");
|
|
586
|
+
return { values: patch };
|
|
587
|
+
}
|
|
588
|
+
if (action.kind === "relate" || action.kind === "unrelate") {
|
|
589
|
+
let targetField = action.input?.find((field) => field.kind === "item");
|
|
590
|
+
if (!targetField) refused("A relation action requires an item input.");
|
|
591
|
+
let target = input(() => parseWorkItemRef(values[targetField.id]));
|
|
592
|
+
let editable = store.schema.relations.filter((relation) => relation.editable);
|
|
593
|
+
let relationField = action.input?.find((field) => field.kind === "enum");
|
|
594
|
+
let type = relationField ? values[relationField.id] : editable.length === 1 ? editable[0].type : void 0;
|
|
595
|
+
if (typeof type !== "string" || !editable.some((relation) => relation.type === type)) refused("Select an editable relation type.");
|
|
596
|
+
let remaining = item.relations.filter((relation) => !(relation.direction === "out" && relation.type === type && relation.item.id === target.id));
|
|
597
|
+
return { relations: action.kind === "unrelate" ? remaining : [...remaining, {
|
|
598
|
+
type,
|
|
599
|
+
direction: "out",
|
|
600
|
+
item: target
|
|
601
|
+
}] };
|
|
602
|
+
}
|
|
603
|
+
refused("This action needs an implementation.");
|
|
604
|
+
}
|
|
605
|
+
function reference(store, item) {
|
|
606
|
+
let title = valueByRole(item, store.schema, "title");
|
|
607
|
+
let status = valueByRole(item, store.schema, "status");
|
|
608
|
+
return {
|
|
609
|
+
id: item.id,
|
|
610
|
+
...typeof title === "string" ? { title: title.slice(0, 1e3) } : {},
|
|
611
|
+
...typeof status === "string" ? { status } : {}
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Rebuilds every item's incoming relations and link summaries from the outgoing ones, refusing
|
|
616
|
+
* self-links and parent or blocking cycles, and saves the items that changed.
|
|
617
|
+
* @param store - The memory store.
|
|
618
|
+
* @param data - The project's items.
|
|
619
|
+
* @param at - When the change happens.
|
|
620
|
+
*/
|
|
621
|
+
function reconcileRelations(store, data, at) {
|
|
622
|
+
let incoming = /* @__PURE__ */ new Map();
|
|
623
|
+
for (let row of data.items.values()) for (let relation of row.item.relations.filter((relation) => relation.direction === "out")) {
|
|
624
|
+
store.item(data, relation.item.id);
|
|
625
|
+
if (relation.item.id === row.item.id) refused("A work item cannot relate to itself.");
|
|
626
|
+
let relations = incoming.get(relation.item.id) ?? [];
|
|
627
|
+
relations.push({
|
|
628
|
+
type: relation.type,
|
|
629
|
+
direction: "in",
|
|
630
|
+
item: reference(store, row.item)
|
|
631
|
+
});
|
|
632
|
+
incoming.set(relation.item.id, relations);
|
|
633
|
+
let kind = store.schema.relations.find((kind) => kind.type === relation.type)?.kind;
|
|
634
|
+
if (kind !== "parent" && kind !== "blocks") continue;
|
|
635
|
+
let pending = [relation.item.id];
|
|
636
|
+
let seen = /* @__PURE__ */ new Set();
|
|
637
|
+
while (pending.length) {
|
|
638
|
+
let id = pending.pop();
|
|
639
|
+
if (id === row.item.id) refused("This relationship would create a cycle.");
|
|
640
|
+
if (seen.has(id)) continue;
|
|
641
|
+
seen.add(id);
|
|
642
|
+
for (let edge of store.item(data, id).item.relations) if (edge.direction === "out" && edge.type === relation.type) pending.push(edge.item.id);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
for (let row of data.items.values()) {
|
|
646
|
+
let relations = [...row.item.relations.filter((relation) => relation.direction === "out").map((relation) => ({
|
|
647
|
+
...relation,
|
|
648
|
+
item: reference(store, store.item(data, relation.item.id).item)
|
|
649
|
+
})), ...incoming.get(row.item.id) ?? []];
|
|
650
|
+
if (JSON.stringify(row.item.relations) !== JSON.stringify(relations)) store.save(data, {
|
|
651
|
+
...row.item,
|
|
652
|
+
relations
|
|
653
|
+
}, at, "Relationships changed");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
//#endregion
|
|
657
|
+
//#region ../work/src/server/memory-work.ts
|
|
658
|
+
var actionResult = strict({
|
|
659
|
+
values: any(),
|
|
660
|
+
relations: any()
|
|
661
|
+
});
|
|
662
|
+
/**
|
|
663
|
+
* The work adapter over an in-memory store.
|
|
664
|
+
* @param store - The store shared with the thread adapter.
|
|
665
|
+
* @returns The `WorkAdapter` implementation.
|
|
666
|
+
*/
|
|
667
|
+
function memoryWork(store) {
|
|
668
|
+
let schema = store.schema;
|
|
669
|
+
let connection = {
|
|
670
|
+
state: "ready",
|
|
671
|
+
adapter: schema.adapter
|
|
672
|
+
};
|
|
673
|
+
function detail(data, id) {
|
|
674
|
+
let row = store.item(data, id);
|
|
675
|
+
let messages = [...data.threads.values()].filter((thread) => thread.itemIds.includes(id)).flatMap((thread) => thread.messages);
|
|
676
|
+
return output(() => parseWorkDetail({
|
|
677
|
+
connection,
|
|
678
|
+
item: store.view(data, id),
|
|
679
|
+
discussion: schema.capabilities.discussion ? available(messages, 200) : unsupported(),
|
|
680
|
+
history: schema.capabilities.history ? available(row.history, 50, row.historyCount) : unsupported(),
|
|
681
|
+
activity: schema.capabilities.activity ? available(row.activity, 200, row.activityCount) : unsupported()
|
|
682
|
+
}, schema));
|
|
683
|
+
}
|
|
684
|
+
function result(data, id) {
|
|
685
|
+
return output(() => parseWorkMutationResult({ item: store.view(data, id) }, schema));
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
watch: (root) => store.watch(root),
|
|
689
|
+
describe: (root) => store.run(root, false, () => connection),
|
|
690
|
+
schema: (root) => store.run(root, false, () => schema),
|
|
691
|
+
list: (root, value) => store.run(root, false, (data) => {
|
|
692
|
+
let query = input(() => parseWorkQuery(value));
|
|
693
|
+
let search = query.search?.trim().toLowerCase();
|
|
694
|
+
let rows = [...data.items.values()].map((row) => row.item).filter((item) => {
|
|
695
|
+
if (query.status && valueByRole(item, schema, "status") !== query.status) return false;
|
|
696
|
+
return !search || [item.id, ...Object.values(item.values).filter((value) => typeof value === "string")].some((value) => value.toLowerCase().includes(search));
|
|
697
|
+
});
|
|
698
|
+
let items = rows.slice(0, query.limit).map((row) => {
|
|
699
|
+
let item = store.view(data, row.id);
|
|
700
|
+
return {
|
|
701
|
+
...item,
|
|
702
|
+
values: Object.fromEntries(Object.entries(item.values).filter(([key]) => schema.fields.find((field) => field.id === key)?.kind !== "markdown")),
|
|
703
|
+
contentLoaded: false
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
return output(() => parseWorkSnapshot({
|
|
707
|
+
connection,
|
|
708
|
+
items,
|
|
709
|
+
truncated: rows.length > query.limit,
|
|
710
|
+
schemaVersion: schema.adapter.version
|
|
711
|
+
}, schema));
|
|
712
|
+
}),
|
|
713
|
+
get: (root, id) => store.run(root, false, (data) => detail(data, id)),
|
|
714
|
+
section: (root, id, sectionId) => store.run(root, false, (data) => {
|
|
715
|
+
input(() => parseWork(identifier, sectionId, "sectionId"));
|
|
716
|
+
let item = store.view(data, id);
|
|
717
|
+
let declared = schema.sections.find((section) => section.id === sectionId);
|
|
718
|
+
if (!declared) return {
|
|
719
|
+
kind: "custom",
|
|
720
|
+
data: unsupported()
|
|
721
|
+
};
|
|
722
|
+
let custom = store.options.sections?.[sectionId];
|
|
723
|
+
let result;
|
|
724
|
+
if (custom) result = custom(structuredClone(item));
|
|
725
|
+
else if (declared.kind === "activity") result = {
|
|
726
|
+
kind: "activity",
|
|
727
|
+
data: schema.capabilities.activity ? available(store.item(data, id).activity, 200, store.item(data, id).activityCount) : unsupported()
|
|
728
|
+
};
|
|
729
|
+
else if (declared.kind === "history") result = {
|
|
730
|
+
kind: "history",
|
|
731
|
+
data: schema.capabilities.history ? available(store.item(data, id).history, 50, store.item(data, id).historyCount) : unsupported()
|
|
732
|
+
};
|
|
733
|
+
else if (declared.kind === "metadata") result = {
|
|
734
|
+
kind: "metadata",
|
|
735
|
+
data: available([item.extra], 1)
|
|
736
|
+
};
|
|
737
|
+
else result = {
|
|
738
|
+
kind: declared.kind,
|
|
739
|
+
data: unsupported()
|
|
740
|
+
};
|
|
741
|
+
return output(() => {
|
|
742
|
+
if (result.kind !== declared.kind) throw new TypeError("The section renderer changed.");
|
|
743
|
+
return parseSectionData(result, schema);
|
|
744
|
+
});
|
|
745
|
+
}),
|
|
746
|
+
create: (root, value) => store.run(root, true, (data, at) => {
|
|
747
|
+
if (!schema.capabilities.create) refused("This adapter does not allow creating work items.");
|
|
748
|
+
let request = input(() => parseWorkCreate(value, schema));
|
|
749
|
+
let id = store.next(data, "work");
|
|
750
|
+
store.save(data, {
|
|
751
|
+
id,
|
|
752
|
+
revision: null,
|
|
753
|
+
values: store.initialValues(id, at, request.values),
|
|
754
|
+
relations: [],
|
|
755
|
+
counts: {},
|
|
756
|
+
contentLoaded: true,
|
|
757
|
+
extra: null
|
|
758
|
+
}, at, "Work item created");
|
|
759
|
+
return result(data, id);
|
|
760
|
+
}),
|
|
761
|
+
readItemData: (root, id, key) => store.run(root, false, (data) => {
|
|
762
|
+
let row = store.item(data, id);
|
|
763
|
+
return {
|
|
764
|
+
value: row.data?.[key] ?? null,
|
|
765
|
+
revision: row.item.revision
|
|
766
|
+
};
|
|
767
|
+
}),
|
|
768
|
+
writeItemData: (root, id, key, value, expectedRevision) => store.run(root, true, (data, at) => {
|
|
769
|
+
let row = store.item(data, id);
|
|
770
|
+
expected(row.item, expectedRevision);
|
|
771
|
+
row.data = {
|
|
772
|
+
...row.data,
|
|
773
|
+
[key]: value
|
|
774
|
+
};
|
|
775
|
+
let { run: _run, ...rest } = row.item;
|
|
776
|
+
let run = key === "budRun" ? workRunSummary(value) : row.item.run;
|
|
777
|
+
return { revision: store.save(data, run ? {
|
|
778
|
+
...rest,
|
|
779
|
+
run
|
|
780
|
+
} : rest, at, "Bud record updated").revision };
|
|
781
|
+
}),
|
|
782
|
+
update: (root, id, patch, expectedRevision) => store.run(root, true, (data, at) => {
|
|
783
|
+
if (!schema.capabilities.update) refused("This adapter does not allow editing work items.");
|
|
784
|
+
let request = input(() => parseWorkUpdate({
|
|
785
|
+
values: patch,
|
|
786
|
+
expectedRevision
|
|
787
|
+
}, schema));
|
|
788
|
+
let item = store.item(data, id).item;
|
|
789
|
+
expected(item, request.expectedRevision);
|
|
790
|
+
store.save(data, {
|
|
791
|
+
...item,
|
|
792
|
+
values: {
|
|
793
|
+
...item.values,
|
|
794
|
+
...request.values
|
|
795
|
+
}
|
|
796
|
+
}, at, "Work item updated");
|
|
797
|
+
reconcileRelations(store, data, at);
|
|
798
|
+
return result(data, id);
|
|
799
|
+
}),
|
|
800
|
+
act: (root, id, actionId, values, expectedRevision) => store.run(root, true, (data, at) => {
|
|
801
|
+
let request = input(() => parseWorkActionInput({
|
|
802
|
+
action: actionId,
|
|
803
|
+
input: values,
|
|
804
|
+
expectedRevision
|
|
805
|
+
}, schema));
|
|
806
|
+
let item = store.item(data, id).item;
|
|
807
|
+
expected(item, request.expectedRevision);
|
|
808
|
+
let action = schema.actions.find((action) => action.id === request.action);
|
|
809
|
+
let changed = actionChange(store, item, action, request.input, at);
|
|
810
|
+
output(() => parseWork(actionResult, changed, "actionResult"));
|
|
811
|
+
let next = {
|
|
812
|
+
...item,
|
|
813
|
+
values: {
|
|
814
|
+
...item.values,
|
|
815
|
+
...changed.values
|
|
816
|
+
},
|
|
817
|
+
relations: changed.relations ?? item.relations
|
|
818
|
+
};
|
|
819
|
+
store.save(data, next, at, action.label);
|
|
820
|
+
reconcileRelations(store, data, at);
|
|
821
|
+
return result(data, id);
|
|
822
|
+
})
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region ../work/src/server/memory.ts
|
|
827
|
+
/**
|
|
828
|
+
* Creates isolated work and discussion services for each layer and project root.
|
|
829
|
+
* @param options - The schema, complete seed records and trusted behavior for this adapter.
|
|
830
|
+
* @returns A layer with atomic in-memory writes and no filesystem or process requirements.
|
|
831
|
+
*/
|
|
832
|
+
function memoryAdapterLayer(options) {
|
|
833
|
+
return effectContext(try_({
|
|
834
|
+
try: () => {
|
|
835
|
+
let store = new MemoryStore(options);
|
|
836
|
+
return make(WorkAdapter, memoryWork(store)).pipe(add(ThreadAdapter, memoryThreads(store)));
|
|
837
|
+
},
|
|
838
|
+
catch: (cause) => new WorkError({
|
|
839
|
+
reason: "input",
|
|
840
|
+
message: "The memory adapter configuration is invalid.",
|
|
841
|
+
cause
|
|
842
|
+
})
|
|
843
|
+
}));
|
|
844
|
+
}
|
|
845
|
+
//#endregion
|
|
846
|
+
export { WorkError as a, WorkAdapter as i, parseThreadImport as n, ThreadAdapter as r, memoryAdapterLayer as t };
|