@feltdb/core 0.11.6 → 0.11.8
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/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +290 -7
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +302 -13
- package/dist/create/server-source/crates/feltdb-wasm/test_wasm.db.lock +0 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +23 -2
- package/dist/embedded-query.d.ts.map +1 -1
- package/dist/embedded-query.js +20 -6
- package/dist/file-db.d.ts +160 -14
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +723 -245
- package/dist/file-journal.d.ts +70 -0
- package/dist/file-journal.d.ts.map +1 -0
- package/dist/file-journal.js +184 -0
- package/dist/runtime-snapshot.d.ts +76 -2
- package/dist/runtime-snapshot.d.ts.map +1 -1
- package/dist/runtime-snapshot.js +68 -7
- package/dist/stable-hash.d.ts +17 -0
- package/dist/stable-hash.d.ts.map +1 -0
- package/dist/stable-hash.js +82 -0
- package/dist/studio-app/assets/index-Ccecp6rc.js +30 -0
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/index-gR6d3Tys.js +0 -30
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export declare const JOURNAL_MAGIC = "FJ1";
|
|
2
|
+
export declare class JournalCorruptionError extends Error {
|
|
3
|
+
readonly journalPath: string;
|
|
4
|
+
readonly record: number;
|
|
5
|
+
readonly byteOffset: number;
|
|
6
|
+
readonly reason: string;
|
|
7
|
+
readonly code = "CORRUPT_DURABLE_JOURNAL";
|
|
8
|
+
constructor(journalPath: string, record: number, byteOffset: number, reason: string);
|
|
9
|
+
}
|
|
10
|
+
export interface JournalRecord<Body = unknown> {
|
|
11
|
+
generation: number;
|
|
12
|
+
commit: number;
|
|
13
|
+
body: Body;
|
|
14
|
+
}
|
|
15
|
+
export interface JournalScan<Body = unknown> {
|
|
16
|
+
records: Array<JournalRecord<Body>>;
|
|
17
|
+
/** Bytes consumed by complete, valid records (relative to the scan start). */
|
|
18
|
+
consumed: number;
|
|
19
|
+
/** Unterminated bytes after the last complete record: an incomplete append. */
|
|
20
|
+
tornBytes: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function digest(bytes: Buffer): string;
|
|
23
|
+
/** Frame one commit. The body is serialized once; its bytes are what the digest covers. */
|
|
24
|
+
export declare function encodeRecord(generation: number, commit: number, body: unknown): {
|
|
25
|
+
header: Buffer;
|
|
26
|
+
body: Buffer;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Validate the complete records in `bytes`.
|
|
30
|
+
*
|
|
31
|
+
* `baseOffset` and `firstRecord` only locate failures in the whole file.
|
|
32
|
+
* `expectedCommit` is the commit the first record must carry; every later
|
|
33
|
+
* record must follow it without a gap.
|
|
34
|
+
*/
|
|
35
|
+
export declare function scanJournal<Body = unknown>(bytes: Buffer, options: {
|
|
36
|
+
journalPath: string;
|
|
37
|
+
generation: number;
|
|
38
|
+
expectedCommit: number;
|
|
39
|
+
baseOffset?: number;
|
|
40
|
+
firstRecord?: number;
|
|
41
|
+
}): JournalScan<Body>;
|
|
42
|
+
export type PersistencePhase = 'mutation' | 'serialization' | 'write' | 'fsync' | 'commit' | 'compaction' | 'recovery';
|
|
43
|
+
export declare const PERSISTENCE_PHASES: readonly PersistencePhase[];
|
|
44
|
+
export interface PhaseSummary {
|
|
45
|
+
count: number;
|
|
46
|
+
totalMs: number;
|
|
47
|
+
p50: number;
|
|
48
|
+
p95: number;
|
|
49
|
+
p99: number;
|
|
50
|
+
max: number;
|
|
51
|
+
}
|
|
52
|
+
export interface PersistenceProfile {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
phases: Record<PersistencePhase, PhaseSummary>;
|
|
55
|
+
/** Bytes written by journal appends and by snapshot writes, respectively. */
|
|
56
|
+
journalBytesWritten: number;
|
|
57
|
+
snapshotBytesWritten: number;
|
|
58
|
+
}
|
|
59
|
+
export declare class PersistenceProfiler {
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
private samples;
|
|
62
|
+
private journalBytes;
|
|
63
|
+
private snapshotBytes;
|
|
64
|
+
constructor(enabled: boolean);
|
|
65
|
+
now(): number;
|
|
66
|
+
record(phase: PersistencePhase, startedAt: number): void;
|
|
67
|
+
bytes(kind: 'journal' | 'snapshot', count: number): void;
|
|
68
|
+
summary(reset?: boolean): PersistenceProfile;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=file-journal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-journal.d.ts","sourceRoot":"","sources":["../src/file-journal.ts"],"names":[],"mappings":"AA8BA,eAAO,MAAM,aAAa,QAAQ,CAAC;AAGnC,qBAAa,sBAAuB,SAAQ,KAAK;IAI7C,QAAQ,CAAC,WAAW,EAAE,MAAM;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM;IANzB,QAAQ,CAAC,IAAI,6BAA6B;gBAG/B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM;CAQ1B;AAED,MAAM,WAAW,aAAa,CAAC,IAAI,GAAG,OAAO;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,WAAW,WAAW,CAAC,IAAI,GAAG,OAAO;IACzC,OAAO,EAAE,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAIhH;AAID;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,GAAG,OAAO,EACxC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACtH,WAAW,CAAC,IAAI,CAAC,CAiDnB;AAOD,MAAM,MAAM,gBAAgB,GACxB,UAAU,GACV,eAAe,GACf,OAAO,GACP,OAAO,GACP,QAAQ,GACR,YAAY,GACZ,UAAU,CAAC;AAEf,eAAO,MAAM,kBAAkB,EAAE,SAAS,gBAAgB,EAEzD,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAAC;IAC/C,6EAA6E;IAC7E,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AASD,qBAAa,mBAAmB;IAKX,OAAO,EAAE,OAAO;IAJnC,OAAO,CAAC,OAAO,CAAyC;IACxD,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,aAAa,CAAK;gBAEP,OAAO,EAAE,OAAO;IAEnC,GAAG,IAAI,MAAM;IAIb,MAAM,CAAC,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAOxD,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAMxD,OAAO,CAAC,KAAK,UAAQ,GAAG,kBAAkB;CA0B3C"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only commit journal for the local file runtime.
|
|
3
|
+
*
|
|
4
|
+
* Private to `FileJsDb`. The logical FeltDB state is unchanged; this module
|
|
5
|
+
* only decides how a commit reaches the disk, so that appending one record
|
|
6
|
+
* costs the size of that record rather than the size of all history.
|
|
7
|
+
*
|
|
8
|
+
* Physical layout of a runtime directory:
|
|
9
|
+
*
|
|
10
|
+
* state.json snapshot: complete state as of commit C, generation G
|
|
11
|
+
* state.G.journal commits C+1, C+2, ... written after that snapshot
|
|
12
|
+
* state.json.backup the previous snapshot (generation G-1)
|
|
13
|
+
* state.(G-1).journal its journal, kept so the backup stays complete
|
|
14
|
+
*
|
|
15
|
+
* One line is one commit, however many mutations it carries:
|
|
16
|
+
*
|
|
17
|
+
* FJ1 <generation> <commit> <body-bytes> sha256:<hex> <body-json>\n
|
|
18
|
+
*
|
|
19
|
+
* The digest covers the body bytes exactly as written; the terminating newline
|
|
20
|
+
* is the commit boundary. The durability doctrine is the one the native
|
|
21
|
+
* durable log already follows (docs/architecture/durable-corruption.md):
|
|
22
|
+
*
|
|
23
|
+
* - a final record with no terminator is a torn append: it was never
|
|
24
|
+
* acknowledged, it is not replayed, and only a writer holding the lock
|
|
25
|
+
* removes it;
|
|
26
|
+
* - any terminated record that fails framing, digest, generation or commit
|
|
27
|
+
* contiguity is corruption, wherever it sits, and the open is refused.
|
|
28
|
+
*/
|
|
29
|
+
import { createHash } from 'crypto';
|
|
30
|
+
export const JOURNAL_MAGIC = 'FJ1';
|
|
31
|
+
const NEWLINE = 0x0a;
|
|
32
|
+
export class JournalCorruptionError extends Error {
|
|
33
|
+
journalPath;
|
|
34
|
+
record;
|
|
35
|
+
byteOffset;
|
|
36
|
+
reason;
|
|
37
|
+
code = 'CORRUPT_DURABLE_JOURNAL';
|
|
38
|
+
constructor(journalPath, record, byteOffset, reason) {
|
|
39
|
+
super(`CORRUPT_DURABLE_JOURNAL: journal ${journalPath} record ${record} at byte ${byteOffset} `
|
|
40
|
+
+ `is corrupt: ${reason}. The database was not opened and was not modified.`);
|
|
41
|
+
this.journalPath = journalPath;
|
|
42
|
+
this.record = record;
|
|
43
|
+
this.byteOffset = byteOffset;
|
|
44
|
+
this.reason = reason;
|
|
45
|
+
this.name = 'JournalCorruptionError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function digest(bytes) {
|
|
49
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
50
|
+
}
|
|
51
|
+
/** Frame one commit. The body is serialized once; its bytes are what the digest covers. */
|
|
52
|
+
export function encodeRecord(generation, commit, body) {
|
|
53
|
+
const bodyBytes = Buffer.from(JSON.stringify(body), 'utf8');
|
|
54
|
+
const header = Buffer.from(`${JOURNAL_MAGIC} ${generation} ${commit} ${bodyBytes.length} ${digest(bodyBytes)} `, 'utf8');
|
|
55
|
+
return { header, body: bodyBytes };
|
|
56
|
+
}
|
|
57
|
+
const HEADER = /^FJ1 (\d+) (\d+) (\d+) (sha256:[0-9a-f]{64}) $/;
|
|
58
|
+
/**
|
|
59
|
+
* Validate the complete records in `bytes`.
|
|
60
|
+
*
|
|
61
|
+
* `baseOffset` and `firstRecord` only locate failures in the whole file.
|
|
62
|
+
* `expectedCommit` is the commit the first record must carry; every later
|
|
63
|
+
* record must follow it without a gap.
|
|
64
|
+
*/
|
|
65
|
+
export function scanJournal(bytes, options) {
|
|
66
|
+
const records = [];
|
|
67
|
+
const baseOffset = options.baseOffset ?? 0;
|
|
68
|
+
let position = 0;
|
|
69
|
+
let expected = options.expectedCommit;
|
|
70
|
+
let recordNumber = options.firstRecord ?? 1;
|
|
71
|
+
while (position < bytes.length) {
|
|
72
|
+
const end = bytes.indexOf(NEWLINE, position);
|
|
73
|
+
if (end === -1)
|
|
74
|
+
break;
|
|
75
|
+
const fail = (reason) => {
|
|
76
|
+
throw new JournalCorruptionError(options.journalPath, recordNumber, baseOffset + position, reason);
|
|
77
|
+
};
|
|
78
|
+
const line = bytes.subarray(position, end);
|
|
79
|
+
// The header is ASCII and ends at the fifth space; the body follows it.
|
|
80
|
+
let spaces = 0;
|
|
81
|
+
let headerEnd = -1;
|
|
82
|
+
for (let index = 0; index < line.length && index < 160; index += 1) {
|
|
83
|
+
if (line[index] === 0x20 && ++spaces === 5) {
|
|
84
|
+
headerEnd = index + 1;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (headerEnd === -1)
|
|
89
|
+
fail('record framing header is missing or malformed');
|
|
90
|
+
const match = HEADER.exec(line.subarray(0, headerEnd).toString('latin1'));
|
|
91
|
+
if (!match)
|
|
92
|
+
fail('record framing header is missing or malformed');
|
|
93
|
+
const [, generationText, commitText, lengthText, expectedDigest] = match;
|
|
94
|
+
const generation = Number(generationText);
|
|
95
|
+
const commit = Number(commitText);
|
|
96
|
+
const bodyBytes = line.subarray(headerEnd);
|
|
97
|
+
if (bodyBytes.length !== Number(lengthText)) {
|
|
98
|
+
fail(`record declares ${lengthText} body bytes but carries ${bodyBytes.length}`);
|
|
99
|
+
}
|
|
100
|
+
if (digest(bodyBytes) !== expectedDigest)
|
|
101
|
+
fail('record digest does not match its body');
|
|
102
|
+
if (generation !== options.generation) {
|
|
103
|
+
fail(`record belongs to generation ${generation}, journal is generation ${options.generation}`);
|
|
104
|
+
}
|
|
105
|
+
if (commit !== expected)
|
|
106
|
+
fail(`record carries commit ${commit} where commit ${expected} was required`);
|
|
107
|
+
let body;
|
|
108
|
+
try {
|
|
109
|
+
body = JSON.parse(bodyBytes.toString('utf8'));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return fail('record body is not valid JSON');
|
|
113
|
+
}
|
|
114
|
+
records.push({ generation, commit, body });
|
|
115
|
+
expected += 1;
|
|
116
|
+
recordNumber += 1;
|
|
117
|
+
position = end + 1;
|
|
118
|
+
}
|
|
119
|
+
return { records, consumed: position, tornBytes: bytes.length - position };
|
|
120
|
+
}
|
|
121
|
+
export const PERSISTENCE_PHASES = [
|
|
122
|
+
'mutation', 'serialization', 'write', 'fsync', 'commit', 'compaction', 'recovery',
|
|
123
|
+
];
|
|
124
|
+
const MAX_SAMPLES = 100_000;
|
|
125
|
+
function percentile(sorted, fraction) {
|
|
126
|
+
if (sorted.length === 0)
|
|
127
|
+
return 0;
|
|
128
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(fraction * sorted.length) - 1))];
|
|
129
|
+
}
|
|
130
|
+
export class PersistenceProfiler {
|
|
131
|
+
enabled;
|
|
132
|
+
samples = new Map();
|
|
133
|
+
journalBytes = 0;
|
|
134
|
+
snapshotBytes = 0;
|
|
135
|
+
constructor(enabled) {
|
|
136
|
+
this.enabled = enabled;
|
|
137
|
+
}
|
|
138
|
+
now() {
|
|
139
|
+
return this.enabled ? performance.now() : 0;
|
|
140
|
+
}
|
|
141
|
+
record(phase, startedAt) {
|
|
142
|
+
if (!this.enabled)
|
|
143
|
+
return;
|
|
144
|
+
let list = this.samples.get(phase);
|
|
145
|
+
if (!list)
|
|
146
|
+
this.samples.set(phase, list = []);
|
|
147
|
+
if (list.length < MAX_SAMPLES)
|
|
148
|
+
list.push(performance.now() - startedAt);
|
|
149
|
+
}
|
|
150
|
+
bytes(kind, count) {
|
|
151
|
+
if (!this.enabled)
|
|
152
|
+
return;
|
|
153
|
+
if (kind === 'journal')
|
|
154
|
+
this.journalBytes += count;
|
|
155
|
+
else
|
|
156
|
+
this.snapshotBytes += count;
|
|
157
|
+
}
|
|
158
|
+
summary(reset = false) {
|
|
159
|
+
const phases = {};
|
|
160
|
+
for (const phase of PERSISTENCE_PHASES) {
|
|
161
|
+
const values = [...(this.samples.get(phase) ?? [])].sort((left, right) => left - right);
|
|
162
|
+
phases[phase] = {
|
|
163
|
+
count: values.length,
|
|
164
|
+
totalMs: values.reduce((sum, value) => sum + value, 0),
|
|
165
|
+
p50: percentile(values, 0.5),
|
|
166
|
+
p95: percentile(values, 0.95),
|
|
167
|
+
p99: percentile(values, 0.99),
|
|
168
|
+
max: values.length ? values[values.length - 1] : 0,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const profile = {
|
|
172
|
+
enabled: this.enabled,
|
|
173
|
+
phases,
|
|
174
|
+
journalBytesWritten: this.journalBytes,
|
|
175
|
+
snapshotBytesWritten: this.snapshotBytes,
|
|
176
|
+
};
|
|
177
|
+
if (reset) {
|
|
178
|
+
this.samples.clear();
|
|
179
|
+
this.journalBytes = 0;
|
|
180
|
+
this.snapshotBytes = 0;
|
|
181
|
+
}
|
|
182
|
+
return profile;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -132,6 +132,44 @@ export interface SnapshotStaleness {
|
|
|
132
132
|
/** Why currency could not be decided. */
|
|
133
133
|
reason?: string;
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Which authoritative state a published snapshot represents, stated explicitly.
|
|
137
|
+
*
|
|
138
|
+
* Two snapshots with the same `id` represent the same durable state, derived
|
|
139
|
+
* by the same definition, under the same authority. The identity is computed
|
|
140
|
+
* from what the snapshot already is — its revision, definition, and authority
|
|
141
|
+
* — rather than stored as parallel metadata that could disagree with it.
|
|
142
|
+
*/
|
|
143
|
+
export interface SnapshotIdentity {
|
|
144
|
+
/** `snap_` + SHA-256 over the fields below. Deterministic across handles and processes. */
|
|
145
|
+
id: string;
|
|
146
|
+
name: string;
|
|
147
|
+
/**
|
|
148
|
+
* The authoritative revision value, when the runtime has one. `null` when it
|
|
149
|
+
* does not, in which case `contentDigest` identifies the state instead.
|
|
150
|
+
*/
|
|
151
|
+
version: number | null;
|
|
152
|
+
/** The revision's consistency domain, when there is a revision. */
|
|
153
|
+
scope?: string;
|
|
154
|
+
/** Publication counter of the handle. Informational; not part of `id`. */
|
|
155
|
+
generation: number;
|
|
156
|
+
/** The source collections, sorted. */
|
|
157
|
+
sourceSet: readonly string[];
|
|
158
|
+
/** SHA-256 of the definition's sources, filters, and indexes. */
|
|
159
|
+
definitionDigest: string;
|
|
160
|
+
/** The authority the materialization read under (`tenant/…`, `platform`, or `unscoped`). */
|
|
161
|
+
authorityContext: string;
|
|
162
|
+
/**
|
|
163
|
+
* SHA-256 of the canonical materialized records. Present only when no
|
|
164
|
+
* authoritative revision exists, because then the content is the only honest
|
|
165
|
+
* identity there is.
|
|
166
|
+
*/
|
|
167
|
+
contentDigest?: string;
|
|
168
|
+
/** How `id` was derived. */
|
|
169
|
+
derivation: 'revision' | 'content';
|
|
170
|
+
/** When materialization completed. Reporting only; not part of `id`. */
|
|
171
|
+
createdAt: number;
|
|
172
|
+
}
|
|
135
173
|
export interface SnapshotIndexDescription {
|
|
136
174
|
collection: string;
|
|
137
175
|
name: string;
|
|
@@ -175,6 +213,14 @@ export interface SnapshotHandleExplanation extends SnapshotExplanation {
|
|
|
175
213
|
* claim.
|
|
176
214
|
*/
|
|
177
215
|
durableReads: number;
|
|
216
|
+
/**
|
|
217
|
+
* `refresh()` calls answered by the fast path: the active snapshot already
|
|
218
|
+
* represented the current authoritative revision, so nothing was read or
|
|
219
|
+
* rebuilt.
|
|
220
|
+
*/
|
|
221
|
+
reused: number;
|
|
222
|
+
/** The authority the handle materializes under. */
|
|
223
|
+
authorityContext: string;
|
|
178
224
|
}
|
|
179
225
|
export declare class SnapshotDefinitionError extends Error {
|
|
180
226
|
constructor(message: string);
|
|
@@ -209,6 +255,11 @@ export interface RuntimeSnapshot<D = undefined> {
|
|
|
209
255
|
/** Materialized records, across every source. */
|
|
210
256
|
readonly records: number;
|
|
211
257
|
readonly collections: readonly string[];
|
|
258
|
+
/**
|
|
259
|
+
* Which authoritative state this snapshot represents. Computed on first
|
|
260
|
+
* access and memoized, so the hot read path never pays for it.
|
|
261
|
+
*/
|
|
262
|
+
readonly identity: SnapshotIdentity;
|
|
212
263
|
get<T = Record<string, unknown>>(collection: string, id: string | number): T | undefined;
|
|
213
264
|
has(collection: string, id: string | number): boolean;
|
|
214
265
|
/** Every materialized record of one collection, in materialization order. Frozen. */
|
|
@@ -231,6 +282,17 @@ export interface RuntimeSnapshot<D = undefined> {
|
|
|
231
282
|
export interface SnapshotSourceRuntime {
|
|
232
283
|
/** Read every authoritative record of one collection. */
|
|
233
284
|
readRecords(collection: string): Promise<Array<Record<string, unknown>>>;
|
|
285
|
+
/**
|
|
286
|
+
* Read only the authoritative records that satisfy `where`, through the
|
|
287
|
+
* runtime's own bounded query (and its indexes, where it has them).
|
|
288
|
+
*
|
|
289
|
+
* Optional. When present it is the bounded source-selection fast path: a
|
|
290
|
+
* snapshot that selects ten records of a large collection reads ten records,
|
|
291
|
+
* not the collection. It must return exactly the records `readRecords` would
|
|
292
|
+
* after filtering by the same conditions — the build re-applies the filter
|
|
293
|
+
* regardless, so a runtime that over-returns cannot widen a snapshot.
|
|
294
|
+
*/
|
|
295
|
+
readSelected?(collection: string, where: SnapshotCondition[]): Promise<Array<Record<string, unknown>>>;
|
|
234
296
|
/** What freshness validation the underlying runtime can honestly support. */
|
|
235
297
|
freshness(): Promise<FreshnessCapability>;
|
|
236
298
|
/** The runtime's current revision. Called only when freshness advertises one. */
|
|
@@ -254,7 +316,10 @@ export declare class RuntimeSnapshotHandle<D = undefined> {
|
|
|
254
316
|
private durableReads;
|
|
255
317
|
private lastFailure?;
|
|
256
318
|
private closed;
|
|
257
|
-
|
|
319
|
+
private reused;
|
|
320
|
+
private readonly definitionDigest;
|
|
321
|
+
readonly authorityContext: string;
|
|
322
|
+
constructor(definition: SnapshotDefinition<D>, source: SnapshotSourceRuntime, options?: RuntimeSnapshotOptions);
|
|
258
323
|
get name(): string;
|
|
259
324
|
/**
|
|
260
325
|
* The active snapshot, or `undefined` before the first successful build.
|
|
@@ -325,7 +390,16 @@ export declare function assertSnapshotDefinition(definition: SnapshotDefinition<
|
|
|
325
390
|
* `StateFirstDB.snapshot()` is the application-facing form; this is the same
|
|
326
391
|
* primitive for runtimes and tools that hold a source of their own.
|
|
327
392
|
*/
|
|
328
|
-
export declare function createRuntimeSnapshot<D = undefined>(definition: SnapshotDefinition<D>, source: SnapshotSourceRuntime): RuntimeSnapshotHandle<D>;
|
|
393
|
+
export declare function createRuntimeSnapshot<D = undefined>(definition: SnapshotDefinition<D>, source: SnapshotSourceRuntime, options?: RuntimeSnapshotOptions): RuntimeSnapshotHandle<D>;
|
|
394
|
+
export interface RuntimeSnapshotOptions {
|
|
395
|
+
/**
|
|
396
|
+
* The authority the source reads under. `db.snapshot()` supplies the
|
|
397
|
+
* database's own authority scope; the source it hands the handle is already
|
|
398
|
+
* bound to that scope, so a snapshot can never read more than its database
|
|
399
|
+
* could. Recorded in every snapshot's identity.
|
|
400
|
+
*/
|
|
401
|
+
authorityContext?: string;
|
|
402
|
+
}
|
|
329
403
|
/**
|
|
330
404
|
* Whether two definitions describe the same snapshot.
|
|
331
405
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-snapshot.d.ts","sourceRoot":"","sources":["../src/runtime-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,QAAQ,EACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAyC,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC3G,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime-snapshot.d.ts","sourceRoot":"","sources":["../src/runtime-snapshot.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime-snapshot.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { isCacheCurrent, } from './freshness.js';
|
|
32
32
|
import { conditionMatches, EmbeddedQueryEngine } from './embedded-query.js';
|
|
33
|
+
import { canonicalJson, sha256Hex } from './stable-hash.js';
|
|
33
34
|
export class SnapshotDefinitionError extends Error {
|
|
34
35
|
constructor(message) { super(message); this.name = 'SnapshotDefinitionError'; }
|
|
35
36
|
}
|
|
@@ -56,17 +57,57 @@ class PublishedSnapshot {
|
|
|
56
57
|
generation;
|
|
57
58
|
basis;
|
|
58
59
|
materialization;
|
|
60
|
+
definitionDigest;
|
|
61
|
+
authorityContext;
|
|
59
62
|
collections;
|
|
60
63
|
records;
|
|
61
|
-
|
|
64
|
+
// Memo cell, not snapshot state: identity is a pure function of what is
|
|
65
|
+
// already frozen, computed once on first access.
|
|
66
|
+
memo = {};
|
|
67
|
+
constructor(name, generation, basis, materialization, definitionDigest, authorityContext) {
|
|
62
68
|
this.name = name;
|
|
63
69
|
this.generation = generation;
|
|
64
70
|
this.basis = basis;
|
|
65
71
|
this.materialization = materialization;
|
|
72
|
+
this.definitionDigest = definitionDigest;
|
|
73
|
+
this.authorityContext = authorityContext;
|
|
66
74
|
this.collections = Object.freeze([...materialization.byCollection.keys()]);
|
|
67
75
|
this.records = materialization.records.size;
|
|
68
76
|
Object.freeze(this);
|
|
69
77
|
}
|
|
78
|
+
get identity() {
|
|
79
|
+
if (this.memo.identity)
|
|
80
|
+
return this.memo.identity;
|
|
81
|
+
const sourceSet = Object.freeze([...this.collections].sort());
|
|
82
|
+
const revision = this.basis.validation === 'revision' ? this.basis.revision : undefined;
|
|
83
|
+
const contentDigest = revision ? undefined : sha256Hex(canonicalJson(sourceSet.map(collection => [
|
|
84
|
+
collection,
|
|
85
|
+
[...(this.materialization.byCollection.get(collection) ?? EMPTY)]
|
|
86
|
+
.map(record => canonicalJson(record))
|
|
87
|
+
.sort(),
|
|
88
|
+
])));
|
|
89
|
+
const id = `snap_${sha256Hex(canonicalJson([
|
|
90
|
+
'feltdb.snapshot.v1',
|
|
91
|
+
this.name,
|
|
92
|
+
this.definitionDigest,
|
|
93
|
+
this.authorityContext,
|
|
94
|
+
revision ? ['revision', revision.scope, revision.value] : ['content', contentDigest],
|
|
95
|
+
]))}`;
|
|
96
|
+
this.memo.identity = Object.freeze({
|
|
97
|
+
id,
|
|
98
|
+
name: this.name,
|
|
99
|
+
version: revision ? revision.value : null,
|
|
100
|
+
...(revision ? { scope: revision.scope } : {}),
|
|
101
|
+
generation: this.generation,
|
|
102
|
+
sourceSet,
|
|
103
|
+
definitionDigest: this.definitionDigest,
|
|
104
|
+
authorityContext: this.authorityContext,
|
|
105
|
+
...(contentDigest ? { contentDigest } : {}),
|
|
106
|
+
derivation: revision ? 'revision' : 'content',
|
|
107
|
+
createdAt: this.basis.materializedAt,
|
|
108
|
+
});
|
|
109
|
+
return this.memo.identity;
|
|
110
|
+
}
|
|
70
111
|
get revision() { return this.basis.revision; }
|
|
71
112
|
get materializedAt() { return this.basis.materializedAt; }
|
|
72
113
|
get(collection, id) {
|
|
@@ -129,10 +170,15 @@ export class RuntimeSnapshotHandle {
|
|
|
129
170
|
durableReads = 0;
|
|
130
171
|
lastFailure;
|
|
131
172
|
closed = false;
|
|
132
|
-
|
|
173
|
+
reused = 0;
|
|
174
|
+
definitionDigest;
|
|
175
|
+
authorityContext;
|
|
176
|
+
constructor(definition, source, options = {}) {
|
|
133
177
|
this.definition = definition;
|
|
134
178
|
this.source = source;
|
|
135
179
|
assertSnapshotDefinition(definition);
|
|
180
|
+
this.definitionDigest = sha256Hex(definitionShape(definition));
|
|
181
|
+
this.authorityContext = options.authorityContext ?? 'unscoped';
|
|
136
182
|
}
|
|
137
183
|
get name() { return this.definition.name; }
|
|
138
184
|
/**
|
|
@@ -162,8 +208,10 @@ export class RuntimeSnapshotHandle {
|
|
|
162
208
|
this.assertOpen();
|
|
163
209
|
if (!options.force && this.active) {
|
|
164
210
|
const staleness = await this.staleness();
|
|
165
|
-
if (staleness.state === 'current')
|
|
211
|
+
if (staleness.state === 'current') {
|
|
212
|
+
this.reused += 1;
|
|
166
213
|
return this.active;
|
|
214
|
+
}
|
|
167
215
|
}
|
|
168
216
|
return this.build();
|
|
169
217
|
}
|
|
@@ -240,6 +288,8 @@ export class RuntimeSnapshotHandle {
|
|
|
240
288
|
failures: this.failures,
|
|
241
289
|
...(this.lastFailure ? { lastFailure: this.lastFailure } : {}),
|
|
242
290
|
durableReads: this.durableReads,
|
|
291
|
+
reused: this.reused,
|
|
292
|
+
authorityContext: this.authorityContext,
|
|
243
293
|
};
|
|
244
294
|
}
|
|
245
295
|
/**
|
|
@@ -322,7 +372,9 @@ export class RuntimeSnapshotHandle {
|
|
|
322
372
|
const indexes = [];
|
|
323
373
|
for (const source of this.definition.sources) {
|
|
324
374
|
this.durableReads += 1;
|
|
325
|
-
const durable =
|
|
375
|
+
const durable = source.where?.length && this.source.readSelected
|
|
376
|
+
? await this.source.readSelected(source.collection, source.where)
|
|
377
|
+
: await this.source.readRecords(source.collection);
|
|
326
378
|
const kept = [];
|
|
327
379
|
for (const record of durable) {
|
|
328
380
|
if (!record || typeof record !== 'object' || Array.isArray(record))
|
|
@@ -389,7 +441,7 @@ export class RuntimeSnapshotHandle {
|
|
|
389
441
|
* visible.
|
|
390
442
|
*/
|
|
391
443
|
publish(materialization, basis) {
|
|
392
|
-
const candidate = new PublishedSnapshot(this.name, this.generation + 1, Object.freeze(basis), materialization);
|
|
444
|
+
const candidate = new PublishedSnapshot(this.name, this.generation + 1, Object.freeze(basis), materialization, this.definitionDigest, this.authorityContext);
|
|
393
445
|
this.definition.validate?.(candidate);
|
|
394
446
|
this.active = candidate;
|
|
395
447
|
this.generation = candidate.generation;
|
|
@@ -455,8 +507,17 @@ export function assertSnapshotDefinition(definition) {
|
|
|
455
507
|
* `StateFirstDB.snapshot()` is the application-facing form; this is the same
|
|
456
508
|
* primitive for runtimes and tools that hold a source of their own.
|
|
457
509
|
*/
|
|
458
|
-
export function createRuntimeSnapshot(definition, source) {
|
|
459
|
-
return new RuntimeSnapshotHandle(definition, source);
|
|
510
|
+
export function createRuntimeSnapshot(definition, source, options = {}) {
|
|
511
|
+
return new RuntimeSnapshotHandle(definition, source, options);
|
|
512
|
+
}
|
|
513
|
+
/** The identity-bearing shape of a definition: sources, filters, and indexes. */
|
|
514
|
+
function definitionShape(definition) {
|
|
515
|
+
return canonicalJson({
|
|
516
|
+
name: definition.name,
|
|
517
|
+
sources: [...definition.sources]
|
|
518
|
+
.map(source => ({ collection: source.collection, where: source.where ?? [], indexes: source.indexes ?? [] }))
|
|
519
|
+
.sort((left, right) => (left.collection < right.collection ? -1 : left.collection > right.collection ? 1 : 0)),
|
|
520
|
+
});
|
|
460
521
|
}
|
|
461
522
|
/**
|
|
462
523
|
* Whether two definitions describe the same snapshot.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous, dependency-free SHA-256 and canonical JSON.
|
|
3
|
+
*
|
|
4
|
+
* Snapshot identity must be computable in every runtime `@feltdb/core` ships
|
|
5
|
+
* to — Node, browsers, workers — and synchronously, because a published
|
|
6
|
+
* snapshot answers without awaiting anything. WebCrypto is asynchronous and
|
|
7
|
+
* `node:crypto` is not available in a browser, so this is the small portable
|
|
8
|
+
* implementation both need. It is used for identity, not for security.
|
|
9
|
+
*/
|
|
10
|
+
/** SHA-256 of a UTF-8 string, as lowercase hex. */
|
|
11
|
+
export declare function sha256Hex(input: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* JSON with object keys sorted at every depth, so two structurally equal
|
|
14
|
+
* values serialize to the same bytes regardless of insertion order.
|
|
15
|
+
*/
|
|
16
|
+
export declare function canonicalJson(value: unknown): string;
|
|
17
|
+
//# sourceMappingURL=stable-hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stable-hash.d.ts","sourceRoot":"","sources":["../src/stable-hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA+B/C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUpD"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous, dependency-free SHA-256 and canonical JSON.
|
|
3
|
+
*
|
|
4
|
+
* Snapshot identity must be computable in every runtime `@feltdb/core` ships
|
|
5
|
+
* to — Node, browsers, workers — and synchronously, because a published
|
|
6
|
+
* snapshot answers without awaiting anything. WebCrypto is asynchronous and
|
|
7
|
+
* `node:crypto` is not available in a browser, so this is the small portable
|
|
8
|
+
* implementation both need. It is used for identity, not for security.
|
|
9
|
+
*/
|
|
10
|
+
const K = new Uint32Array([
|
|
11
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
12
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
13
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
14
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
15
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
16
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
17
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
18
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
19
|
+
]);
|
|
20
|
+
/** SHA-256 of a UTF-8 string, as lowercase hex. */
|
|
21
|
+
export function sha256Hex(input) {
|
|
22
|
+
const bytes = new TextEncoder().encode(input);
|
|
23
|
+
const bitLength = bytes.length * 8;
|
|
24
|
+
const padded = new Uint8Array(((bytes.length + 9 + 63) >> 6) << 6);
|
|
25
|
+
padded.set(bytes);
|
|
26
|
+
padded[bytes.length] = 0x80;
|
|
27
|
+
const view = new DataView(padded.buffer);
|
|
28
|
+
view.setUint32(padded.length - 8, Math.floor(bitLength / 0x100000000));
|
|
29
|
+
view.setUint32(padded.length - 4, bitLength >>> 0);
|
|
30
|
+
const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
31
|
+
const w = new Uint32Array(64);
|
|
32
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
33
|
+
for (let i = 0; i < 16; i += 1)
|
|
34
|
+
w[i] = view.getUint32(offset + i * 4);
|
|
35
|
+
for (let i = 16; i < 64; i += 1) {
|
|
36
|
+
const a = w[i - 15], b = w[i - 2];
|
|
37
|
+
const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
|
|
38
|
+
const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
|
|
39
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
40
|
+
}
|
|
41
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
42
|
+
for (let i = 0; i < 64; i += 1) {
|
|
43
|
+
const s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
|
|
44
|
+
const t1 = (hh + s1 + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
|
|
45
|
+
const s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
|
|
46
|
+
const t2 = (s0 + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
|
|
47
|
+
hh = g;
|
|
48
|
+
g = f;
|
|
49
|
+
f = e;
|
|
50
|
+
e = (d + t1) >>> 0;
|
|
51
|
+
d = c;
|
|
52
|
+
c = b;
|
|
53
|
+
b = a;
|
|
54
|
+
a = (t1 + t2) >>> 0;
|
|
55
|
+
}
|
|
56
|
+
h[0] += a;
|
|
57
|
+
h[1] += b;
|
|
58
|
+
h[2] += c;
|
|
59
|
+
h[3] += d;
|
|
60
|
+
h[4] += e;
|
|
61
|
+
h[5] += f;
|
|
62
|
+
h[6] += g;
|
|
63
|
+
h[7] += hh;
|
|
64
|
+
}
|
|
65
|
+
return Array.from(h, word => word.toString(16).padStart(8, '0')).join('');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* JSON with object keys sorted at every depth, so two structurally equal
|
|
69
|
+
* values serialize to the same bytes regardless of insertion order.
|
|
70
|
+
*/
|
|
71
|
+
export function canonicalJson(value) {
|
|
72
|
+
if (value === null || typeof value !== 'object') {
|
|
73
|
+
return value === undefined ? 'null' : JSON.stringify(value);
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(value))
|
|
76
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
77
|
+
const entries = Object.keys(value)
|
|
78
|
+
.filter(key => value[key] !== undefined)
|
|
79
|
+
.sort()
|
|
80
|
+
.map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`);
|
|
81
|
+
return `{${entries.join(',')}}`;
|
|
82
|
+
}
|