@volter/world-core 3.0.149 → 3.0.150
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/dist/src/changeset.js +7 -5
- package/dist/src/history.d.ts +3 -0
- package/dist/src/history.js +7 -0
- package/dist/src/log.d.ts +4 -0
- package/dist/src/log.js +30 -15
- package/package.json +1 -1
- package/src/changeset.ts +7 -5
- package/src/history.ts +6 -0
- package/src/log.ts +26 -11
package/dist/src/changeset.js
CHANGED
|
@@ -57,7 +57,7 @@ import { worldPaths } from "./storage.js";
|
|
|
57
57
|
// the control root that holds it) and never learns what a world is. `volter-world` supplies
|
|
58
58
|
// discovery and the CLI verbs; the state-level logic lives here, beside the ledgers it reads.
|
|
59
59
|
import { createHash } from 'node:crypto';
|
|
60
|
-
import { aliasesFrom,
|
|
60
|
+
import { aliasesFrom, readTreeMapsAt, wholeLog } from "./log.js";
|
|
61
61
|
import { appendActionIfAbsent, checkPrecondition, listActions, projectedPreconditionValue, projectResources, isTwinBookkeeping } from "./actions.js";
|
|
62
62
|
import { canonicalJson, diffSubjectFields, hashFieldValue } from "./hash.js";
|
|
63
63
|
import { applyTwinWrite } from "./serve.js";
|
|
@@ -173,11 +173,13 @@ export function describeActionFields(ledgers, actions) {
|
|
|
173
173
|
if (!positions.has(e.id))
|
|
174
174
|
positions.set(e.id, e.batch ? batchStart : i);
|
|
175
175
|
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
const mine = actions.filter(a => a.service === ledger.service && a.stateService === ledger.stateService && a.action.fields && positions.has(a.action.id));
|
|
177
|
+
// every write's pre-state from one capture of this twin's history (a capture per write read the whole log each time)
|
|
178
|
+
const trees = mine.length ? readTreeMapsAt(ledger.stateService, ledger.controlRoot, mine.map(a => positions.get(a.action.id))) : new Map();
|
|
179
|
+
for (const { action, service, stateService } of mine) {
|
|
180
|
+
if (!action.fields)
|
|
179
181
|
continue;
|
|
180
|
-
const subject =
|
|
182
|
+
const subject = trees.get(positions.get(action.id))?.get(`${action.subject.type}:${action.subject.id}`);
|
|
181
183
|
const fields = subject?.fields ?? {};
|
|
182
184
|
fieldChanges[fieldDiffKey(service, stateService, action.id)] = diffSubjectFields({
|
|
183
185
|
subject: action.subject, fields, fieldHashes: Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, hashFieldValue(v)])),
|
package/dist/src/history.d.ts
CHANGED
|
@@ -40,6 +40,9 @@ export declare function validateHistoryOrigin(origin: HistoryOrigin, layout?: Hi
|
|
|
40
40
|
export declare const historyEntryTime: (entry: Entry) => number;
|
|
41
41
|
export declare const historyDigest: (value: unknown) => string;
|
|
42
42
|
export declare function historyLength(layout: HistoryLayout, depth?: number): number;
|
|
43
|
+
/** The layout of a view's first `at` entries: historyPrefix's, without the segment digests a prefix view carries (a
|
|
44
|
+
* fold reads only the layout, and a partial segment's digest re-reads and hashes its log). */
|
|
45
|
+
export declare function historyLayoutAt(layout: HistoryLayout, at: number): HistoryLayout;
|
|
43
46
|
export declare function readHistoryView(directory: string, id: string): HistoryView;
|
|
44
47
|
export declare function historyEntries(view: HistoryView): Entry[];
|
|
45
48
|
export declare function historyPrefix(view: HistoryView, at: number): HistoryView;
|
package/dist/src/history.js
CHANGED
|
@@ -40,6 +40,13 @@ const viewPath = (directory, id) => {
|
|
|
40
40
|
function rows(directory, file) {
|
|
41
41
|
return getActiveWorldStore().readLines(join(directory, file)).filter(line => line.trim()).map(line => toEntry(parseExactJson(line)));
|
|
42
42
|
}
|
|
43
|
+
/** The layout of a view's first `at` entries: historyPrefix's, without the segment digests a prefix view carries (a
|
|
44
|
+
* fold reads only the layout, and a partial segment's digest re-reads and hashes its log). */
|
|
45
|
+
export function historyLayoutAt(layout, at) {
|
|
46
|
+
if (!Number.isInteger(at) || at < 0 || at > historyLength(layout))
|
|
47
|
+
throw new Error(`Invalid history position ${at}`);
|
|
48
|
+
return sliceLayout(layout, at);
|
|
49
|
+
}
|
|
43
50
|
function sliceLayout(layout, at) {
|
|
44
51
|
const base = layout.base ? historyLength(layout.base) : 0;
|
|
45
52
|
return { ...(layout.base ? { base: sliceLayout(layout.base, Math.min(at, base)) } : {}), parent: Math.min(Math.max(0, at - base), layout.parent), branch: Math.min(Math.max(0, at - base - layout.parent), layout.branch) };
|
package/dist/src/log.d.ts
CHANGED
|
@@ -183,6 +183,10 @@ export declare function treeChangesSince(service: string, root: string | undefin
|
|
|
183
183
|
removed: string[];
|
|
184
184
|
appended: string[];
|
|
185
185
|
} | undefined;
|
|
186
|
+
/** The tree at each of several positions of a twin's history, folded from one capture of it: what
|
|
187
|
+
* `readTreeMap(service, root, { at })` answers for each, without capturing the history again per position (a
|
|
188
|
+
* changeset's field changes read one position per write, and a capture reads the whole log). */
|
|
189
|
+
export declare function readTreeMapsAt(service: string, root: string | undefined, positions: Iterable<number>): Map<number, Tree>;
|
|
186
190
|
export declare function cutCheckpoint(service: string, root?: string): void;
|
|
187
191
|
/** The branch's entries the parent does not hold: `set` rows, not reverted, not landed. */
|
|
188
192
|
export declare function unpushedEntries(service: string, root?: string): Entry[];
|
package/dist/src/log.js
CHANGED
|
@@ -23,7 +23,7 @@ import { parseExactJson } from "./exact-json.js";
|
|
|
23
23
|
// holds. The parent log accepts the v1 observed-event rows too (`toEntry` reads a delta event as
|
|
24
24
|
// an entry; a non-delta observation folds nothing and is kept for `listEvents`), so a World whose
|
|
25
25
|
// log was written in that form still reads.
|
|
26
|
-
import { captureHistory, historyChanges, historyEntryTime, historyEntries, historyLength, historyPrefix, inheritedHistory, inheritedHistoryKey, originHead, readHistoryView, replaceHistoryOrigin, saveHistoryView, validateHistoryOrigin } from "./history.js";
|
|
26
|
+
import { captureHistory, historyChanges, historyEntryTime, historyEntries, historyLayoutAt, historyLength, historyPrefix, inheritedHistory, inheritedHistoryKey, originHead, readHistoryView, replaceHistoryOrigin, saveHistoryView, validateHistoryOrigin } from "./history.js";
|
|
27
27
|
import { dirname, join } from 'node:path';
|
|
28
28
|
import { canonicalStatePath, checkParent, commitParentPin, pinParent, withAncestryLock, withHistoryLock } from "./ancestry.js";
|
|
29
29
|
import { resolveReferences } from "./references.js";
|
|
@@ -569,24 +569,39 @@ export function treeChangesSince(service, root, since) {
|
|
|
569
569
|
}
|
|
570
570
|
return { stamp, changed: treeResources(present), removed, appended: [...order.keys()].filter((k) => tree.has(k)) };
|
|
571
571
|
}
|
|
572
|
+
/** The tree at each of several positions of a twin's history, folded from one capture of it: what
|
|
573
|
+
* `readTreeMap(service, root, { at })` answers for each, without capturing the history again per position (a
|
|
574
|
+
* changeset's field changes read one position per write, and a capture reads the whole log). */
|
|
575
|
+
export function readTreeMapsAt(service, root, positions) {
|
|
576
|
+
return withHistoryLock(worldPaths(service, root).dir, () => {
|
|
577
|
+
const view = captureHistory(service, root).descriptor;
|
|
578
|
+
const entries = historyEntries(view);
|
|
579
|
+
const trees = new Map();
|
|
580
|
+
for (const at of positions)
|
|
581
|
+
if (!trees.has(at))
|
|
582
|
+
trees.set(at, treeAt(view, entries, at, service));
|
|
583
|
+
return trees;
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
/** The tree after a view's first `at` entries; a position inside an observation has none. */
|
|
587
|
+
function treeAt(view, entries, at, service) {
|
|
588
|
+
if (at > 0 && at < entries.length && entries[at]?.batch && entries[at]?.batch === entries[at - 1]?.batch) {
|
|
589
|
+
let from = at;
|
|
590
|
+
let to = at;
|
|
591
|
+
const batch = entries[at].batch;
|
|
592
|
+
while (from > 0 && entries[from - 1]?.batch === batch)
|
|
593
|
+
from--;
|
|
594
|
+
while (to < entries.length && entries[to]?.batch === batch)
|
|
595
|
+
to++;
|
|
596
|
+
throw new Error(`position ${at} falls inside observation ${batch} — use ${from} or ${to}`);
|
|
597
|
+
}
|
|
598
|
+
return foldHistory(entries.slice(0, at), historyLayoutAt(view.layout, at), service);
|
|
599
|
+
}
|
|
572
600
|
function readTreeMapFull(service, root, opts = {}) {
|
|
573
601
|
return withHistoryLock(worldPaths(service, root).dir, () => {
|
|
574
602
|
if (opts.at !== undefined || opts.view !== undefined) {
|
|
575
603
|
const view = opts.view ? readHistoryView(worldPaths(service, root).dir, opts.view) : captureHistory(service, root).descriptor;
|
|
576
|
-
|
|
577
|
-
const entries = historyEntries(view);
|
|
578
|
-
if (at > 0 && at < entries.length && entries[at]?.batch && entries[at]?.batch === entries[at - 1]?.batch) {
|
|
579
|
-
let from = at;
|
|
580
|
-
let to = at;
|
|
581
|
-
const batch = entries[at].batch;
|
|
582
|
-
while (from > 0 && entries[from - 1]?.batch === batch)
|
|
583
|
-
from--;
|
|
584
|
-
while (to < entries.length && entries[to]?.batch === batch)
|
|
585
|
-
to++;
|
|
586
|
-
throw new Error(`position ${at} falls inside observation ${batch} — use ${from} or ${to}`);
|
|
587
|
-
}
|
|
588
|
-
const prefix = historyPrefix(view, at);
|
|
589
|
-
return foldHistory(entries.slice(0, at), prefix.layout, service);
|
|
604
|
+
return treeAt(view, historyEntries(view), opts.at ?? historyLength(view.layout), service);
|
|
590
605
|
}
|
|
591
606
|
// a cold read (no memo) trusts a checkpoint cut at exactly these log facts without re-hashing the logs
|
|
592
607
|
const facts = opts.until === undefined ? `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}` : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.150",
|
|
4
4
|
"description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
package/src/changeset.ts
CHANGED
|
@@ -57,7 +57,7 @@ import { worldPaths } from './storage.ts';
|
|
|
57
57
|
// the control root that holds it) and never learns what a world is. `volter-world` supplies
|
|
58
58
|
// discovery and the CLI verbs; the state-level logic lives here, beside the ledgers it reads.
|
|
59
59
|
import { createHash } from 'node:crypto';
|
|
60
|
-
import { aliasesFrom, parentEntries,
|
|
60
|
+
import { aliasesFrom, parentEntries, readTreeMapsAt, wholeLog, type Entry } from './log.ts';
|
|
61
61
|
import { appendActionIfAbsent, checkPrecondition, listActions, projectedPreconditionValue, projectResources, isTwinBookkeeping } from './actions.ts';
|
|
62
62
|
import type { TwinActionRevertSpec } from './actions.ts';
|
|
63
63
|
import { touchedSubjects } from './actions.ts';
|
|
@@ -351,10 +351,12 @@ export function describeActionFields(ledgers: LedgerRef[], actions: ChangesetAct
|
|
|
351
351
|
if (!e.batch || e.batch !== entries[i - 1]?.batch) batchStart = i;
|
|
352
352
|
if (!positions.has(e.id)) positions.set(e.id, e.batch ? batchStart : i);
|
|
353
353
|
});
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
354
|
+
const mine = actions.filter(a => a.service === ledger.service && a.stateService === ledger.stateService && a.action.fields && positions.has(a.action.id));
|
|
355
|
+
// every write's pre-state from one capture of this twin's history (a capture per write read the whole log each time)
|
|
356
|
+
const trees = mine.length ? readTreeMapsAt(ledger.stateService, ledger.controlRoot, mine.map(a => positions.get(a.action.id)!)) : new Map();
|
|
357
|
+
for (const { action, service, stateService } of mine) {
|
|
358
|
+
if (!action.fields) continue;
|
|
359
|
+
const subject = trees.get(positions.get(action.id)!)?.get(`${action.subject.type}:${action.subject.id}`);
|
|
358
360
|
const fields = subject?.fields ?? {};
|
|
359
361
|
fieldChanges[fieldDiffKey(service, stateService, action.id)] = diffSubjectFields({
|
|
360
362
|
subject: action.subject, fields, fieldHashes: Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, hashFieldValue(v)])),
|
package/src/history.ts
CHANGED
|
@@ -37,6 +37,12 @@ const viewPath = (directory: string, id: string) => {
|
|
|
37
37
|
function rows(directory: string, file: Segment['file']): Entry[] {
|
|
38
38
|
return getActiveWorldStore().readLines(join(directory, file)).filter(line => line.trim()).map(line => toEntry(parseExactJson(line)));
|
|
39
39
|
}
|
|
40
|
+
/** The layout of a view's first `at` entries: historyPrefix's, without the segment digests a prefix view carries (a
|
|
41
|
+
* fold reads only the layout, and a partial segment's digest re-reads and hashes its log). */
|
|
42
|
+
export function historyLayoutAt(layout: HistoryLayout, at: number): HistoryLayout {
|
|
43
|
+
if (!Number.isInteger(at) || at < 0 || at > historyLength(layout)) throw new Error(`Invalid history position ${at}`);
|
|
44
|
+
return sliceLayout(layout, at);
|
|
45
|
+
}
|
|
40
46
|
function sliceLayout(layout: HistoryLayout, at: number): HistoryLayout {
|
|
41
47
|
const base = layout.base ? historyLength(layout.base) : 0;
|
|
42
48
|
return { ...(layout.base ? { base: sliceLayout(layout.base, Math.min(at, base)) } : {}), parent: Math.min(Math.max(0, at - base), layout.parent), branch: Math.min(Math.max(0, at - base - layout.parent), layout.branch) };
|
package/src/log.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { parseExactJson } from './exact-json.ts';
|
|
|
23
23
|
// holds. The parent log accepts the v1 observed-event rows too (`toEntry` reads a delta event as
|
|
24
24
|
// an entry; a non-delta observation folds nothing and is kept for `listEvents`), so a World whose
|
|
25
25
|
// log was written in that form still reads.
|
|
26
|
-
import { captureHistory, historyChanges, historyEntryTime, historyEntries, historyLength, historyPrefix, inheritedHistory, inheritedHistoryKey, originHead, readHistoryView, replaceHistoryOrigin, saveHistoryView, validateHistoryOrigin, type HistoryLayout } from './history.ts';
|
|
26
|
+
import { captureHistory, historyChanges, historyEntryTime, historyEntries, historyLayoutAt, historyLength, historyPrefix, inheritedHistory, inheritedHistoryKey, originHead, readHistoryView, replaceHistoryOrigin, saveHistoryView, validateHistoryOrigin, type HistoryLayout } from './history.ts';
|
|
27
27
|
import { dirname, join } from 'node:path';
|
|
28
28
|
import { canonicalStatePath, checkParent, commitParentPin, pinParent, withAncestryLock, withHistoryLock } from './ancestry.ts';
|
|
29
29
|
import { resolveReferences } from './references.ts';
|
|
@@ -591,20 +591,35 @@ export function treeChangesSince(service: string, root: string | undefined, sinc
|
|
|
591
591
|
return { stamp, changed: treeResources(present), removed, appended: [...order.keys()].filter((k) => tree.has(k)) };
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
+
/** The tree at each of several positions of a twin's history, folded from one capture of it: what
|
|
595
|
+
* `readTreeMap(service, root, { at })` answers for each, without capturing the history again per position (a
|
|
596
|
+
* changeset's field changes read one position per write, and a capture reads the whole log). */
|
|
597
|
+
export function readTreeMapsAt(service: string, root: string | undefined, positions: Iterable<number>): Map<number, Tree> {
|
|
598
|
+
return withHistoryLock(worldPaths(service, root).dir, () => {
|
|
599
|
+
const view = captureHistory(service, root).descriptor;
|
|
600
|
+
const entries = historyEntries(view);
|
|
601
|
+
const trees = new Map<number, Tree>();
|
|
602
|
+
for (const at of positions) if (!trees.has(at)) trees.set(at, treeAt(view, entries, at, service));
|
|
603
|
+
return trees;
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** The tree after a view's first `at` entries; a position inside an observation has none. */
|
|
608
|
+
function treeAt(view: ReturnType<typeof captureHistory>['descriptor'], entries: Entry[], at: number, service: string): Tree {
|
|
609
|
+
if (at > 0 && at < entries.length && entries[at]?.batch && entries[at]?.batch === entries[at - 1]?.batch) {
|
|
610
|
+
let from = at; let to = at; const batch = entries[at]!.batch;
|
|
611
|
+
while (from > 0 && entries[from - 1]?.batch === batch) from--;
|
|
612
|
+
while (to < entries.length && entries[to]?.batch === batch) to++;
|
|
613
|
+
throw new Error(`position ${at} falls inside observation ${batch} — use ${from} or ${to}`);
|
|
614
|
+
}
|
|
615
|
+
return foldHistory(entries.slice(0, at), historyLayoutAt(view.layout, at), service);
|
|
616
|
+
}
|
|
617
|
+
|
|
594
618
|
function readTreeMapFull(service: string, root?: string, opts: { until?: string; at?: number; view?: string } = {}): Tree {
|
|
595
619
|
return withHistoryLock(worldPaths(service, root).dir, () => {
|
|
596
620
|
if (opts.at !== undefined || opts.view !== undefined) {
|
|
597
621
|
const view = opts.view ? readHistoryView(worldPaths(service, root).dir, opts.view) : captureHistory(service, root).descriptor;
|
|
598
|
-
|
|
599
|
-
const entries = historyEntries(view);
|
|
600
|
-
if (at > 0 && at < entries.length && entries[at]?.batch && entries[at]?.batch === entries[at - 1]?.batch) {
|
|
601
|
-
let from = at; let to = at; const batch = entries[at]!.batch;
|
|
602
|
-
while (from > 0 && entries[from - 1]?.batch === batch) from--;
|
|
603
|
-
while (to < entries.length && entries[to]?.batch === batch) to++;
|
|
604
|
-
throw new Error(`position ${at} falls inside observation ${batch} — use ${from} or ${to}`);
|
|
605
|
-
}
|
|
606
|
-
const prefix = historyPrefix(view, at);
|
|
607
|
-
return foldHistory(entries.slice(0, at), prefix.layout, service);
|
|
622
|
+
return treeAt(view, historyEntries(view), opts.at ?? historyLength(view.layout), service);
|
|
608
623
|
}
|
|
609
624
|
// a cold read (no memo) trusts a checkpoint cut at exactly these log facts without re-hashing the logs
|
|
610
625
|
const facts = opts.until === undefined ? `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}` : null;
|