@plurnk/plurnk-service 1.6.0 → 1.6.1
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/.env.defaults +7 -21
- package/INSTALL.md +18 -17
- package/README.md +1 -1
- package/SPEC.md +125 -115
- package/dist/build-info.json +1 -1
- package/dist/core/ChannelWrite.sql +3 -3
- package/dist/core/CoreSchemeServices.d.ts +1 -1
- package/dist/core/CoreSchemeServices.d.ts.map +1 -1
- package/dist/core/CoreSchemeServices.js +1 -1
- package/dist/core/CoreSchemeServices.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +2 -2
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +79 -1115
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Engine.d.ts +9 -33
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +58 -1656
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +46 -32
- package/dist/core/LogBody.d.ts +1 -0
- package/dist/core/LogBody.d.ts.map +1 -1
- package/dist/core/LogBody.js +4 -0
- package/dist/core/LogBody.js.map +1 -1
- package/dist/core/ModelCall.d.ts +2 -2
- package/dist/core/ModelCall.d.ts.map +1 -1
- package/dist/core/ModelCall.js +4 -2
- package/dist/core/ModelCall.js.map +1 -1
- package/dist/core/PacketBuilder.d.ts +11 -22
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +66 -168
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProblemLog.d.ts +1 -1
- package/dist/core/ProblemLog.d.ts.map +1 -1
- package/dist/core/ProblemLog.js +14 -3
- package/dist/core/ProblemLog.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts +2 -2
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +14 -5
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.js +2 -2
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/ResourceMutations.d.ts +73 -0
- package/dist/core/ResourceMutations.d.ts.map +1 -0
- package/dist/core/ResourceMutations.js +1142 -0
- package/dist/core/ResourceMutations.js.map +1 -0
- package/dist/core/SchemeRegistry.js +3 -3
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/StoredPacket.d.ts +2 -2
- package/dist/core/StoredPacket.js +7 -7
- package/dist/core/TurnRunner.d.ts +72 -0
- package/dist/core/TurnRunner.d.ts.map +1 -0
- package/dist/core/TurnRunner.js +1730 -0
- package/dist/core/TurnRunner.js.map +1 -0
- package/dist/core/caps/DbChannelCaps.js +4 -4
- package/dist/core/caps/DbChannelCaps.js.map +1 -1
- package/dist/core/content-hash.js +2 -2
- package/dist/core/content-hash.js.map +1 -1
- package/dist/core/content-weight.d.ts +2 -0
- package/dist/core/content-weight.d.ts.map +1 -0
- package/dist/core/content-weight.js +4 -0
- package/dist/core/content-weight.js.map +1 -0
- package/dist/core/content_weight.d.ts +4 -0
- package/dist/core/content_weight.d.ts.map +1 -0
- package/dist/core/content_weight.js +6 -0
- package/dist/core/content_weight.js.map +1 -0
- package/dist/core/fork.sql +5 -5
- package/dist/core/packet-wire.d.ts +4 -4
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +21 -21
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/scheme-types.d.ts +1 -1
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/digest/Digest.js +1 -1
- package/dist/digest/Digest.js.map +1 -1
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +26 -10
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +1 -1
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +1 -1
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +2 -2
- package/dist/schemes/Log.sql +2 -2
- package/dist/schemes/Worker.js +2 -2
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.js +4 -4
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +2 -2
- package/dist/schemes/_entry-find.d.ts +5 -5
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +29 -28
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-manifest.js +4 -4
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +3 -3
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +3 -3
- package/dist/server/Daemon.d.ts +7 -42
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +124 -887
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DrainSupervisor.d.ts +117 -0
- package/dist/server/DrainSupervisor.d.ts.map +1 -0
- package/dist/server/DrainSupervisor.js +802 -0
- package/dist/server/DrainSupervisor.js.map +1 -0
- package/dist/server/lifecycle-recovery.sql +3 -0
- package/dist/server/logEntry.d.ts +1 -1
- package/dist/server/logEntry.js +1 -1
- package/dist/server/seam-entry-read.sql +2 -2
- package/dist/service.js +1 -1
- package/dist/service.js.map +1 -1
- package/migrations/001_schema.sql +20 -10
- package/package.json +33 -33
- package/dist/core/ruler_count.d.ts +0 -4
- package/dist/core/ruler_count.d.ts.map +0 -1
- package/dist/core/ruler_count.js +0 -7
- package/dist/core/ruler_count.js.map +0 -1
- package/dist/core/token-ruler.d.ts +0 -2
- package/dist/core/token-ruler.d.ts.map +0 -1
- package/dist/core/token-ruler.js +0 -5
- package/dist/core/token-ruler.js.map +0 -1
package/dist/core/Dispatcher.js
CHANGED
|
@@ -9,8 +9,7 @@ import { PathSyntax, TagSignal } from "@plurnk/plurnk-contracts";
|
|
|
9
9
|
import Namespace from "./namespace.js";
|
|
10
10
|
import LoopFlagsReader from "./LoopFlagsReader.js";
|
|
11
11
|
import ChannelWrite from "./ChannelWrite.js";
|
|
12
|
-
import {
|
|
13
|
-
import DbProjectionCaps from "./caps/DbProjectionCaps.js";
|
|
12
|
+
import { ReadProjector } from "../content/index.js";
|
|
14
13
|
import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
|
|
15
14
|
import EntryCrud from "../schemes/_entry-crud.js";
|
|
16
15
|
import EntryOps from "../schemes/_entry-ops.js";
|
|
@@ -24,6 +23,8 @@ import EffectPolicy from "../schemes/EffectPolicy.js";
|
|
|
24
23
|
import { CoreSchemeAdapterBase, } from "./CoreSchemeServices.js";
|
|
25
24
|
import { InvalidOperationResultError, NetworkAddress, } from "@plurnk/plurnk-schemes";
|
|
26
25
|
import DurableStatement from "./DurableStatement.js";
|
|
26
|
+
import ResourceMutations from "./ResourceMutations.js";
|
|
27
|
+
import LogBody from "./LogBody.js";
|
|
27
28
|
// SPEC {§scheme-surface}: writer must be in target scheme's manifest.writableBy.
|
|
28
29
|
// OPEN/FOLD/READ/FIND are not gated — they curate the log or read, never mutating an entry.
|
|
29
30
|
const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC", "KILL", "FORK", "WORK"]);
|
|
@@ -37,9 +38,8 @@ const assertClassifyingSignal = (statement) => {
|
|
|
37
38
|
TagSignal.applied(statement.signal);
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
|
-
// Op dispatch ({§op-methods-op-dispatch}):
|
|
41
|
-
//
|
|
42
|
-
// routing, the durable log write, and the proposal pause.
|
|
41
|
+
// Op dispatch ({§op-methods-op-dispatch}): admission, operation-owner routing,
|
|
42
|
+
// durable log writing, and proposal lifecycle.
|
|
43
43
|
class Dispatcher {
|
|
44
44
|
static #failure(code, status, detail, fields = {}, extensions = {}) {
|
|
45
45
|
return Results.failure("engine:dispatcher", code, status, detail, fields, extensions);
|
|
@@ -52,7 +52,7 @@ class Dispatcher {
|
|
|
52
52
|
#db;
|
|
53
53
|
#schemes;
|
|
54
54
|
#mimetypes;
|
|
55
|
-
#
|
|
55
|
+
#weighContent;
|
|
56
56
|
#notices;
|
|
57
57
|
#proposals;
|
|
58
58
|
// Boot-discovered runtime executors, late-injected on Engine — thunked.
|
|
@@ -74,12 +74,12 @@ class Dispatcher {
|
|
|
74
74
|
#joinTargets;
|
|
75
75
|
#liveSubscriptions;
|
|
76
76
|
#lifecycle;
|
|
77
|
-
#
|
|
78
|
-
constructor({ db, schemes, mimetypes,
|
|
77
|
+
#resourceMutations;
|
|
78
|
+
constructor({ db, schemes, mimetypes, weigh, notices, proposals, executors, loopSignal, streamEventNotify, wakeWorkerNotify, injectWorker, branchWorker, branchCompletionGate, cancelWorker, cancelDescendants, searchGate, parkDeadlines, joinTargets, liveSubscriptions }) {
|
|
79
79
|
this.#db = db;
|
|
80
80
|
this.#schemes = schemes;
|
|
81
81
|
this.#mimetypes = mimetypes;
|
|
82
|
-
this.#
|
|
82
|
+
this.#weighContent = weigh;
|
|
83
83
|
this.#notices = notices;
|
|
84
84
|
this.#proposals = proposals;
|
|
85
85
|
this.#executors = executors;
|
|
@@ -96,6 +96,23 @@ class Dispatcher {
|
|
|
96
96
|
this.#joinTargets = joinTargets ?? new Set();
|
|
97
97
|
this.#liveSubscriptions = liveSubscriptions;
|
|
98
98
|
this.#lifecycle = new LoopLifecycle(db);
|
|
99
|
+
this.#resourceMutations = new ResourceMutations({
|
|
100
|
+
schemes,
|
|
101
|
+
liveSubscriptions,
|
|
102
|
+
run: (schemeName, statement, ctx) => this.#run(schemeName, statement, ctx),
|
|
103
|
+
checkWritable: (statement, origin) => this.#checkWritable(statement, origin),
|
|
104
|
+
checkFlagsGate: (statement, loopId) => this.#checkFlagsGate(statement, loopId),
|
|
105
|
+
editTargetIdentity: (statement, workspaceId) => this.#editTargetIdentity(statement, workspaceId),
|
|
106
|
+
canonicalFilePath: (pathname, workspaceId) => this.#canonicalFilePath(pathname, workspaceId),
|
|
107
|
+
prepareDataRepresentation: (args) => this.#prepareDataRepresentation({
|
|
108
|
+
...args,
|
|
109
|
+
handler: args.handler,
|
|
110
|
+
}),
|
|
111
|
+
readEntry: (scheme, pathname, ctx) => this.#readEntry(scheme, pathname, ctx),
|
|
112
|
+
writeEntry: (scheme, pathname, entry, ctx) => this.#writeEntry(scheme, pathname, entry, ctx),
|
|
113
|
+
deleteChannel: (scheme, pathname, channel, ctx) => this.#deleteChannel(scheme, pathname, channel, ctx),
|
|
114
|
+
applyProposal: (statement, result, resolution, ids) => this.#proposals.workerApply(statement, result, resolution, ids),
|
|
115
|
+
});
|
|
99
116
|
}
|
|
100
117
|
// workspace → project_root, memoized: {§fs-namespace} fixes the root immutably at
|
|
101
118
|
// workspace creation, so a process-lifetime cache can never go stale.
|
|
@@ -213,230 +230,35 @@ class Dispatcher {
|
|
|
213
230
|
if (key !== null)
|
|
214
231
|
target.pathname = key;
|
|
215
232
|
}
|
|
216
|
-
async #
|
|
217
|
-
|
|
218
|
-
if (anchored.length === 0) {
|
|
219
|
-
return { statements: statements, precondition: null };
|
|
220
|
-
}
|
|
221
|
-
if (manifest.textEditScopes !== true || !manifest.writableBy.includes("model")) {
|
|
222
|
-
return {
|
|
223
|
-
result: _a.#failure("line-anchor-unsupported", 400, `Scheme '${schemeName}' does not support textual EDIT scopes.`, {}, {
|
|
224
|
-
scheme: schemeName,
|
|
225
|
-
operation: "EDIT",
|
|
226
|
-
recovery: "Remove the scope and submit the scheme's complete editable unit.",
|
|
227
|
-
retryable: false,
|
|
228
|
-
}),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
if (identity === null) {
|
|
232
|
-
return {
|
|
233
|
-
result: _a.#failure("edit-target-required", 400, "A line-anchored EDIT requires a target resource.", {}, { recovery: "Provide the target that rendered the line anchor.", retryable: false }),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
const first = statements[0];
|
|
237
|
-
if (first === undefined)
|
|
238
|
-
return { statements: [], precondition: null };
|
|
239
|
-
const current = await this.#run(schemeName, {
|
|
240
|
-
op: "READ",
|
|
241
|
-
suffix: first.suffix,
|
|
242
|
-
signal: null,
|
|
243
|
-
target: first.target,
|
|
244
|
-
lineMarker: { marks: [1, -1] },
|
|
245
|
-
body: null,
|
|
246
|
-
position: first.position,
|
|
247
|
-
}, ctx);
|
|
248
|
-
if (current.status === 204 || current.status === 404) {
|
|
249
|
-
return { result: EditCollision.result(identity) };
|
|
250
|
-
}
|
|
251
|
-
if (current.status >= 300) {
|
|
252
|
-
return {
|
|
253
|
-
result: _a.#failure("line-anchor-validation-failed", current.status, `EDIT could not validate its line anchor at ${identity}: ${current.problem?.detail ?? `READ returned ${current.status}`}`, {}, {
|
|
254
|
-
target: identity,
|
|
255
|
-
upstreamStatus: current.status,
|
|
256
|
-
stage: "mutation-precondition",
|
|
257
|
-
retryable: false,
|
|
258
|
-
}),
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
if (current.status !== 200) {
|
|
262
|
-
return { result: EditCollision.result(identity) };
|
|
263
|
-
}
|
|
264
|
-
const content = current.content;
|
|
265
|
-
if (typeof content !== "string") {
|
|
266
|
-
throw new InvalidOperationResultError(`Scheme '${schemeName}' returned READ ${current.status} without textual content while validating an EDIT anchor.`);
|
|
267
|
-
}
|
|
268
|
-
const lineAnchors = current.lineAnchors;
|
|
269
|
-
const lineAnchorIdentity = current.lineAnchorIdentity;
|
|
270
|
-
try {
|
|
271
|
-
LineAnchors.assertProjection(content, lineAnchors);
|
|
272
|
-
if (typeof lineAnchorIdentity !== "string" || lineAnchorIdentity.length === 0) {
|
|
273
|
-
throw new TypeError("READ line anchors require their canonical derivation identity.");
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
catch (cause) {
|
|
277
|
-
throw new InvalidOperationResultError(`Scheme '${schemeName}' returned READ 200 without its core-owned line-anchor projection.`, { cause });
|
|
278
|
-
}
|
|
279
|
-
const resolved = [];
|
|
280
|
-
const checks = [];
|
|
281
|
-
for (const statement of statements) {
|
|
282
|
-
if (statement.lineMarker === null || !LineAnchors.hasAnchor(statement.lineMarker)) {
|
|
283
|
-
resolved.push(statement);
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
const resolution = LineAnchors.resolve(lineAnchors, statement.lineMarker);
|
|
287
|
-
if (!resolution.ok) {
|
|
288
|
-
const { anchor, kind } = resolution.failure;
|
|
289
|
-
const invalid = kind === "invalid";
|
|
290
|
-
if (!invalid)
|
|
291
|
-
return { result: EditCollision.result(lineAnchorIdentity) };
|
|
292
|
-
return {
|
|
293
|
-
result: _a.#failure("line-anchor-invalid", 400, `${anchor} is not valid in that EDIT line-coordinate position.`, {}, {
|
|
294
|
-
anchor,
|
|
295
|
-
target: identity,
|
|
296
|
-
recovery: "Use anchors only where the EDIT scope denotes a line.",
|
|
297
|
-
retryable: false,
|
|
298
|
-
}),
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
for (const [index, anchor] of statement.lineMarker.marks.entries()) {
|
|
302
|
-
if (typeof anchor !== "string")
|
|
303
|
-
continue;
|
|
304
|
-
const line = resolution.marker.marks[index];
|
|
305
|
-
if (typeof line !== "number") {
|
|
306
|
-
throw new InvalidOperationResultError("An EDIT line anchor did not lower to a numeric line.");
|
|
307
|
-
}
|
|
308
|
-
checks.push({ anchor, line });
|
|
309
|
-
}
|
|
310
|
-
resolved.push({ ...statement, lineMarker: resolution.marker });
|
|
311
|
-
}
|
|
312
|
-
const uniqueChecks = [...new Map(checks.map((check) => [`${check.anchor}:${check.line}`, check])).values()];
|
|
313
|
-
return {
|
|
314
|
-
statements: resolved,
|
|
315
|
-
precondition: { identity: lineAnchorIdentity, checks: uniqueChecks },
|
|
316
|
-
};
|
|
233
|
+
async #canonicalFilePath(pathname, workspaceId) {
|
|
234
|
+
return Namespace.canonicalizeSpelling(pathname, await this.#workspaceRoot(workspaceId));
|
|
317
235
|
}
|
|
318
|
-
async
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
assertClassifyingSignal(statement); // {§log-tag-signal}
|
|
324
|
-
const target = this.#extractTarget(statement.target);
|
|
325
|
-
await this.#canonColumns(target, workspaceId);
|
|
326
|
-
const key = JSON.stringify([
|
|
236
|
+
async #editTargetIdentity(statement, workspaceId) {
|
|
237
|
+
const target = this.#extractTarget(statement.target);
|
|
238
|
+
await this.#canonColumns(target, workspaceId);
|
|
239
|
+
return {
|
|
240
|
+
key: JSON.stringify([
|
|
327
241
|
schemeNameOf(statement.target),
|
|
328
242
|
target.scheme,
|
|
329
243
|
target.hostname,
|
|
330
244
|
target.pathname,
|
|
331
245
|
target.fragment,
|
|
332
|
-
])
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
for (const preparedGroup of groups.values()) {
|
|
343
|
-
const group = preparedGroup.statements;
|
|
344
|
-
const first = group[0];
|
|
345
|
-
const schemeName = schemeNameOf(first.target);
|
|
346
|
-
let initial;
|
|
347
|
-
let denial = group.map((statement) => this.#checkWritable(statement, origin)).find((result) => result !== null) ?? null;
|
|
348
|
-
if (denial === null) {
|
|
349
|
-
for (const statement of group) {
|
|
350
|
-
denial = await this.#checkFlagsGate(statement, loopId);
|
|
351
|
-
if (denial !== null)
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if (denial !== null) {
|
|
356
|
-
initial = denial;
|
|
357
|
-
}
|
|
358
|
-
else if (schemeName === null) {
|
|
359
|
-
initial = _a.#failure("target-required", 400, "EDIT requires a target scheme.", {}, { retryable: false });
|
|
360
|
-
}
|
|
361
|
-
else {
|
|
362
|
-
const handler = this.#schemes.get(schemeName);
|
|
363
|
-
const method = handler?.editBatch;
|
|
364
|
-
const manifest = this.#schemes.manifestFor(schemeName);
|
|
365
|
-
if (typeof method !== "function" || manifest === undefined) {
|
|
366
|
-
initial = _a.#failure("operation-not-implemented", 501, `Scheme '${schemeName}' does not implement EDIT batches.`, {}, {
|
|
367
|
-
scheme: schemeName,
|
|
368
|
-
operation: "EDIT",
|
|
369
|
-
retryable: false,
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
else {
|
|
373
|
-
try {
|
|
374
|
-
const resolved = await this.#resolveEditAnchors(group, preparedGroup.identity, schemeName, manifest, schemeCtx);
|
|
375
|
-
if ("result" in resolved) {
|
|
376
|
-
initial = resolved.result;
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
const addressedScheme = first.target?.kind === "url" ? first.target.scheme : schemeName;
|
|
380
|
-
const publishedChannel = first.target?.kind === "url"
|
|
381
|
-
? first.target.fragment ?? manifest.defaultChannel
|
|
382
|
-
: manifest.defaultChannel;
|
|
383
|
-
initial = Results.assert(await method.call(handler, resolved.statements, new SchemeCtxImpl(schemeCtx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions, {
|
|
384
|
-
publishedChannel,
|
|
385
|
-
editPrecondition: resolved.precondition,
|
|
386
|
-
})));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
catch (err) {
|
|
390
|
-
if (err instanceof InvalidOperationResultError)
|
|
391
|
-
throw err;
|
|
392
|
-
console.error(`Scheme '${schemeName}' EDIT batch threw outside its operation result contract:`, err);
|
|
393
|
-
initial = _a.#failure("scheme-handler-threw", 500, `The '${schemeName}' scheme did not produce an EDIT result.`, {}, {
|
|
394
|
-
stage: "scheme-dispatch",
|
|
395
|
-
scheme: schemeName,
|
|
396
|
-
operation: "EDIT",
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
let resolveSettled;
|
|
402
|
-
const settled = new Promise((resolve) => { resolveSettled = resolve; });
|
|
403
|
-
const candidate = initial.editReceipt;
|
|
404
|
-
const expectedNormalizations = group.filter(({ lineMarker }) => lineMarker?.marks.length === 3).length;
|
|
405
|
-
if (initial.status < 400
|
|
406
|
-
&& (initial.scopeNormalizations?.length ?? 0) !== expectedNormalizations) {
|
|
407
|
-
throw new InvalidOperationResultError(`EDIT batch normalized ${initial.scopeNormalizations?.length ?? 0} scope(s), expected ${expectedNormalizations}.`);
|
|
408
|
-
}
|
|
409
|
-
const batch = {
|
|
410
|
-
initial,
|
|
411
|
-
settled,
|
|
412
|
-
aggregate: candidate === undefined || candidate === null
|
|
413
|
-
? undefined
|
|
414
|
-
: assertEditBatchReceipt(candidate),
|
|
415
|
-
settle: resolveSettled,
|
|
416
|
-
};
|
|
417
|
-
let normalizationIndex = 0;
|
|
418
|
-
for (const [index, statement] of group.entries()) {
|
|
419
|
-
const ownsNormalization = statement.lineMarker?.marks.length === 3 && initial.status < 400;
|
|
420
|
-
this.#preparedEdits.set(statement, {
|
|
421
|
-
first: index === 0,
|
|
422
|
-
index,
|
|
423
|
-
normalizationIndex: ownsNormalization ? normalizationIndex++ : null,
|
|
424
|
-
batch,
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
}
|
|
246
|
+
]),
|
|
247
|
+
identity: renderTarget(target),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async prepareEditBatches(statements, context) {
|
|
251
|
+
for (const statement of statements)
|
|
252
|
+
assertClassifyingSignal(statement); // {§log-tag-signal}
|
|
253
|
+
const { workspaceId, workerId, loopId, turnId, origin } = context;
|
|
254
|
+
const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
|
|
255
|
+
await this.#resourceMutations.prepareEditBatches(statements, context, schemeCtx);
|
|
428
256
|
}
|
|
429
257
|
async dispatch(context) {
|
|
430
258
|
assertClassifyingSignal(context.statement); // {§log-tag-signal}
|
|
431
259
|
const result = await this.#dispatchOne(context);
|
|
432
260
|
if (context.statement.op === "EDIT") {
|
|
433
|
-
|
|
434
|
-
if (prepared?.first === true) {
|
|
435
|
-
const normalizations = prepared.batch.initial.scopeNormalizations;
|
|
436
|
-
prepared.batch.settle(normalizations === undefined
|
|
437
|
-
? result
|
|
438
|
-
: Results.assert({ ...result, scopeNormalizations: normalizations }));
|
|
439
|
-
}
|
|
261
|
+
this.#resourceMutations.settleEdit(context.statement, result);
|
|
440
262
|
}
|
|
441
263
|
return result;
|
|
442
264
|
}
|
|
@@ -459,34 +281,7 @@ class Dispatcher {
|
|
|
459
281
|
// those are system failures.
|
|
460
282
|
try {
|
|
461
283
|
if (statement.op === "EDIT") {
|
|
462
|
-
|
|
463
|
-
if (prepared === undefined) {
|
|
464
|
-
throw new InvalidOperationResultError("EDIT reached dispatch without a prepared resource batch.");
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
467
|
-
const settled = prepared.first
|
|
468
|
-
? prepared.batch.initial
|
|
469
|
-
: await prepared.batch.settled;
|
|
470
|
-
const normalization = prepared.normalizationIndex === null
|
|
471
|
-
? undefined
|
|
472
|
-
: settled.scopeNormalizations?.[prepared.normalizationIndex];
|
|
473
|
-
const { scopeNormalizations: _scopeNormalizations, ...projectedSettlement } = settled;
|
|
474
|
-
const statementResult = normalization === undefined
|
|
475
|
-
? projectedSettlement
|
|
476
|
-
: { ...projectedSettlement, scopeNormalizations: [normalization] };
|
|
477
|
-
const aggregate = prepared.batch.aggregate;
|
|
478
|
-
if (aggregate !== undefined) {
|
|
479
|
-
const receipt = assertEditBatchReceipt(aggregate);
|
|
480
|
-
const { editReceipt: _editReceipt, ...withoutAggregate } = statementResult;
|
|
481
|
-
result = {
|
|
482
|
-
...withoutAggregate,
|
|
483
|
-
receipt: projectEditReceipt(receipt, prepared.index),
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
else {
|
|
487
|
-
result = statementResult;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
284
|
+
result = await this.#resourceMutations.preparedEditResult(statement);
|
|
490
285
|
}
|
|
491
286
|
else if (statement.op === "SEND" && statement.target === null) {
|
|
492
287
|
result = await this.#handleSendBroadcast(statement, { workspaceId, workerId, loopId, turnId, sequence });
|
|
@@ -500,10 +295,10 @@ class Dispatcher {
|
|
|
500
295
|
result = await this.#handleWorkerControl(statement, schemeCtx);
|
|
501
296
|
}
|
|
502
297
|
else if (statement.op === "COPY") {
|
|
503
|
-
result = await this.#handleCopy(statement, schemeCtx);
|
|
298
|
+
result = await this.#resourceMutations.handleCopy(statement, schemeCtx);
|
|
504
299
|
}
|
|
505
300
|
else if (statement.op === "MOVE") {
|
|
506
|
-
result = await this.#handleMove(statement, schemeCtx);
|
|
301
|
+
result = await this.#resourceMutations.handleMove(statement, schemeCtx);
|
|
507
302
|
}
|
|
508
303
|
else if (statement.op === "KILL") {
|
|
509
304
|
result = await this.#handleKill(statement, schemeCtx);
|
|
@@ -583,15 +378,13 @@ class Dispatcher {
|
|
|
583
378
|
}
|
|
584
379
|
if (autoAccept) {
|
|
585
380
|
const initialSettlement = await this.#proposals.workerApply(statement, result, { decision: "accept" }, { workspaceId, workerId, loopId, turnId });
|
|
586
|
-
const
|
|
587
|
-
const effective = await this.#settleMoveProposal({
|
|
381
|
+
const effective = await this.#resourceMutations.settleProposal({
|
|
588
382
|
statement,
|
|
589
383
|
result,
|
|
590
|
-
settlement,
|
|
384
|
+
settlement: initialSettlement,
|
|
591
385
|
ctx: schemeCtx,
|
|
592
386
|
ids: { workspaceId, workerId, loopId, turnId },
|
|
593
387
|
});
|
|
594
|
-
this.#recordEditSettlement(statement, effective);
|
|
595
388
|
return this.#proposals.applyResolution(logEntryId, effective);
|
|
596
389
|
}
|
|
597
390
|
// Register the resolution waiter SYNCHRONOUSLY before any await
|
|
@@ -620,15 +413,13 @@ class Dispatcher {
|
|
|
620
413
|
// preserved: an apply failure keeps its original status and
|
|
621
414
|
// Problem Details instead of masquerading as a client rejection.
|
|
622
415
|
const initialSettlement = await this.#proposals.workerApply(statement, result, resolution, { workspaceId, workerId, loopId, turnId });
|
|
623
|
-
const
|
|
624
|
-
const effective = await this.#settleMoveProposal({
|
|
416
|
+
const effective = await this.#resourceMutations.settleProposal({
|
|
625
417
|
statement,
|
|
626
418
|
result,
|
|
627
|
-
settlement,
|
|
419
|
+
settlement: initialSettlement,
|
|
628
420
|
ctx: schemeCtx,
|
|
629
421
|
ids: { workspaceId, workerId, loopId, turnId },
|
|
630
422
|
});
|
|
631
|
-
this.#recordEditSettlement(statement, effective);
|
|
632
423
|
const post = await this.#proposals.applyResolution(logEntryId, effective);
|
|
633
424
|
return post;
|
|
634
425
|
}
|
|
@@ -805,7 +596,7 @@ class Dispatcher {
|
|
|
805
596
|
wakeWorkerNotify: this.#wakeWorkerNotify,
|
|
806
597
|
injectWorker: this.#injectWorker,
|
|
807
598
|
mimetypes: this.#mimetypes,
|
|
808
|
-
|
|
599
|
+
weigh: this.#weighContent,
|
|
809
600
|
pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
|
|
810
601
|
executors: this.#executors(),
|
|
811
602
|
};
|
|
@@ -979,7 +770,7 @@ class Dispatcher {
|
|
|
979
770
|
return check(statement.target);
|
|
980
771
|
}
|
|
981
772
|
// Worker control is FORK/WORK (grammar 0.74.55), not COPY — its body
|
|
982
|
-
// is the new worker's seed prompt, not a destination path. The COPY gates and
|
|
773
|
+
// is the new worker's seed prompt, not a destination path. The COPY gates and ResourceMutations.handleCopy
|
|
983
774
|
// branch on this so they never parse the prompt as a dst path.
|
|
984
775
|
#isWorkerControl(statement) {
|
|
985
776
|
return statement.op === "FORK" || statement.op === "WORK"; // worker control targets worker://<name> (grammar 0.74.55)
|
|
@@ -1056,53 +847,6 @@ class Dispatcher {
|
|
|
1056
847
|
await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: row.id, prompt, flags, parentLoopId: ctx.loopId });
|
|
1057
848
|
return { status: 200, body: name };
|
|
1058
849
|
}
|
|
1059
|
-
async #handleCopy(statement, ctx) {
|
|
1060
|
-
if (statement.target === null) {
|
|
1061
|
-
return _a.#failure("copy-source-required", 400, "COPY requires a source path.", {}, { retryable: false });
|
|
1062
|
-
}
|
|
1063
|
-
if (statement.body === null) {
|
|
1064
|
-
return _a.#failure("copy-destination-required", 400, "COPY requires a destination.", {}, { retryable: false });
|
|
1065
|
-
}
|
|
1066
|
-
return this.#copyOrchestration({
|
|
1067
|
-
statement,
|
|
1068
|
-
source: {
|
|
1069
|
-
target: statement.target,
|
|
1070
|
-
lineMarker: statement.lineMarker,
|
|
1071
|
-
},
|
|
1072
|
-
destination: statement.body,
|
|
1073
|
-
ctx,
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
async #handleMove(statement, ctx) {
|
|
1077
|
-
if (statement.target === null) {
|
|
1078
|
-
return _a.#failure("move-source-required", 400, "MOVE requires a source path.", {}, { retryable: false });
|
|
1079
|
-
}
|
|
1080
|
-
// MOVE is relocation only - deletion is KILL's job ({§move}, {§move-dev-null-not-special}). The /dev/null
|
|
1081
|
-
// and null-body delete-by-MOVE has no alternate meaning.
|
|
1082
|
-
if (statement.body === null) {
|
|
1083
|
-
return _a.#failure("move-destination-required", 400, "MOVE requires a destination.", {}, {
|
|
1084
|
-
recovery: "Use KILL when the intended operation is deletion.",
|
|
1085
|
-
retryable: false,
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
const sourceMarks = statement.lineMarker?.marks;
|
|
1089
|
-
const sourceLineMarker = sourceMarks?.length === 2
|
|
1090
|
-
&& sourceMarks[0] === 1
|
|
1091
|
-
&& sourceMarks[1] === -1
|
|
1092
|
-
? null
|
|
1093
|
-
: statement.lineMarker;
|
|
1094
|
-
return this.#moveOrchestration({
|
|
1095
|
-
statement,
|
|
1096
|
-
source: {
|
|
1097
|
-
target: statement.target,
|
|
1098
|
-
// Canonicalize only the execution selection. #writeLog retains
|
|
1099
|
-
// the authored marker as operation evidence. {§move-canonical-whole-source}
|
|
1100
|
-
lineMarker: sourceLineMarker,
|
|
1101
|
-
},
|
|
1102
|
-
destination: statement.body,
|
|
1103
|
-
ctx,
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
850
|
// KILL is target-polymorphic. Scheme handlers own the optional numeric code's
|
|
1107
851
|
// meaning; core retains worker and entry dispatch. {§operation-code-polymorphism}
|
|
1108
852
|
async #handleKill(statement, ctx) {
|
|
@@ -1173,6 +917,8 @@ class Dispatcher {
|
|
|
1173
917
|
return this.#deleteEntry(schemeName, entryPathnameOf(path), ctx);
|
|
1174
918
|
}
|
|
1175
919
|
async #writeActionlessEntry({ verbatim, workerId, loopId, turnId, sequence, origin, kind, folded, modelCallId = null, attrs = {} }) {
|
|
920
|
+
const durableAttrs = { ...attrs, kind };
|
|
921
|
+
const rx = JSON.stringify({ content: verbatim, mimetype: "text/vnd.plurnk" });
|
|
1176
922
|
const row = await this.#db.engine_insert_log_entry.get({
|
|
1177
923
|
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
|
|
1178
924
|
origin, source: null, model_call_id: modelCallId,
|
|
@@ -1180,10 +926,19 @@ class Dispatcher {
|
|
|
1180
926
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
1181
927
|
pathname: null, query: null, fragment: null, lineMarker: null,
|
|
1182
928
|
tx: "", mimetype_tx: "text/vnd.plurnk",
|
|
1183
|
-
rx
|
|
929
|
+
rx,
|
|
1184
930
|
mimetype_rx: "application/json",
|
|
1185
|
-
status_rx: 200,
|
|
1186
|
-
|
|
931
|
+
status_rx: 200,
|
|
932
|
+
weight: LogBody.weight({
|
|
933
|
+
op: null,
|
|
934
|
+
attrs: durableAttrs,
|
|
935
|
+
tx: "",
|
|
936
|
+
rx,
|
|
937
|
+
mimetypeTx: "text/vnd.plurnk",
|
|
938
|
+
mimetypeRx: "application/json",
|
|
939
|
+
}, this.#weighContent),
|
|
940
|
+
state: "resolved", outcome: null,
|
|
941
|
+
attrs: JSON.stringify(durableAttrs),
|
|
1187
942
|
});
|
|
1188
943
|
if (row === undefined)
|
|
1189
944
|
throw new Error("Dispatcher.#writeActionlessEntry: insert returned no row");
|
|
@@ -1236,804 +991,6 @@ class Dispatcher {
|
|
|
1236
991
|
context.onDispatch?.(logEntryId);
|
|
1237
992
|
return result;
|
|
1238
993
|
}
|
|
1239
|
-
static #isDispatchResult(value) {
|
|
1240
|
-
return "status" in value;
|
|
1241
|
-
}
|
|
1242
|
-
async #resolveResourceSelection(selection, ctx) {
|
|
1243
|
-
const { target, lineMarker } = selection;
|
|
1244
|
-
const scheme = schemeNameOf(target);
|
|
1245
|
-
if (scheme === null) {
|
|
1246
|
-
return _a.#failure("resource-scheme-required", 400, "COPY and MOVE resources require a scheme.", {}, { retryable: false });
|
|
1247
|
-
}
|
|
1248
|
-
if (target.kind === "url" && target.scheme === "worker" && (target.hostname ?? "") !== "") {
|
|
1249
|
-
return _a.#failure("worker-copy-address-invalid", 400, "COPY and MOVE do not address named worker spaces.", {}, {
|
|
1250
|
-
recovery: "Move worker-space content with READ and EDIT.",
|
|
1251
|
-
retryable: false,
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
const handler = this.#schemes.get(scheme);
|
|
1255
|
-
const manifest = this.#schemes.manifestFor(scheme);
|
|
1256
|
-
if (handler === undefined || manifest === undefined) {
|
|
1257
|
-
return _a.#failure("scheme-not-found", 501, `COPY or MOVE addressed the unregistered scheme '${scheme}'.`, {}, {
|
|
1258
|
-
scheme,
|
|
1259
|
-
retryable: false,
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
if (manifest.category !== "data") {
|
|
1263
|
-
return _a.#failure("entry-operation-unsupported", 400, `COPY and MOVE require entry-bearing resources; '${scheme}' is a ${manifest.category} scheme.`, {}, {
|
|
1264
|
-
scheme,
|
|
1265
|
-
category: manifest.category,
|
|
1266
|
-
retryable: false,
|
|
1267
|
-
});
|
|
1268
|
-
}
|
|
1269
|
-
const fragment = target.kind === "url" ? target.fragment : null;
|
|
1270
|
-
const channel = fragment ?? manifest.defaultChannel;
|
|
1271
|
-
if (channel.length === 0) {
|
|
1272
|
-
return _a.#failure("channel-required", 400, `The '${scheme}' scheme has no default channel.`, {}, {
|
|
1273
|
-
scheme,
|
|
1274
|
-
recovery: "Address a named channel with a URI fragment.",
|
|
1275
|
-
retryable: false,
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
if (fragment !== null
|
|
1279
|
-
&& fragment !== manifest.defaultChannel
|
|
1280
|
-
&& !Object.hasOwn(manifest.channels, fragment)) {
|
|
1281
|
-
const availableChannels = [
|
|
1282
|
-
...new Set([manifest.defaultChannel, ...Object.keys(manifest.channels)]),
|
|
1283
|
-
].filter((candidate) => candidate.length > 0);
|
|
1284
|
-
return _a.#failure("channel-not-found", 400, `Channel #${fragment} is not declared by the '${scheme}' scheme.`, {}, {
|
|
1285
|
-
requestedChannel: fragment,
|
|
1286
|
-
availableChannels,
|
|
1287
|
-
retryable: false,
|
|
1288
|
-
});
|
|
1289
|
-
}
|
|
1290
|
-
const pathname = entryPathnameOf(target);
|
|
1291
|
-
const canonicalFilePath = scheme === "file"
|
|
1292
|
-
? Namespace.canonicalizeSpelling(pathname, await this.#workspaceRoot(ctx.workspaceId))
|
|
1293
|
-
: pathname;
|
|
1294
|
-
return {
|
|
1295
|
-
target,
|
|
1296
|
-
lineMarker,
|
|
1297
|
-
scheme,
|
|
1298
|
-
pathname,
|
|
1299
|
-
identityPathname: canonicalFilePath ?? pathname,
|
|
1300
|
-
channel,
|
|
1301
|
-
manifest,
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
#resolveResourceLineMarker(selection, content, operation) {
|
|
1305
|
-
if (!LineAnchors.hasAnchor(selection.lineMarker)) {
|
|
1306
|
-
return {
|
|
1307
|
-
selection: {
|
|
1308
|
-
...selection,
|
|
1309
|
-
lineMarker: selection.lineMarker,
|
|
1310
|
-
},
|
|
1311
|
-
precondition: null,
|
|
1312
|
-
};
|
|
1313
|
-
}
|
|
1314
|
-
const target = this.#resourceAddress(selection);
|
|
1315
|
-
if (selection.manifest.textEditScopes !== true || !selection.manifest.writableBy.includes("model")) {
|
|
1316
|
-
return {
|
|
1317
|
-
result: _a.#failure("line-anchor-unsupported", 400, `The representation at ${target} does not publish line anchors.`, {}, {
|
|
1318
|
-
operation,
|
|
1319
|
-
target,
|
|
1320
|
-
recovery: "Use numeric text coordinates.",
|
|
1321
|
-
retryable: false,
|
|
1322
|
-
}),
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
const resolution = LineAnchors.resolve(LineAnchors.tokens(target, content), selection.lineMarker);
|
|
1326
|
-
if (!resolution.ok) {
|
|
1327
|
-
if (resolution.failure.kind === "invalid") {
|
|
1328
|
-
return {
|
|
1329
|
-
result: _a.#failure("line-anchor-invalid", 400, "A line anchor appeared outside a line-coordinate position.", {}, {
|
|
1330
|
-
operation,
|
|
1331
|
-
target,
|
|
1332
|
-
recovery: "Use anchors only for L, SL, or EL; columns remain numeric.",
|
|
1333
|
-
retryable: false,
|
|
1334
|
-
}),
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
return {
|
|
1338
|
-
result: _a.#failure("line-anchor-collision", 409, `${operation} coordinates collided with current content at ${target}.`, {}, {
|
|
1339
|
-
operation,
|
|
1340
|
-
target,
|
|
1341
|
-
recovery: `READ ${target} again and retry against its current coordinates.`,
|
|
1342
|
-
retryable: true,
|
|
1343
|
-
}),
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
return {
|
|
1347
|
-
selection: { ...selection, lineMarker: resolution.marker },
|
|
1348
|
-
precondition: {
|
|
1349
|
-
identity: target,
|
|
1350
|
-
checks: LineAnchors.checks(selection.lineMarker, resolution.marker),
|
|
1351
|
-
},
|
|
1352
|
-
};
|
|
1353
|
-
}
|
|
1354
|
-
async #selectSource(selection, ctx, operation) {
|
|
1355
|
-
const handler = this.#schemes.get(selection.scheme);
|
|
1356
|
-
if (handler === undefined) {
|
|
1357
|
-
throw new InvalidOperationResultError(`Resolved COPY/MOVE source scheme '${selection.scheme}' is no longer registered.`);
|
|
1358
|
-
}
|
|
1359
|
-
const addressedScheme = selection.target.kind === "url"
|
|
1360
|
-
? selection.target.scheme
|
|
1361
|
-
: selection.scheme;
|
|
1362
|
-
const schemeCtx = new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions, { publishedChannel: selection.channel });
|
|
1363
|
-
const prepared = await this.#prepareDataRepresentation({
|
|
1364
|
-
target: selection.target,
|
|
1365
|
-
routedScheme: selection.scheme,
|
|
1366
|
-
handler,
|
|
1367
|
-
manifest: selection.manifest,
|
|
1368
|
-
ctx,
|
|
1369
|
-
schemeCtx,
|
|
1370
|
-
publishedChannel: selection.channel,
|
|
1371
|
-
});
|
|
1372
|
-
if (prepared.result !== null)
|
|
1373
|
-
return prepared.result;
|
|
1374
|
-
if (prepared.address === null) {
|
|
1375
|
-
return _a.#failure("entry-not-found", 404, `No entry exists at ${this.#resourceAddress(selection)}.`, {}, { target: this.#resourceAddress(selection) });
|
|
1376
|
-
}
|
|
1377
|
-
const storageAddress = prepared.address;
|
|
1378
|
-
const read = await EntryCrud.readEntry(storageAddress.pathname, ctx, storageAddress.scheme, storageAddress.ownerId);
|
|
1379
|
-
if (read.status >= 400)
|
|
1380
|
-
return read;
|
|
1381
|
-
if (read.status !== 200 || read.entry === null) {
|
|
1382
|
-
throw new InvalidOperationResultError(`The '${selection.scheme}' scheme returned status ${read.status} without a COPY/MOVE source entry.`);
|
|
1383
|
-
}
|
|
1384
|
-
const selected = read.entry.channels[selection.channel];
|
|
1385
|
-
if (selected === undefined) {
|
|
1386
|
-
return _a.#failure("channel-not-found", 404, `No channel named #${selection.channel} exists at ${renderAddress(storageAddress.scheme, storageAddress.pathname)}.`, {}, {
|
|
1387
|
-
target: renderAddress(storageAddress.scheme, storageAddress.pathname),
|
|
1388
|
-
requestedChannel: selection.channel,
|
|
1389
|
-
availableChannels: Object.keys(read.entry.channels),
|
|
1390
|
-
retryable: false,
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
const resolvedMarker = this.#resolveResourceLineMarker(selection, selected.content, operation);
|
|
1394
|
-
if ("result" in resolvedMarker)
|
|
1395
|
-
return resolvedMarker.result;
|
|
1396
|
-
let content = selected.content;
|
|
1397
|
-
let scopeNormalizations;
|
|
1398
|
-
if (await MimetypeBinary.isBinaryMimetype(selected.mimetype, ctx.mimetypes)) {
|
|
1399
|
-
return _a.#failure("binary-source-unsupported", 415, `Channel #${selection.channel} is a binary marker and cannot be copied or moved.`, {}, {
|
|
1400
|
-
channel: selection.channel,
|
|
1401
|
-
mimetype: selected.mimetype,
|
|
1402
|
-
recovery: "Use a source with a readable text projection.",
|
|
1403
|
-
retryable: false,
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
if (resolvedMarker.selection.lineMarker !== null) {
|
|
1407
|
-
const sliced = LineMarkerOps.sliceLinesRaw(content, resolvedMarker.selection.lineMarker);
|
|
1408
|
-
if (sliced.status !== 200)
|
|
1409
|
-
return Results.assert(sliced);
|
|
1410
|
-
content = sliced.text ?? "";
|
|
1411
|
-
scopeNormalizations = sliced.scopeNormalizations;
|
|
1412
|
-
}
|
|
1413
|
-
if (selected.producerResult !== undefined && selected.producerResult.status >= 400) {
|
|
1414
|
-
return Results.assert(selected.producerResult);
|
|
1415
|
-
}
|
|
1416
|
-
return {
|
|
1417
|
-
...resolvedMarker.selection,
|
|
1418
|
-
storageAddress,
|
|
1419
|
-
content,
|
|
1420
|
-
completeContent: selected.content,
|
|
1421
|
-
mimetype: selected.mimetype,
|
|
1422
|
-
lineAnchorPrecondition: resolvedMarker.precondition,
|
|
1423
|
-
...(scopeNormalizations === undefined ? {} : { scopeNormalizations }),
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
static #prependScopeNormalizations(result, scopeNormalizations) {
|
|
1427
|
-
if (scopeNormalizations === undefined)
|
|
1428
|
-
return result;
|
|
1429
|
-
return Results.assert({
|
|
1430
|
-
...result,
|
|
1431
|
-
scopeNormalizations: [
|
|
1432
|
-
...scopeNormalizations,
|
|
1433
|
-
...(result.scopeNormalizations ?? []),
|
|
1434
|
-
],
|
|
1435
|
-
});
|
|
1436
|
-
}
|
|
1437
|
-
static #mergeLineAnchorPreconditions(...values) {
|
|
1438
|
-
const present = values.filter((value) => value !== null);
|
|
1439
|
-
if (present.length === 0)
|
|
1440
|
-
return null;
|
|
1441
|
-
const identity = present[0].identity;
|
|
1442
|
-
if (present.some((value) => value.identity !== identity)) {
|
|
1443
|
-
throw new TypeError("Line-anchor preconditions for one edit batch must share a resource identity.");
|
|
1444
|
-
}
|
|
1445
|
-
const checks = [...new Map(present.flatMap(({ checks: valueChecks }) => valueChecks)
|
|
1446
|
-
.map((check) => [`${check.anchor}:${check.line}`, check])).values()];
|
|
1447
|
-
return { identity, checks };
|
|
1448
|
-
}
|
|
1449
|
-
#resourceAddress(selection) {
|
|
1450
|
-
const address = renderTarget({
|
|
1451
|
-
scheme: selection.scheme === "file" ? null : selection.scheme,
|
|
1452
|
-
pathname: selection.scheme === "file"
|
|
1453
|
-
? selection.identityPathname.replace(/^\//, "")
|
|
1454
|
-
: selection.identityPathname,
|
|
1455
|
-
fragment: selection.channel === selection.manifest.defaultChannel
|
|
1456
|
-
? null
|
|
1457
|
-
: selection.channel,
|
|
1458
|
-
});
|
|
1459
|
-
if (address === null)
|
|
1460
|
-
throw new Error("resolved resource selection has no renderable address");
|
|
1461
|
-
return address;
|
|
1462
|
-
}
|
|
1463
|
-
#pendingEffect(selection, action) {
|
|
1464
|
-
return {
|
|
1465
|
-
target: this.#resourceAddress(selection),
|
|
1466
|
-
action,
|
|
1467
|
-
};
|
|
1468
|
-
}
|
|
1469
|
-
static #appliedEffects(result, pending) {
|
|
1470
|
-
// {§edit-result-copy-move-effects} — only an applied mutation earns
|
|
1471
|
-
// engine-composed effects; native scheme receipts remain internal input.
|
|
1472
|
-
const exact = Results.assert(result);
|
|
1473
|
-
if (exact.status === 304 || exact.status === 202 || exact.status >= 300)
|
|
1474
|
-
return exact;
|
|
1475
|
-
if (exact.effects !== undefined) {
|
|
1476
|
-
throw new InvalidOperationResultError("A COPY/MOVE mutation result supplied effects before engine composition.");
|
|
1477
|
-
}
|
|
1478
|
-
const batch = exact.editReceipt === undefined
|
|
1479
|
-
? undefined
|
|
1480
|
-
: assertEditBatchReceipt(exact.editReceipt);
|
|
1481
|
-
const single = batch === undefined && exact.receipt !== undefined
|
|
1482
|
-
? assertEditReceipt(exact.receipt)
|
|
1483
|
-
: undefined;
|
|
1484
|
-
const batchSize = batch === undefined
|
|
1485
|
-
? undefined
|
|
1486
|
-
: "disposition" in batch
|
|
1487
|
-
? batch.superseded.length
|
|
1488
|
-
: batch.effects.length;
|
|
1489
|
-
if (batchSize !== undefined && batchSize !== pending.length) {
|
|
1490
|
-
throw new InvalidOperationResultError(`COPY/MOVE expected ${pending.length} receipt projections, got ${batchSize}.`);
|
|
1491
|
-
}
|
|
1492
|
-
if (single !== undefined && pending.length !== 1) {
|
|
1493
|
-
throw new InvalidOperationResultError(`COPY/MOVE received one receipt for ${pending.length} resource effects.`);
|
|
1494
|
-
}
|
|
1495
|
-
let effects;
|
|
1496
|
-
if (batch !== undefined && "disposition" in batch) {
|
|
1497
|
-
const replacement = pending[0];
|
|
1498
|
-
if (replacement === undefined) {
|
|
1499
|
-
throw new InvalidOperationResultError("A reviewer-replaced COPY/MOVE batch has no resource effect.");
|
|
1500
|
-
}
|
|
1501
|
-
if (pending.some(({ target, action }) => target !== replacement.target || action !== replacement.action)) {
|
|
1502
|
-
throw new InvalidOperationResultError("A reviewer-replaced COPY/MOVE batch spans incompatible resource effects.");
|
|
1503
|
-
}
|
|
1504
|
-
effects = [{
|
|
1505
|
-
...replacement,
|
|
1506
|
-
receipt: projectEditReceipt(batch, 0),
|
|
1507
|
-
}];
|
|
1508
|
-
}
|
|
1509
|
-
else {
|
|
1510
|
-
effects = pending.map((effect, index) => ({
|
|
1511
|
-
...effect,
|
|
1512
|
-
...(batch !== undefined
|
|
1513
|
-
? { receipt: projectEditReceipt(batch, index) }
|
|
1514
|
-
: single !== undefined
|
|
1515
|
-
? { receipt: single }
|
|
1516
|
-
: {}),
|
|
1517
|
-
}));
|
|
1518
|
-
}
|
|
1519
|
-
assertResourceEffects(effects);
|
|
1520
|
-
const { editReceipt: _editReceipt, receipt: _receipt, ...withoutInternalReceipts } = exact;
|
|
1521
|
-
return {
|
|
1522
|
-
...withoutInternalReceipts,
|
|
1523
|
-
effects,
|
|
1524
|
-
};
|
|
1525
|
-
}
|
|
1526
|
-
// {§copy-move-observation} {§edit-result-copy-move-effects} — scoped
|
|
1527
|
-
// COPY/MOVE into an unscoped channel is a text materialization even when
|
|
1528
|
-
// CRUD creates the channel wholesale. Carry the receipt through synchronous
|
|
1529
|
-
// writes and proposals; the destination scheme owns reviewed output.
|
|
1530
|
-
static #withEditMaterialization(result, receipt) {
|
|
1531
|
-
const exact = Results.assert(result);
|
|
1532
|
-
if (exact.status !== 200 && exact.status !== 201 && exact.status !== 202)
|
|
1533
|
-
return exact;
|
|
1534
|
-
const materialized = exact.editReceipt === undefined
|
|
1535
|
-
? assertEditBatchReceipt(receipt)
|
|
1536
|
-
: assertEditBatchReceipt(exact.editReceipt);
|
|
1537
|
-
return {
|
|
1538
|
-
...exact,
|
|
1539
|
-
editReceipt: materialized,
|
|
1540
|
-
...(exact.status === 202
|
|
1541
|
-
? {
|
|
1542
|
-
attrs: {
|
|
1543
|
-
...exact.attrs,
|
|
1544
|
-
editReceipt: materialized,
|
|
1545
|
-
},
|
|
1546
|
-
}
|
|
1547
|
-
: {}),
|
|
1548
|
-
};
|
|
1549
|
-
}
|
|
1550
|
-
#finalizeEffects(result, selection, pending) {
|
|
1551
|
-
const routed = this.#withProposalRoute(result, selection);
|
|
1552
|
-
if (routed.status !== 202)
|
|
1553
|
-
return _a.#appliedEffects(routed, pending);
|
|
1554
|
-
return {
|
|
1555
|
-
...routed,
|
|
1556
|
-
attrs: {
|
|
1557
|
-
...routed.attrs,
|
|
1558
|
-
proposalEffects: pending,
|
|
1559
|
-
},
|
|
1560
|
-
};
|
|
1561
|
-
}
|
|
1562
|
-
static #effectsOf(result) {
|
|
1563
|
-
return result.effects === undefined
|
|
1564
|
-
? []
|
|
1565
|
-
: assertResourceEffects(result.effects);
|
|
1566
|
-
}
|
|
1567
|
-
static #withCombinedEffects(result, ...additional) {
|
|
1568
|
-
const existing = _a.#effectsOf(result);
|
|
1569
|
-
const effects = [...existing, ...additional.flat()];
|
|
1570
|
-
const { effects: _effects, ...withoutEffects } = result;
|
|
1571
|
-
return effects.length === 0
|
|
1572
|
-
? withoutEffects
|
|
1573
|
-
: { ...withoutEffects, effects: assertResourceEffects(effects) };
|
|
1574
|
-
}
|
|
1575
|
-
static #settleProposalEffects(original, settlement) {
|
|
1576
|
-
const pending = original.attrs
|
|
1577
|
-
?.proposalEffects;
|
|
1578
|
-
if (pending === undefined
|
|
1579
|
-
|| settlement.resolution.decision !== "accept"
|
|
1580
|
-
|| settlement.applied === undefined
|
|
1581
|
-
|| settlement.applied.status >= 300) {
|
|
1582
|
-
return settlement;
|
|
1583
|
-
}
|
|
1584
|
-
const projected = settlement.resolution.result ?? {};
|
|
1585
|
-
const aggregate = settlement.applied.editReceipt;
|
|
1586
|
-
const applied = _a.#appliedEffects({
|
|
1587
|
-
...projected,
|
|
1588
|
-
status: settlement.applied.status,
|
|
1589
|
-
...(aggregate === undefined ? {} : { editReceipt: aggregate }),
|
|
1590
|
-
}, pending);
|
|
1591
|
-
const { status: _status, body: _body, ...result } = applied;
|
|
1592
|
-
const { body: _resolutionBody, ...resolution } = settlement.resolution;
|
|
1593
|
-
return {
|
|
1594
|
-
...settlement,
|
|
1595
|
-
resolution: {
|
|
1596
|
-
...resolution,
|
|
1597
|
-
result,
|
|
1598
|
-
},
|
|
1599
|
-
};
|
|
1600
|
-
}
|
|
1601
|
-
#recordEditSettlement(statement, settlement) {
|
|
1602
|
-
if (statement.op !== "EDIT")
|
|
1603
|
-
return;
|
|
1604
|
-
const prepared = this.#preparedEdits.get(statement);
|
|
1605
|
-
if (prepared === undefined || !prepared.first) {
|
|
1606
|
-
throw new InvalidOperationResultError("An EDIT proposal settled without its prepared batch owner.");
|
|
1607
|
-
}
|
|
1608
|
-
const { resolution, applied } = settlement;
|
|
1609
|
-
if (resolution.decision !== "accept"
|
|
1610
|
-
|| applied === undefined
|
|
1611
|
-
|| applied.status >= 300) {
|
|
1612
|
-
prepared.batch.aggregate = undefined;
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
if (applied.editReceipt === null) {
|
|
1616
|
-
prepared.batch.aggregate = undefined;
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
if (applied.editReceipt !== undefined) {
|
|
1620
|
-
prepared.batch.aggregate = assertEditBatchReceipt(applied.editReceipt);
|
|
1621
|
-
return;
|
|
1622
|
-
}
|
|
1623
|
-
if (resolution.body !== undefined)
|
|
1624
|
-
prepared.batch.aggregate = undefined;
|
|
1625
|
-
}
|
|
1626
|
-
static #settlementEffects(settlement) {
|
|
1627
|
-
const effects = settlement.resolution.result
|
|
1628
|
-
?.effects;
|
|
1629
|
-
return effects === undefined ? [] : assertResourceEffects(effects);
|
|
1630
|
-
}
|
|
1631
|
-
static #withSettlementEffects(settlement, effects) {
|
|
1632
|
-
const projected = (settlement.resolution.result ?? {});
|
|
1633
|
-
const { effects: _effects, ...withoutEffects } = projected;
|
|
1634
|
-
const result = effects.length === 0
|
|
1635
|
-
? withoutEffects
|
|
1636
|
-
: {
|
|
1637
|
-
...withoutEffects,
|
|
1638
|
-
effects: assertResourceEffects(effects),
|
|
1639
|
-
};
|
|
1640
|
-
return {
|
|
1641
|
-
...settlement,
|
|
1642
|
-
resolution: {
|
|
1643
|
-
...settlement.resolution,
|
|
1644
|
-
result,
|
|
1645
|
-
},
|
|
1646
|
-
};
|
|
1647
|
-
}
|
|
1648
|
-
#sameChannel(left, right) {
|
|
1649
|
-
return left.scheme === right.scheme
|
|
1650
|
-
&& left.identityPathname === right.identityPathname
|
|
1651
|
-
&& left.channel === right.channel;
|
|
1652
|
-
}
|
|
1653
|
-
#withProposalRoute(result, selection) {
|
|
1654
|
-
if (result.status !== 202)
|
|
1655
|
-
return result;
|
|
1656
|
-
return {
|
|
1657
|
-
...result,
|
|
1658
|
-
attrs: {
|
|
1659
|
-
...result.attrs,
|
|
1660
|
-
proposalScheme: selection.scheme,
|
|
1661
|
-
proposalTarget: {
|
|
1662
|
-
scheme: selection.scheme,
|
|
1663
|
-
pathname: selection.identityPathname,
|
|
1664
|
-
},
|
|
1665
|
-
},
|
|
1666
|
-
};
|
|
1667
|
-
}
|
|
1668
|
-
async #invokeEditBatch(selection, edits, ctx, precondition = null) {
|
|
1669
|
-
const handler = this.#schemes.get(selection.scheme);
|
|
1670
|
-
if (typeof handler?.editBatch !== "function") {
|
|
1671
|
-
return _a.#failure("operation-not-implemented", 501, `Scheme '${selection.scheme}' does not implement EDIT batches.`, {}, {
|
|
1672
|
-
scheme: selection.scheme,
|
|
1673
|
-
operation: "EDIT",
|
|
1674
|
-
retryable: false,
|
|
1675
|
-
});
|
|
1676
|
-
}
|
|
1677
|
-
const statements = edits.map(({ marker, body, position }) => ({
|
|
1678
|
-
op: "EDIT",
|
|
1679
|
-
suffix: "",
|
|
1680
|
-
signal: null,
|
|
1681
|
-
target: selection.target,
|
|
1682
|
-
lineMarker: marker,
|
|
1683
|
-
body,
|
|
1684
|
-
position,
|
|
1685
|
-
}));
|
|
1686
|
-
const addressedScheme = selection.target.kind === "url"
|
|
1687
|
-
? selection.target.scheme
|
|
1688
|
-
: selection.scheme;
|
|
1689
|
-
try {
|
|
1690
|
-
const result = Results.assert(await handler.editBatch(statements, new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions, {
|
|
1691
|
-
publishedChannel: selection.channel,
|
|
1692
|
-
editPrecondition: precondition,
|
|
1693
|
-
})));
|
|
1694
|
-
return this.#withProposalRoute(result, selection);
|
|
1695
|
-
}
|
|
1696
|
-
catch (err) {
|
|
1697
|
-
if (err instanceof InvalidOperationResultError)
|
|
1698
|
-
throw err;
|
|
1699
|
-
console.error(`Scheme '${selection.scheme}' COPY/MOVE edit threw outside its operation result contract:`, err);
|
|
1700
|
-
return _a.#failure("scheme-handler-threw", 500, `The '${selection.scheme}' scheme did not produce a COPY/MOVE edit result.`, {}, {
|
|
1701
|
-
stage: "scheme-dispatch",
|
|
1702
|
-
scheme: selection.scheme,
|
|
1703
|
-
operation: "EDIT",
|
|
1704
|
-
});
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
async #writeDestination(statement, source, destination, ctx) {
|
|
1708
|
-
const existingResult = await this.#readEntry(destination.scheme, destination.pathname, ctx);
|
|
1709
|
-
if (existingResult.status >= 400 && existingResult.status !== 404) {
|
|
1710
|
-
return existingResult;
|
|
1711
|
-
}
|
|
1712
|
-
const existing = existingResult.status === 200
|
|
1713
|
-
? existingResult.entry
|
|
1714
|
-
: null;
|
|
1715
|
-
if (existingResult.status === 200 && existing === null) {
|
|
1716
|
-
throw new InvalidOperationResultError(`The '${destination.scheme}' scheme returned 200 without a destination entry.`);
|
|
1717
|
-
}
|
|
1718
|
-
const destinationChannel = existing?.channels[destination.channel];
|
|
1719
|
-
const expectedMimetype = destinationChannel?.mimetype
|
|
1720
|
-
?? await PathMimetype.resolveEntryMimetype(destination.pathname, destination.manifest.channels[destination.channel] ?? source.mimetype, ctx.mimetypes);
|
|
1721
|
-
if (source.mimetype !== expectedMimetype) {
|
|
1722
|
-
return _a.#failure("mimetype-mismatch", 415, `COPY or MOVE cannot write '${source.mimetype}' into a '${expectedMimetype}' channel.`, {}, {
|
|
1723
|
-
channel: destination.channel,
|
|
1724
|
-
sourceMimetype: source.mimetype,
|
|
1725
|
-
destinationMimetype: expectedMimetype,
|
|
1726
|
-
retryable: false,
|
|
1727
|
-
});
|
|
1728
|
-
}
|
|
1729
|
-
const destinationEffect = this.#pendingEffect(destination, destinationChannel === undefined ? "create" : "update");
|
|
1730
|
-
if (destination.lineMarker !== null) {
|
|
1731
|
-
if (destinationChannel === undefined) {
|
|
1732
|
-
return _a.#failure("destination-region-not-found", 404, `A destination region requires an existing #${destination.channel} channel.`, {}, {
|
|
1733
|
-
destination: this.#resourceAddress(destination),
|
|
1734
|
-
retryable: false,
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
|
-
if (await MimetypeBinary.isBinaryMimetype(destinationChannel.mimetype, ctx.mimetypes)) {
|
|
1738
|
-
return _a.#failure("binary-region-unsupported", 415, `Channel #${destination.channel} is binary and cannot receive a textual region.`, {}, {
|
|
1739
|
-
channel: destination.channel,
|
|
1740
|
-
mimetype: destinationChannel.mimetype,
|
|
1741
|
-
retryable: false,
|
|
1742
|
-
});
|
|
1743
|
-
}
|
|
1744
|
-
const resolvedMarker = this.#resolveResourceLineMarker(destination, destinationChannel.content, statement.op);
|
|
1745
|
-
if ("result" in resolvedMarker)
|
|
1746
|
-
return resolvedMarker.result;
|
|
1747
|
-
const edited = await this.#invokeEditBatch(resolvedMarker.selection, [{
|
|
1748
|
-
marker: resolvedMarker.selection.lineMarker,
|
|
1749
|
-
body: source.content,
|
|
1750
|
-
position: statement.position,
|
|
1751
|
-
}], ctx, resolvedMarker.precondition);
|
|
1752
|
-
return this.#finalizeEffects(edited, resolvedMarker.selection, [destinationEffect]);
|
|
1753
|
-
}
|
|
1754
|
-
if (destinationChannel !== undefined
|
|
1755
|
-
&& destinationChannel.content !== source.content) {
|
|
1756
|
-
return _a.#failure("copy-destination-exists", 409, `COPY or MOVE destination ${this.#resourceAddress(destination)} already contains different content.`, {}, {
|
|
1757
|
-
destination: this.#resourceAddress(destination),
|
|
1758
|
-
retryable: false,
|
|
1759
|
-
});
|
|
1760
|
-
}
|
|
1761
|
-
if (destinationChannel !== undefined)
|
|
1762
|
-
return { status: 304 };
|
|
1763
|
-
const channels = {
|
|
1764
|
-
...(existing?.channels ?? {}),
|
|
1765
|
-
[destination.channel]: {
|
|
1766
|
-
content: source.content,
|
|
1767
|
-
mimetype: source.mimetype,
|
|
1768
|
-
},
|
|
1769
|
-
};
|
|
1770
|
-
const written = await this.#writeEntry(destination.scheme, destination.pathname, { channels }, ctx);
|
|
1771
|
-
const exactWritten = Results.assert(written);
|
|
1772
|
-
const parseIssues = exactWritten.status === 200 || exactWritten.status === 201
|
|
1773
|
-
? await new DbProjectionCaps(ctx).parseIssues(source.content, source.mimetype)
|
|
1774
|
-
: undefined;
|
|
1775
|
-
const materialized = source.lineMarker === null
|
|
1776
|
-
|| (exactWritten.status !== 200 && exactWritten.status !== 201 && exactWritten.status !== 202)
|
|
1777
|
-
? exactWritten
|
|
1778
|
-
: _a.#withEditMaterialization(exactWritten, editReceipt("", source.content, [{
|
|
1779
|
-
marker: { marks: [1, -1] },
|
|
1780
|
-
body: source.content,
|
|
1781
|
-
}], parseIssues));
|
|
1782
|
-
return this.#finalizeEffects(materialized, destination, [destinationEffect]);
|
|
1783
|
-
}
|
|
1784
|
-
async #copyOrchestration({ statement, source, destination, ctx, }) {
|
|
1785
|
-
const resolvedSource = await this.#resolveResourceSelection(source, ctx);
|
|
1786
|
-
if (_a.#isDispatchResult(resolvedSource))
|
|
1787
|
-
return resolvedSource;
|
|
1788
|
-
const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
|
|
1789
|
-
if (_a.#isDispatchResult(resolvedDestination))
|
|
1790
|
-
return resolvedDestination;
|
|
1791
|
-
const selected = await this.#selectSource(resolvedSource, ctx, "COPY");
|
|
1792
|
-
if (_a.#isDispatchResult(selected))
|
|
1793
|
-
return selected;
|
|
1794
|
-
const result = await this.#writeDestination(statement, selected, resolvedDestination, ctx);
|
|
1795
|
-
return _a.#prependScopeNormalizations(result, selected.scopeNormalizations);
|
|
1796
|
-
}
|
|
1797
|
-
#deferredMoveSource(source, destination, lineAnchorPrecondition) {
|
|
1798
|
-
return {
|
|
1799
|
-
target: source.target,
|
|
1800
|
-
lineMarker: source.lineMarker,
|
|
1801
|
-
scheme: source.scheme,
|
|
1802
|
-
pathname: source.pathname,
|
|
1803
|
-
channel: source.channel,
|
|
1804
|
-
destination: this.#resourceAddress(destination),
|
|
1805
|
-
lineAnchorPrecondition,
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
#moveFailureAfterDestination(result, destination, destinationEffects) {
|
|
1809
|
-
const exact = Results.assert(result);
|
|
1810
|
-
if (exact.status < 400) {
|
|
1811
|
-
throw new InvalidOperationResultError("A successful MOVE source result was classified as a partial failure.");
|
|
1812
|
-
}
|
|
1813
|
-
if (exact.problem === undefined) {
|
|
1814
|
-
throw new InvalidOperationResultError("A failed MOVE source mutation has no Problem Details.");
|
|
1815
|
-
}
|
|
1816
|
-
const failed = Results.assert({
|
|
1817
|
-
...exact,
|
|
1818
|
-
problem: {
|
|
1819
|
-
...exact.problem,
|
|
1820
|
-
operation: "MOVE",
|
|
1821
|
-
destinationWritten: true,
|
|
1822
|
-
destination,
|
|
1823
|
-
},
|
|
1824
|
-
});
|
|
1825
|
-
return _a.#withCombinedEffects(failed, destinationEffects);
|
|
1826
|
-
}
|
|
1827
|
-
async #removeMoveSource(statement, source, ctx, lineAnchorPrecondition = null) {
|
|
1828
|
-
const effect = this.#pendingEffect(source, source.lineMarker === null ? "delete" : "update");
|
|
1829
|
-
if (source.lineMarker === null) {
|
|
1830
|
-
const deleted = await this.#deleteChannel(source.scheme, source.pathname, source.channel, ctx);
|
|
1831
|
-
return this.#finalizeEffects(Results.assert(deleted), source, [effect]);
|
|
1832
|
-
}
|
|
1833
|
-
const edited = await this.#invokeEditBatch(source, [{
|
|
1834
|
-
marker: source.lineMarker,
|
|
1835
|
-
body: "",
|
|
1836
|
-
position: statement.position,
|
|
1837
|
-
}], ctx, lineAnchorPrecondition);
|
|
1838
|
-
return this.#finalizeEffects(edited, source, [effect]);
|
|
1839
|
-
}
|
|
1840
|
-
async #moveWithinChannel(statement, source, destination, ctx) {
|
|
1841
|
-
if (source.lineMarker === null) {
|
|
1842
|
-
if (destination.lineMarker !== null) {
|
|
1843
|
-
return _a.#failure("move-region-overlap", 409, "MOVE cannot insert a whole channel into itself and then remove that channel.", {}, {
|
|
1844
|
-
source: this.#resourceAddress(source),
|
|
1845
|
-
destination: this.#resourceAddress(destination),
|
|
1846
|
-
retryable: false,
|
|
1847
|
-
});
|
|
1848
|
-
}
|
|
1849
|
-
return this.#writeDestination(statement, source, destination, ctx);
|
|
1850
|
-
}
|
|
1851
|
-
if (destination.lineMarker === null) {
|
|
1852
|
-
return this.#writeDestination(statement, source, destination, ctx);
|
|
1853
|
-
}
|
|
1854
|
-
const resolvedDestination = this.#resolveResourceLineMarker(destination, source.completeContent, "MOVE");
|
|
1855
|
-
if ("result" in resolvedDestination)
|
|
1856
|
-
return resolvedDestination.result;
|
|
1857
|
-
const precondition = _a.#mergeLineAnchorPreconditions(source.lineAnchorPrecondition, resolvedDestination.precondition);
|
|
1858
|
-
const moved = await this.#invokeEditBatch(resolvedDestination.selection, [
|
|
1859
|
-
{
|
|
1860
|
-
marker: resolvedDestination.selection.lineMarker,
|
|
1861
|
-
body: source.content,
|
|
1862
|
-
position: statement.position,
|
|
1863
|
-
},
|
|
1864
|
-
{
|
|
1865
|
-
marker: source.lineMarker,
|
|
1866
|
-
body: "",
|
|
1867
|
-
position: statement.position,
|
|
1868
|
-
},
|
|
1869
|
-
], ctx, precondition);
|
|
1870
|
-
const effect = this.#pendingEffect(resolvedDestination.selection, "update");
|
|
1871
|
-
return this.#finalizeEffects(moved, resolvedDestination.selection, [effect, effect]);
|
|
1872
|
-
}
|
|
1873
|
-
async #moveOrchestration({ statement, source, destination, ctx, }) {
|
|
1874
|
-
const resolvedSource = await this.#resolveResourceSelection(source, ctx);
|
|
1875
|
-
if (_a.#isDispatchResult(resolvedSource))
|
|
1876
|
-
return resolvedSource;
|
|
1877
|
-
const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
|
|
1878
|
-
if (_a.#isDispatchResult(resolvedDestination))
|
|
1879
|
-
return resolvedDestination;
|
|
1880
|
-
const selected = await this.#selectSource(resolvedSource, ctx, "MOVE");
|
|
1881
|
-
if (_a.#isDispatchResult(selected))
|
|
1882
|
-
return selected;
|
|
1883
|
-
if (this.#sameChannel(resolvedSource, resolvedDestination)) {
|
|
1884
|
-
const result = await this.#moveWithinChannel(statement, selected, resolvedDestination, ctx);
|
|
1885
|
-
return resolvedDestination.lineMarker === null
|
|
1886
|
-
? _a.#prependScopeNormalizations(result, selected.scopeNormalizations)
|
|
1887
|
-
: result;
|
|
1888
|
-
}
|
|
1889
|
-
const destinationResult = _a.#prependScopeNormalizations(await this.#writeDestination(statement, selected, resolvedDestination, ctx), selected.scopeNormalizations);
|
|
1890
|
-
if (destinationResult.status >= 400)
|
|
1891
|
-
return destinationResult;
|
|
1892
|
-
const destinationAddress = this.#resourceAddress(resolvedDestination);
|
|
1893
|
-
const destinationEffects = _a.#effectsOf(destinationResult);
|
|
1894
|
-
if (destinationResult.status === 202) {
|
|
1895
|
-
return {
|
|
1896
|
-
...destinationResult,
|
|
1897
|
-
attrs: {
|
|
1898
|
-
...destinationResult.attrs,
|
|
1899
|
-
moveSource: this.#deferredMoveSource(selected, resolvedDestination, selected.lineAnchorPrecondition),
|
|
1900
|
-
},
|
|
1901
|
-
};
|
|
1902
|
-
}
|
|
1903
|
-
const sourceResult = await this.#removeMoveSource(statement, selected, ctx, selected.lineAnchorPrecondition);
|
|
1904
|
-
if (sourceResult.status === 202) {
|
|
1905
|
-
return {
|
|
1906
|
-
...sourceResult,
|
|
1907
|
-
...(destinationResult.scopeNormalizations === undefined
|
|
1908
|
-
? {}
|
|
1909
|
-
: { scopeNormalizations: destinationResult.scopeNormalizations }),
|
|
1910
|
-
attrs: {
|
|
1911
|
-
...sourceResult.attrs,
|
|
1912
|
-
moveDestinationWritten: destinationAddress,
|
|
1913
|
-
moveDestinationEffects: destinationEffects,
|
|
1914
|
-
},
|
|
1915
|
-
};
|
|
1916
|
-
}
|
|
1917
|
-
if (sourceResult.status >= 400) {
|
|
1918
|
-
return this.#moveFailureAfterDestination(destinationResult.scopeNormalizations === undefined
|
|
1919
|
-
? sourceResult
|
|
1920
|
-
: Results.assert({
|
|
1921
|
-
...sourceResult,
|
|
1922
|
-
scopeNormalizations: destinationResult.scopeNormalizations,
|
|
1923
|
-
}), destinationAddress, destinationEffects);
|
|
1924
|
-
}
|
|
1925
|
-
const base = destinationResult.status === 304
|
|
1926
|
-
? { ...destinationResult, status: 200 }
|
|
1927
|
-
: destinationResult;
|
|
1928
|
-
return _a.#withCombinedEffects(base, _a.#effectsOf(sourceResult));
|
|
1929
|
-
}
|
|
1930
|
-
async #settleMoveProposal({ statement, result, settlement, ctx, ids, }) {
|
|
1931
|
-
if (statement.op !== "MOVE")
|
|
1932
|
-
return settlement;
|
|
1933
|
-
const attrs = result.attrs;
|
|
1934
|
-
const destinationWritten = attrs?.moveDestinationWritten;
|
|
1935
|
-
if (destinationWritten !== undefined) {
|
|
1936
|
-
const destinationEffects = attrs?.moveDestinationEffects === undefined
|
|
1937
|
-
? []
|
|
1938
|
-
: assertResourceEffects(attrs.moveDestinationEffects);
|
|
1939
|
-
if (settlement.resolution.decision !== "accept") {
|
|
1940
|
-
const decision = settlement.resolution.decision;
|
|
1941
|
-
return {
|
|
1942
|
-
resolution: settlement.resolution,
|
|
1943
|
-
applied: this.#moveFailureAfterDestination(_a.#failure("move-source-not-applied", decision === "cancel" ? 499 : 409, `The MOVE destination was written, but source removal was ${decision === "cancel" ? "cancelled" : "rejected"}.`, {}, {
|
|
1944
|
-
retryable: false,
|
|
1945
|
-
}), destinationWritten, destinationEffects),
|
|
1946
|
-
};
|
|
1947
|
-
}
|
|
1948
|
-
if (settlement.applied === undefined) {
|
|
1949
|
-
return {
|
|
1950
|
-
resolution: settlement.resolution,
|
|
1951
|
-
applied: this.#moveFailureAfterDestination(_a.#failure("proposal-apply-missing", 500, "The source scheme accepted its MOVE proposal without applying the source mutation.", {}, {
|
|
1952
|
-
stage: "proposal-application",
|
|
1953
|
-
retryable: false,
|
|
1954
|
-
}), destinationWritten, destinationEffects),
|
|
1955
|
-
};
|
|
1956
|
-
}
|
|
1957
|
-
if (settlement.applied.status >= 400) {
|
|
1958
|
-
return {
|
|
1959
|
-
resolution: settlement.resolution,
|
|
1960
|
-
applied: this.#moveFailureAfterDestination(settlement.applied, destinationWritten, destinationEffects),
|
|
1961
|
-
};
|
|
1962
|
-
}
|
|
1963
|
-
return _a.#withSettlementEffects(settlement, [
|
|
1964
|
-
...destinationEffects,
|
|
1965
|
-
..._a.#settlementEffects(settlement),
|
|
1966
|
-
]);
|
|
1967
|
-
}
|
|
1968
|
-
const deferred = attrs?.moveSource;
|
|
1969
|
-
if (deferred === undefined
|
|
1970
|
-
|| settlement.resolution.decision !== "accept"
|
|
1971
|
-
|| (settlement.applied?.status ?? 200) >= 400) {
|
|
1972
|
-
return settlement;
|
|
1973
|
-
}
|
|
1974
|
-
if (settlement.applied === undefined) {
|
|
1975
|
-
return {
|
|
1976
|
-
resolution: settlement.resolution,
|
|
1977
|
-
applied: _a.#failure("proposal-apply-missing", 500, "The destination scheme accepted its MOVE proposal without applying the destination mutation.", {}, {
|
|
1978
|
-
stage: "proposal-application",
|
|
1979
|
-
retryable: false,
|
|
1980
|
-
}),
|
|
1981
|
-
};
|
|
1982
|
-
}
|
|
1983
|
-
const destinationEffects = _a.#settlementEffects(settlement);
|
|
1984
|
-
const resolvedSource = await this.#resolveResourceSelection({
|
|
1985
|
-
target: deferred.target,
|
|
1986
|
-
lineMarker: deferred.lineMarker,
|
|
1987
|
-
}, ctx);
|
|
1988
|
-
if (_a.#isDispatchResult(resolvedSource)) {
|
|
1989
|
-
return {
|
|
1990
|
-
resolution: settlement.resolution,
|
|
1991
|
-
applied: this.#moveFailureAfterDestination(resolvedSource, deferred.destination, destinationEffects),
|
|
1992
|
-
};
|
|
1993
|
-
}
|
|
1994
|
-
if (resolvedSource.scheme !== deferred.scheme
|
|
1995
|
-
|| resolvedSource.pathname !== deferred.pathname
|
|
1996
|
-
|| resolvedSource.channel !== deferred.channel) {
|
|
1997
|
-
throw new InvalidOperationResultError("A deferred MOVE source no longer resolves to its recorded identity.");
|
|
1998
|
-
}
|
|
1999
|
-
const removed = await this.#removeMoveSource(statement, {
|
|
2000
|
-
...resolvedSource,
|
|
2001
|
-
lineMarker: resolvedSource.lineMarker,
|
|
2002
|
-
}, ctx, deferred.lineAnchorPrecondition);
|
|
2003
|
-
if (removed.status >= 400) {
|
|
2004
|
-
return {
|
|
2005
|
-
resolution: settlement.resolution,
|
|
2006
|
-
applied: this.#moveFailureAfterDestination(removed, deferred.destination, destinationEffects),
|
|
2007
|
-
};
|
|
2008
|
-
}
|
|
2009
|
-
if (removed.status !== 202) {
|
|
2010
|
-
return _a.#withSettlementEffects(settlement, [
|
|
2011
|
-
...destinationEffects,
|
|
2012
|
-
..._a.#effectsOf(removed),
|
|
2013
|
-
]);
|
|
2014
|
-
}
|
|
2015
|
-
const initialSourceSettlement = await this.#proposals.workerApply(statement, removed, { decision: "accept" }, ids);
|
|
2016
|
-
const sourceSettlement = _a.#settleProposalEffects(removed, initialSourceSettlement);
|
|
2017
|
-
if (sourceSettlement.applied === undefined) {
|
|
2018
|
-
return {
|
|
2019
|
-
resolution: settlement.resolution,
|
|
2020
|
-
applied: this.#moveFailureAfterDestination(_a.#failure("proposal-apply-missing", 500, "The source scheme accepted its MOVE proposal without applying the source mutation.", {}, {
|
|
2021
|
-
stage: "proposal-application",
|
|
2022
|
-
retryable: false,
|
|
2023
|
-
}), deferred.destination, destinationEffects),
|
|
2024
|
-
};
|
|
2025
|
-
}
|
|
2026
|
-
if (sourceSettlement.applied.status >= 400) {
|
|
2027
|
-
return {
|
|
2028
|
-
resolution: settlement.resolution,
|
|
2029
|
-
applied: this.#moveFailureAfterDestination(sourceSettlement.applied, deferred.destination, destinationEffects),
|
|
2030
|
-
};
|
|
2031
|
-
}
|
|
2032
|
-
return _a.#withSettlementEffects(settlement, [
|
|
2033
|
-
...destinationEffects,
|
|
2034
|
-
..._a.#settlementEffects(sourceSettlement),
|
|
2035
|
-
]);
|
|
2036
|
-
}
|
|
2037
994
|
// {§send-premature-terminate} — the unified PENDING SET, judged at the terminal's OWN dispatch
|
|
2038
995
|
// (post-batch: the emission's earlier ops already executed, so a same-turn KILL+[200] repairs in
|
|
2039
996
|
// ONE turn, and a same-turn WORK+[200] is caught — the spawn is live by the time the SEND lands).
|
|
@@ -2262,8 +1219,8 @@ class Dispatcher {
|
|
|
2262
1219
|
content: null,
|
|
2263
1220
|
mimetype: null,
|
|
2264
1221
|
results: [],
|
|
2265
|
-
|
|
2266
|
-
|
|
1222
|
+
itemsWeightTotal: 0,
|
|
1223
|
+
returnedItemsWeightTotal: 0,
|
|
2267
1224
|
matchingPathCount: 0,
|
|
2268
1225
|
matchLocationCount: 0,
|
|
2269
1226
|
}
|
|
@@ -2406,8 +1363,8 @@ class Dispatcher {
|
|
|
2406
1363
|
content: null,
|
|
2407
1364
|
mimetype: null,
|
|
2408
1365
|
results: [],
|
|
2409
|
-
|
|
2410
|
-
|
|
1366
|
+
itemsWeightTotal: 0,
|
|
1367
|
+
returnedItemsWeightTotal: 0,
|
|
2411
1368
|
matchingPathCount: 0,
|
|
2412
1369
|
matchLocationCount: 0,
|
|
2413
1370
|
}, { target });
|
|
@@ -2558,7 +1515,14 @@ class Dispatcher {
|
|
|
2558
1515
|
rx: rxJson,
|
|
2559
1516
|
mimetype_rx: "application/json",
|
|
2560
1517
|
status_rx: result.status,
|
|
2561
|
-
|
|
1518
|
+
weight: LogBody.weight({
|
|
1519
|
+
op: durableStatement.op,
|
|
1520
|
+
attrs,
|
|
1521
|
+
tx: txJson,
|
|
1522
|
+
rx: rxJson,
|
|
1523
|
+
mimetypeTx: "application/json",
|
|
1524
|
+
mimetypeRx: "application/json",
|
|
1525
|
+
}, this.#weighContent),
|
|
2562
1526
|
state: isProposed ? "proposed" : "resolved",
|
|
2563
1527
|
outcome: null,
|
|
2564
1528
|
attrs,
|