@nimbus-sh/platform 0.8.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composition.d.ts +3 -2
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +34 -19
- package/dist/crc32.d.ts +5 -5
- package/dist/crc32.d.ts.map +1 -1
- package/dist/crc32.js +8 -12
- package/dist/diag-counters.d.ts +18 -10
- package/dist/diag-counters.d.ts.map +1 -1
- package/dist/diag-counters.js +7 -20
- package/dist/heap-estimate.d.ts +5 -20
- package/dist/heap-estimate.d.ts.map +1 -1
- package/dist/heap-estimate.js +6 -33
- package/dist/heavy-alloc-coord.d.ts +131 -11
- package/dist/heavy-alloc-coord.d.ts.map +1 -1
- package/dist/heavy-alloc-coord.js +241 -22
- package/dist/hex.d.ts +20 -0
- package/dist/hex.d.ts.map +1 -0
- package/dist/hex.js +29 -0
- package/dist/install-phase.d.ts +1 -15
- package/dist/install-phase.d.ts.map +1 -1
- package/dist/lexical-path.d.ts +11 -0
- package/dist/lexical-path.d.ts.map +1 -0
- package/dist/lexical-path.js +19 -0
- package/dist/limits.d.ts +4 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +25 -0
- package/dist/module-source.d.ts.map +1 -1
- package/dist/module-source.js +2 -1
- package/dist/oom-classify.d.ts +4 -0
- package/dist/oom-classify.d.ts.map +1 -1
- package/dist/oom-classify.js +6 -0
- package/dist/realm-builtin.d.ts +2 -0
- package/dist/realm-builtin.d.ts.map +1 -0
- package/dist/realm-builtin.js +11 -0
- package/dist/utf8.d.ts +21 -1
- package/dist/utf8.d.ts.map +1 -1
- package/dist/utf8.js +73 -1
- package/dist/w7-frame.d.ts +92 -17
- package/dist/w7-frame.d.ts.map +1 -1
- package/dist/w7-frame.js +87 -35
- package/dist/wave-writer.d.ts +11 -6
- package/dist/wave-writer.d.ts.map +1 -1
- package/dist/wave-writer.js +28 -29
- package/dist/weighted-credit-pool.d.ts +43 -0
- package/dist/weighted-credit-pool.d.ts.map +1 -1
- package/dist/weighted-credit-pool.js +250 -46
- package/dist/worker-code.d.ts +48 -0
- package/dist/worker-code.d.ts.map +1 -0
- package/dist/worker-code.js +10 -0
- package/package.json +2 -2
- package/src/composition.ts +41 -21
- package/src/crc32.ts +9 -12
- package/src/diag-counters.ts +23 -22
- package/src/heap-estimate.ts +11 -40
- package/src/heavy-alloc-coord.ts +275 -20
- package/src/hex.ts +29 -0
- package/src/install-phase.ts +1 -15
- package/src/lexical-path.ts +17 -0
- package/src/limits.ts +29 -0
- package/src/module-source.ts +3 -1
- package/src/oom-classify.ts +8 -0
- package/src/realm-builtin.ts +22 -0
- package/src/utf8.ts +75 -1
- package/src/w7-frame.ts +152 -53
- package/src/wave-writer.ts +28 -27
- package/src/weighted-credit-pool.ts +256 -51
- package/src/worker-code.ts +38 -0
package/src/w7-frame.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { crc32 } from './crc32.js';
|
|
17
17
|
import { CHUNK_SIZE } from './limits.js';
|
|
18
18
|
import { utf8Length } from './utf8.js';
|
|
19
|
+
import { rootClampedSegments } from './lexical-path.js';
|
|
19
20
|
|
|
20
21
|
// The batch payload types live WITH the wire format that encodes them: every
|
|
21
22
|
// producer and consumer of a W7 stream speaks exactly these records, and the
|
|
@@ -79,11 +80,26 @@ export interface BatchWritePayload {
|
|
|
79
80
|
ops?: W7Op[];
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* One attribute change (setattr): the mode, the owner, or the times; what
|
|
85
|
+
* chmod, chown and utimes each make. An owner side that is null keeps what
|
|
86
|
+
* the file has when the change lands (chown's -1).
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* A time utimes and futimes set, as utimensat(2) takes it: ms since the
|
|
90
|
+
* epoch (explicit: only the owner may set it), 'now' (UTIME_NOW: the
|
|
91
|
+
* clock of the filesystem that commits the change, as it commits it) or
|
|
92
|
+
* 'omit' (UTIME_OMIT: left as it is then). 'now' and 'omit' need the owner or write
|
|
93
|
+
* permission. Carried as it is to the filesystem that commits the change,
|
|
94
|
+
* which alone resolves it (posix-access.ts, timeAt) and judges it
|
|
95
|
+
* (utimesRefusal, explicitTimes).
|
|
96
|
+
*/
|
|
97
|
+
export type FsTime = number | 'now' | 'omit';
|
|
98
|
+
|
|
83
99
|
export type W7Attrs =
|
|
84
100
|
| { mode: number }
|
|
85
|
-
| { uid: number; gid: number }
|
|
86
|
-
| { atime:
|
|
101
|
+
| { uid: number | null; gid: number | null }
|
|
102
|
+
| { atime: FsTime; mtime: FsTime };
|
|
87
103
|
|
|
88
104
|
/**
|
|
89
105
|
* A process's filesystem call as a record (v4), applied by the session's own
|
|
@@ -94,6 +110,36 @@ export type W7Attrs =
|
|
|
94
110
|
* nothing makes a missing parent. The program-order counterpart of the
|
|
95
111
|
* syscall, where `file` and `directory` are a checkout's upserts.
|
|
96
112
|
*/
|
|
113
|
+
/** What a W7 open asks of its file, as open(2)'s O_ACCMODE: to read it, to write it, or both. */
|
|
114
|
+
export type W7OpenAccess = 'read' | 'write' | 'readwrite';
|
|
115
|
+
function isOpenAccess(value: unknown): value is W7OpenAccess {
|
|
116
|
+
return value === 'read' || value === 'write' || value === 'readwrite';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The rights an open's access grants its description: what a call through it may do. */
|
|
120
|
+
export function openAccessRights(access: W7OpenAccess): { read: boolean; write: boolean } {
|
|
121
|
+
return { read: access !== 'write', write: access !== 'read' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The permission bits an open asks of the file it reaches (4 read, 2 write): its access's, and write under `truncate`, as open(2)'s. */
|
|
125
|
+
export function openAccessMask(call: { access: W7OpenAccess; truncate?: true }): number {
|
|
126
|
+
const rights = openAccessRights(call.access);
|
|
127
|
+
return (rights.read ? 4 : 0) | (rights.write || call.truncate === true ? 2 : 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* What a process decided a call on from its own view of the name: the file
|
|
132
|
+
* it expected there (its number on the session's filesystem), none (null),
|
|
133
|
+
* or the file its open of description `made` reached, for a file that open
|
|
134
|
+
* made and the process has no number of yet (it keeps that description
|
|
135
|
+
* open until it has). Where the name leads elsewhere now (a stale view: a
|
|
136
|
+
* peer replaced the name, or a link there leads to another file; or that
|
|
137
|
+
* open was refused), the session refuses the call before any effect:
|
|
138
|
+
* ENOENT where it expected a file, EEXIST where it expected none, EXDEV
|
|
139
|
+
* where the name leads to a mount.
|
|
140
|
+
*/
|
|
141
|
+
export type W7Expected = number | null | { readonly made: string };
|
|
142
|
+
|
|
97
143
|
export type W7Call =
|
|
98
144
|
/**
|
|
99
145
|
* `ino`, on a call that makes a name (writeFile, appendFile, mkdir,
|
|
@@ -122,9 +168,17 @@ export type W7Call =
|
|
|
122
168
|
| { call: 'ftruncate'; path: string; ino?: number; description?: string; size: number }
|
|
123
169
|
/** `existing: 'ok'`: a directory already there answers success, as `mkdir -p` takes it (anything else there is still EEXIST). */
|
|
124
170
|
| { call: 'mkdir'; path: string; mode: number; ino?: number; umask?: number; existing?: 'ok' }
|
|
125
|
-
| { call: 'unlink'; path: string }
|
|
126
|
-
| { call: 'rmdir'; path: string }
|
|
171
|
+
| { call: 'unlink'; path: string; expect?: W7Expected }
|
|
172
|
+
| { call: 'rmdir'; path: string; expect?: W7Expected }
|
|
127
173
|
| { call: 'symlink'; path: string; target: string; ino?: number }
|
|
174
|
+
/**
|
|
175
|
+
* copyfile as the session copies a file: the file \`from\` names (a link
|
|
176
|
+
* followed) to \`path\` (a link there followed, a file there replaced),
|
|
177
|
+
* by reference within one filesystem and by its bytes across, no byte of
|
|
178
|
+
* it in the wave. The copy's mode is a copy's there: a new file the
|
|
179
|
+
* source's less the umask, a file there its own.
|
|
180
|
+
*/
|
|
181
|
+
| { call: 'copyFile'; path: string; from: string }
|
|
128
182
|
/**
|
|
129
183
|
* rm(1) as fs.rm makes it: a file or link unlinked, a directory with all
|
|
130
184
|
* it holds when `recursive` (else EISDIR's refusal, as unlink's), and a
|
|
@@ -132,22 +186,27 @@ export type W7Call =
|
|
|
132
186
|
*/
|
|
133
187
|
| { call: 'rm'; path: string; recursive?: true; force?: true }
|
|
134
188
|
/** chown(2) of the link itself (lchown): the name's own entry, never what it names. */
|
|
135
|
-
| { call: 'lchown'; path: string; uid: number; gid: number }
|
|
189
|
+
| { call: 'lchown'; path: string; uid: number | null; gid: number | null }
|
|
136
190
|
/** utimes of the link itself (lutimes). */
|
|
137
191
|
| { call: 'lutimes'; path: string; atime: number; mtime: number }
|
|
138
192
|
/**
|
|
139
|
-
* open(2) of a file
|
|
140
|
-
* session's own open decides it: a name made
|
|
141
|
-
* `umask`) or refused (EEXIST when `exclusive`,
|
|
142
|
-
* EISDIR, EACCES for any access asked that the
|
|
193
|
+
* open(2) of a file with the access `access` asks, as O_ACCMODE does
|
|
194
|
+
* (W7OpenAccess), as the session's own open decides it: a name made
|
|
195
|
+
* (`create`, `mode` less `umask`) or refused (EEXIST when `exclusive`,
|
|
196
|
+
* ENOENT without `create`, EISDIR, EACCES for any access asked that the
|
|
197
|
+
* file's mode refuses, write too under `truncate`: openAccessMask),
|
|
143
198
|
* emptied when `truncate`, a link at the name followed unless `nofollow`
|
|
144
199
|
* (ELOOP). Its answer is the file's stat. With `description` (an id the
|
|
145
200
|
* process chose), the session keeps the open description under it, its
|
|
146
|
-
* access
|
|
147
|
-
* name it, and its `close` ends it. `ino`: the number
|
|
148
|
-
* given (a delegation's, as a mkdir's).
|
|
201
|
+
* rights the access's (openAccessRights); the description's write, append
|
|
202
|
+
* and ftruncate calls name it, and its `close` ends it. `ino`: the number
|
|
203
|
+
* a name it makes is given (a delegation's, as a mkdir's). `fchmod`: the
|
|
204
|
+
* file it opened then given exactly this mode (fchmod(2), as libuv's
|
|
205
|
+
* copyfile narrows its destination before a byte reaches it), part of the
|
|
206
|
+
* open: refused (EPERM: another's file), the open is refused and keeps no
|
|
207
|
+
* description, so a write through it is EBADF.
|
|
149
208
|
*/
|
|
150
|
-
| { call: 'open'; path: string; mode: number; ino?: number; umask?: number;
|
|
209
|
+
| { call: 'open'; path: string; mode: number; access: W7OpenAccess; ino?: number; umask?: number; create?: true; truncate?: true; exclusive?: true; nofollow?: true; description?: string; fchmod?: number; expect?: W7Expected }
|
|
151
210
|
/** close(2) of the session's open description `description` (an `open` call's). */
|
|
152
211
|
| { call: 'close'; path: string; description: string };
|
|
153
212
|
|
|
@@ -162,9 +221,11 @@ export type W7Op =
|
|
|
162
221
|
| { type: 'directory'; inode: BatchInodeEntry }
|
|
163
222
|
| { type: 'file'; inode: BatchInodeEntry; data: Uint8Array }
|
|
164
223
|
| { type: 'file'; inode: BatchInodeEntry; source: AsyncIterable<Uint8Array> }
|
|
165
|
-
|
|
224
|
+
/** `expect`, `expectTo`: the files the process expected at `from` and at `to` (W7Expected). */
|
|
225
|
+
| { type: 'rename'; from: string; to: string; expect?: W7Expected; expectTo?: W7Expected }
|
|
166
226
|
| { type: 'truncate'; path: string; size: number }
|
|
167
|
-
|
|
227
|
+
/** `description`: through the session's open description of that id (fchmod, fchown, futimens): its file, whatever names it now. */
|
|
228
|
+
| { type: 'setattr'; path: string; attrs: W7Attrs; description?: string; expect?: W7Expected }
|
|
168
229
|
| { type: 'call'; call: W7Call };
|
|
169
230
|
|
|
170
231
|
export const W7_MAGIC = new Uint8Array([0x4e, 0x57, 0x37, 0x04]);
|
|
@@ -315,9 +376,9 @@ export type W7DecodedRecord =
|
|
|
315
376
|
chunkCount: number;
|
|
316
377
|
check: number;
|
|
317
378
|
}
|
|
318
|
-
| { type: 'rename'; from: string; to: string }
|
|
379
|
+
| { type: 'rename'; from: string; to: string; expect?: W7Expected; expectTo?: W7Expected }
|
|
319
380
|
| { type: 'truncate'; path: string; size: number }
|
|
320
|
-
| { type: 'setattr'; path: string; attrs: W7Attrs }
|
|
381
|
+
| { type: 'setattr'; path: string; attrs: W7Attrs; description?: string; expect?: W7Expected }
|
|
321
382
|
| { type: 'call'; call: W7PathCall }
|
|
322
383
|
| { type: 'batch-end'; summary: W7BatchSummary };
|
|
323
384
|
|
|
@@ -332,9 +393,9 @@ type EncoderOp =
|
|
|
332
393
|
| { kind: 'delete'; path: string }
|
|
333
394
|
| { kind: 'directory'; inode: W7DirectoryInode }
|
|
334
395
|
| { kind: 'file'; file: EncoderFile }
|
|
335
|
-
| { kind: 'rename'; from: string; to: string }
|
|
396
|
+
| { kind: 'rename'; from: string; to: string; expect?: W7Expected; expectTo?: W7Expected }
|
|
336
397
|
| { kind: 'truncate'; path: string; size: number }
|
|
337
|
-
| { kind: 'setattr'; path: string; attrs: W7Attrs }
|
|
398
|
+
| { kind: 'setattr'; path: string; attrs: W7Attrs; description?: string; expect?: W7Expected }
|
|
338
399
|
| { kind: 'call'; call: W7PathCall };
|
|
339
400
|
|
|
340
401
|
interface EncoderFile {
|
|
@@ -677,7 +738,7 @@ async function* decodeRecords(
|
|
|
677
738
|
if (v3) throw new Error(`w7-frame: unknown record tag ${envelope.tag}`);
|
|
678
739
|
summary.pathCount++;
|
|
679
740
|
summary.opCount++;
|
|
680
|
-
const value = parseObject(payload, 'call', ['call', 'path'], ['mode', 'target', 'ino', 'size', 'existing', 'recursive', 'force', 'uid', 'gid', 'atime', 'mtime', 'umask', '
|
|
741
|
+
const value = parseObject(payload, 'call', ['call', 'path'], ['mode', 'target', 'from', 'ino', 'size', 'existing', 'recursive', 'force', 'uid', 'gid', 'atime', 'mtime', 'umask', 'access', 'create', 'truncate', 'exclusive', 'nofollow', 'description', 'fchmod', 'expect']);
|
|
681
742
|
yield { type: 'call', call: parsePathCall(value, (path, label) => ownedPaths.claim(canonicalPath(path, label))) };
|
|
682
743
|
break;
|
|
683
744
|
}
|
|
@@ -688,18 +749,21 @@ async function* decodeRecords(
|
|
|
688
749
|
summary.pathCount++;
|
|
689
750
|
summary.opCount++;
|
|
690
751
|
if (envelope.tag === RecordTag.Rename) {
|
|
691
|
-
const value = parseObject(payload, 'rename', ['from', 'to']);
|
|
752
|
+
const value = parseObject(payload, 'rename', ['from', 'to'], ['expect', 'expectTo']);
|
|
692
753
|
const from = ownedPaths.claim(canonicalPath(value.from, 'rename source'));
|
|
693
754
|
const to = ownedPaths.claim(canonicalPath(value.to, 'rename target'));
|
|
694
|
-
yield { type: 'rename', from, to };
|
|
755
|
+
yield { type: 'rename', from, to, ...expectations(value, 'rename') };
|
|
695
756
|
} else if (envelope.tag === RecordTag.Truncate) {
|
|
696
757
|
const value = parseObject(payload, 'truncate', ['path', 'size']);
|
|
697
758
|
yield { type: 'truncate', path: ownedPaths.claim(canonicalPath(value.path, 'truncate path')), size: safeInteger(value.size, 'truncate size') };
|
|
698
759
|
} else {
|
|
699
|
-
const value = parseObject(payload, 'setattr', ['path'], ['mode', 'uid', 'gid', 'atime', 'mtime']);
|
|
760
|
+
const value = parseObject(payload, 'setattr', ['path'], ['mode', 'uid', 'gid', 'atime', 'mtime', 'description', 'expect']);
|
|
700
761
|
const path = ownedPaths.claim(canonicalPath(value.path, 'setattr path'));
|
|
701
|
-
const { path: _path, ...attrs } = value;
|
|
702
|
-
yield {
|
|
762
|
+
const { path: _path, description, expect: _expect, ...attrs } = value;
|
|
763
|
+
yield {
|
|
764
|
+
type: 'setattr', path, attrs: parseAttrs(attrs, 'setattr'),
|
|
765
|
+
...(description === undefined ? {} : { description: descriptionId(description, 'setattr description') }), ...expectations(value, 'setattr'),
|
|
766
|
+
};
|
|
703
767
|
}
|
|
704
768
|
break;
|
|
705
769
|
}
|
|
@@ -768,7 +832,7 @@ async function* encodeRecords(batchId: string, ops: readonly EncoderOp[]): Async
|
|
|
768
832
|
case 'rename':
|
|
769
833
|
state.summary.pathCount++;
|
|
770
834
|
state.summary.opCount++;
|
|
771
|
-
yield encodeMetadataRecord(RecordTag.Rename, { from: op.from, to: op.to }, state);
|
|
835
|
+
yield encodeMetadataRecord(RecordTag.Rename, { from: op.from, to: op.to, ...expectations(op, 'rename') }, state);
|
|
772
836
|
break;
|
|
773
837
|
case 'truncate':
|
|
774
838
|
state.summary.pathCount++;
|
|
@@ -778,7 +842,7 @@ async function* encodeRecords(batchId: string, ops: readonly EncoderOp[]): Async
|
|
|
778
842
|
case 'setattr':
|
|
779
843
|
state.summary.pathCount++;
|
|
780
844
|
state.summary.opCount++;
|
|
781
|
-
yield encodeMetadataRecord(RecordTag.SetAttr, { path: op.path, ...op.attrs }, state);
|
|
845
|
+
yield encodeMetadataRecord(RecordTag.SetAttr, { path: op.path, ...op.attrs, ...(op.description === undefined ? {} : { description: op.description }), ...expectations(op, 'setattr') }, state);
|
|
782
846
|
break;
|
|
783
847
|
case 'call':
|
|
784
848
|
state.summary.pathCount++;
|
|
@@ -812,8 +876,8 @@ async function* encodeFile(file: EncoderFile, state: EncoderState): AsyncGenerat
|
|
|
812
876
|
let fileCheck = 0;
|
|
813
877
|
let chunkId = 0;
|
|
814
878
|
const pieces = file.source === null ? givenChunks(file.chunks) : fixedChunks(file.inode, file.source);
|
|
879
|
+
const contentBytes = TEXT_ENCODER.encode(file.contentId);
|
|
815
880
|
for await (const data of pieces) {
|
|
816
|
-
const contentBytes = new TextEncoder().encode(file.contentId);
|
|
817
881
|
const prefix = new Uint8Array(4 + contentBytes.length + 8);
|
|
818
882
|
writeU32LE(prefix, 0, contentBytes.length);
|
|
819
883
|
prefix.set(contentBytes, 4);
|
|
@@ -880,16 +944,20 @@ function encodeMetadataRecord(
|
|
|
880
944
|
value: object,
|
|
881
945
|
state?: EncoderState,
|
|
882
946
|
): Uint8Array[] {
|
|
883
|
-
const payload =
|
|
947
|
+
const payload = TEXT_ENCODER.encode(JSON.stringify(value));
|
|
884
948
|
if (payload.byteLength > MAX_METADATA_BYTES) {
|
|
885
949
|
throw new Error(`w7-frame: metadata record exceeds ${MAX_METADATA_BYTES} bytes`);
|
|
886
950
|
}
|
|
887
|
-
|
|
951
|
+
// The header and the payload in one buffer, checked as one (crc32 composes).
|
|
952
|
+
const record = new Uint8Array(5 + payload.byteLength);
|
|
953
|
+
record[0] = tag;
|
|
954
|
+
writeU32LE(record, 1, payload.byteLength);
|
|
955
|
+
record.set(payload, 5);
|
|
888
956
|
if (state && tag !== RecordTag.BatchEnd) {
|
|
889
|
-
state.batchCheck = updateRecordCheck(state.batchCheck,
|
|
957
|
+
state.batchCheck = updateRecordCheck(state.batchCheck, record);
|
|
890
958
|
state.summary.recordCount++;
|
|
891
959
|
}
|
|
892
|
-
return [
|
|
960
|
+
return [record];
|
|
893
961
|
}
|
|
894
962
|
|
|
895
963
|
/**
|
|
@@ -981,11 +1049,16 @@ function prepareOps(payload: BatchWritePayload, batchId: string): EncoderOp[] {
|
|
|
981
1049
|
kind: 'rename',
|
|
982
1050
|
from: ownedPaths.claim(canonicalPath(op.from, 'rename source')),
|
|
983
1051
|
to: ownedPaths.claim(canonicalPath(op.to, 'rename target')),
|
|
1052
|
+
...expectations(op, 'rename'),
|
|
984
1053
|
};
|
|
985
1054
|
case 'truncate':
|
|
986
1055
|
return { kind: 'truncate', path: ownedPaths.claim(canonicalPath(op.path, 'truncate path')), size: safeInteger(op.size, 'truncate size') };
|
|
987
1056
|
case 'setattr':
|
|
988
|
-
return {
|
|
1057
|
+
return {
|
|
1058
|
+
kind: 'setattr', path: ownedPaths.claim(canonicalPath(op.path, 'setattr path')), attrs: parseAttrs(op.attrs as Record<string, unknown>, 'setattr'),
|
|
1059
|
+
...(op.description === undefined ? {} : { description: descriptionId(op.description, 'setattr description') }),
|
|
1060
|
+
...expectations(op, 'setattr'),
|
|
1061
|
+
};
|
|
989
1062
|
case 'call': {
|
|
990
1063
|
const call = op.call;
|
|
991
1064
|
if ('data' in call) {
|
|
@@ -1020,11 +1093,18 @@ function parseBatchBegin(bytes: Uint8Array, mode: W7Mode): BatchBeginMetadata {
|
|
|
1020
1093
|
function parseAttrs(value: Record<string, unknown>, label: string): W7Attrs {
|
|
1021
1094
|
const keys = Object.keys(value).sort().join(',');
|
|
1022
1095
|
if (keys === 'mode') return { mode: u32(value.mode, `${label} mode`) };
|
|
1023
|
-
if (keys === 'gid,uid') return { uid:
|
|
1024
|
-
if (keys === 'atime,mtime') return { atime:
|
|
1096
|
+
if (keys === 'gid,uid') return { uid: ownerId(value.uid, `${label} uid`), gid: ownerId(value.gid, `${label} gid`) };
|
|
1097
|
+
if (keys === 'atime,mtime') return { atime: parseFsTime(value.atime, `w7-frame: ${label} atime`), mtime: parseFsTime(value.mtime, `w7-frame: ${label} mtime`) };
|
|
1025
1098
|
throw new Error(`w7-frame: ${label} changes the mode, the owner or the times, one of them: got ${keys || 'nothing'}`);
|
|
1026
1099
|
}
|
|
1027
1100
|
|
|
1101
|
+
/** `value` as an FsTime (a finite number of milliseconds, 'now' or 'omit'); else an error naming `label`. */
|
|
1102
|
+
export function parseFsTime(value: unknown, label: string): FsTime {
|
|
1103
|
+
if (value === 'now' || value === 'omit') return value;
|
|
1104
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
1105
|
+
throw new Error(`${label} must be a time: milliseconds, 'now' or 'omit'`);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1028
1108
|
function parseDelete(bytes: Uint8Array): DeleteMetadata {
|
|
1029
1109
|
const value = parseObject(bytes, 'delete', ['path']);
|
|
1030
1110
|
return { path: canonicalPath(value.path, 'delete path') };
|
|
@@ -1100,14 +1180,17 @@ function parsePathCall(value: Record<string, unknown>, path: (value: unknown, la
|
|
|
1100
1180
|
}
|
|
1101
1181
|
case 'unlink':
|
|
1102
1182
|
case 'rmdir':
|
|
1103
|
-
if (keys !== 'call,path') break;
|
|
1104
|
-
return { call: value.call, path: path(value.path, `${value.call} path`) };
|
|
1183
|
+
if (keys !== 'call,path' && keys !== 'call,expect,path') break;
|
|
1184
|
+
return { call: value.call, path: path(value.path, `${value.call} path`), ...expectations(value, value.call) };
|
|
1105
1185
|
case 'symlink':
|
|
1106
1186
|
if (keys !== 'call,path,target' && keys !== 'call,ino,path,target') break;
|
|
1107
1187
|
return {
|
|
1108
1188
|
call: 'symlink', path: path(value.path, 'symlink path'), target: boundedString(value.target, 'symlink target', MAX_PATH_BYTES),
|
|
1109
1189
|
...(value.ino === undefined ? {} : { ino: inodeNumber(value.ino, 'symlink ino') }),
|
|
1110
1190
|
};
|
|
1191
|
+
case 'copyFile':
|
|
1192
|
+
if (keys !== 'call,from,path') break;
|
|
1193
|
+
return { call: 'copyFile', path: path(value.path, 'copyFile path'), from: path(value.from, 'copyFile from') };
|
|
1111
1194
|
case 'ftruncate':
|
|
1112
1195
|
if (keys.replace(',description', '').replace(',ino', '') !== 'call,path,size') break;
|
|
1113
1196
|
return {
|
|
@@ -1131,25 +1214,28 @@ function parsePathCall(value: Record<string, unknown>, path: (value: unknown, la
|
|
|
1131
1214
|
}
|
|
1132
1215
|
case 'lchown':
|
|
1133
1216
|
if (keys !== 'call,gid,path,uid') break;
|
|
1134
|
-
return { call: 'lchown', path: path(value.path, 'lchown path'), uid:
|
|
1217
|
+
return { call: 'lchown', path: path(value.path, 'lchown path'), uid: ownerId(value.uid, 'lchown uid'), gid: ownerId(value.gid, 'lchown gid') };
|
|
1135
1218
|
case 'open': {
|
|
1136
|
-
const flags = ['create', 'exclusive', 'nofollow', '
|
|
1219
|
+
const flags = ['create', 'exclusive', 'nofollow', 'truncate'] as const;
|
|
1137
1220
|
let required = keys;
|
|
1138
|
-
for (const flag of ['create', 'description', 'exclusive', '
|
|
1139
|
-
if (required !== 'call,mode,path') break;
|
|
1221
|
+
for (const flag of ['create', 'description', 'exclusive', 'expect', 'fchmod', 'ino', 'nofollow', 'truncate', 'umask']) required = required.replace(`,${flag}`, '');
|
|
1222
|
+
if (required !== 'access,call,mode,path') break;
|
|
1140
1223
|
for (const flag of flags) {
|
|
1141
1224
|
if (value[flag] !== undefined && value[flag] !== true) throw new Error(`w7-frame: open ${flag} is true or absent`);
|
|
1142
1225
|
}
|
|
1226
|
+
const access = value.access;
|
|
1227
|
+
if (!isOpenAccess(access)) throw new Error(`w7-frame: open access is 'read', 'write' or 'readwrite': got ${String(access)}`);
|
|
1143
1228
|
return {
|
|
1144
|
-
call: 'open', path: path(value.path, 'open path'), mode: u32(value.mode, 'open mode'),
|
|
1229
|
+
call: 'open', path: path(value.path, 'open path'), mode: u32(value.mode, 'open mode'), access,
|
|
1145
1230
|
...(value.ino === undefined ? {} : { ino: inodeNumber(value.ino, 'open ino') }),
|
|
1146
1231
|
...(value.umask === undefined ? {} : { umask: umaskOf(value.umask, 'open umask') }),
|
|
1147
1232
|
...(value.create === true ? { create: true as const } : {}),
|
|
1148
1233
|
...(value.truncate === true ? { truncate: true as const } : {}),
|
|
1149
1234
|
...(value.exclusive === true ? { exclusive: true as const } : {}),
|
|
1150
1235
|
...(value.nofollow === true ? { nofollow: true as const } : {}),
|
|
1151
|
-
...(value.read === true ? { read: true as const } : {}),
|
|
1152
1236
|
...(value.description === undefined ? {} : { description: descriptionId(value.description, 'open description') }),
|
|
1237
|
+
...(value.fchmod === undefined ? {} : { fchmod: u32(value.fchmod, 'open fchmod') }),
|
|
1238
|
+
...expectations(value, 'open'),
|
|
1153
1239
|
};
|
|
1154
1240
|
}
|
|
1155
1241
|
case 'lutimes':
|
|
@@ -1224,6 +1310,22 @@ function inodeNumber(value: unknown, label: string): number {
|
|
|
1224
1310
|
return ino;
|
|
1225
1311
|
}
|
|
1226
1312
|
|
|
1313
|
+
/** A record's expectations (W7Expected), checked: `expect`, and a rename's `expectTo`. */
|
|
1314
|
+
function expectations(value: { expect?: unknown; expectTo?: unknown }, label: string): { expect?: W7Expected; expectTo?: W7Expected } {
|
|
1315
|
+
const expected = (field: unknown, name: string): W7Expected => {
|
|
1316
|
+
if (field === null) return null;
|
|
1317
|
+
if (isObject(field) && !Array.isArray(field)) {
|
|
1318
|
+
if (Object.keys(field).join(',') !== 'made') throw new Error(`w7-frame: ${label} ${name} is a number, null or { made }`);
|
|
1319
|
+
return { made: descriptionId(field.made, `${label} ${name} made`) };
|
|
1320
|
+
}
|
|
1321
|
+
return inodeNumber(field, `${label} ${name}`);
|
|
1322
|
+
};
|
|
1323
|
+
return {
|
|
1324
|
+
...(value.expect === undefined ? {} : { expect: expected(value.expect, 'expect') }),
|
|
1325
|
+
...(value.expectTo === undefined ? {} : { expectTo: expected(value.expectTo, 'expectTo') }),
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1227
1329
|
function parseObject(
|
|
1228
1330
|
bytes: Uint8Array,
|
|
1229
1331
|
label: string,
|
|
@@ -1384,15 +1486,7 @@ class PathOwnership {
|
|
|
1384
1486
|
}
|
|
1385
1487
|
|
|
1386
1488
|
function normalizePath(path: string): string {
|
|
1387
|
-
|
|
1388
|
-
for (const segment of path.split('/')) {
|
|
1389
|
-
if (segment === '..') {
|
|
1390
|
-
if (out.length > 0) out.pop();
|
|
1391
|
-
} else if (segment !== '' && segment !== '.') {
|
|
1392
|
-
out.push(segment);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
return out.join('/');
|
|
1489
|
+
return rootClampedSegments(path).join('/');
|
|
1396
1490
|
}
|
|
1397
1491
|
|
|
1398
1492
|
function parentPath(path: string): string {
|
|
@@ -1415,6 +1509,11 @@ function safeInteger(value: unknown, label: string): number {
|
|
|
1415
1509
|
return value as number;
|
|
1416
1510
|
}
|
|
1417
1511
|
|
|
1512
|
+
/** A chown side: a uid or gid, or null, which keeps the file's. */
|
|
1513
|
+
function ownerId(value: unknown, label: string): number | null {
|
|
1514
|
+
return value === null ? null : u32(value, label);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1418
1517
|
function u32(value: unknown, label: string): number {
|
|
1419
1518
|
const integer = safeInteger(value, label);
|
|
1420
1519
|
if (integer > 0xffff_ffff) throw new Error(`w7-frame: ${label} exceeds uint32`);
|
package/src/wave-writer.ts
CHANGED
|
@@ -164,7 +164,7 @@ export interface WaveWriterOptions<Meta = undefined> {
|
|
|
164
164
|
root: string | null;
|
|
165
165
|
/** An existing worktree: only directories strictly below it are published. */
|
|
166
166
|
worktreeRoot?: string | null;
|
|
167
|
-
/** Record paths are relative to it (joined with '/'); omitted, they are VFS paths. */
|
|
167
|
+
/** Record paths are relative to it (joined with '/'; '' is the base itself); omitted, they are VFS paths. */
|
|
168
168
|
base?: string;
|
|
169
169
|
/** Wall-clock time (ms) after which no new wave starts. */
|
|
170
170
|
deadline?: number | null;
|
|
@@ -300,7 +300,6 @@ export class WaveWriter<Meta = undefined> {
|
|
|
300
300
|
private ownedPathBytes = 0;
|
|
301
301
|
private pin: Pin | null = null;
|
|
302
302
|
private inFlight: Promise<void> | null = null;
|
|
303
|
-
private cutQueue: Promise<unknown> = Promise.resolve();
|
|
304
303
|
private sequence = 0;
|
|
305
304
|
private failure: WaveFailure | null = null;
|
|
306
305
|
/** Owners (records' `meta`) whose records a failed wave carried. */
|
|
@@ -532,7 +531,8 @@ export class WaveWriter<Meta = undefined> {
|
|
|
532
531
|
// ── Internals ────────────────────────────────────────────────────────
|
|
533
532
|
|
|
534
533
|
private key(path: string): string {
|
|
535
|
-
|
|
534
|
+
if (!this.options.base) return path;
|
|
535
|
+
return path ? this.options.base + '/' + path : this.options.base;
|
|
536
536
|
}
|
|
537
537
|
|
|
538
538
|
private tally(): Tally {
|
|
@@ -551,13 +551,24 @@ export class WaveWriter<Meta = undefined> {
|
|
|
551
551
|
if (admit) this.owned.add(path);
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
-
/**
|
|
555
|
-
|
|
554
|
+
/**
|
|
555
|
+
* The directories a wave publishes for something at or below `dir`:
|
|
556
|
+
* `dir` and each one above it, deepest first, up to the writer's root
|
|
557
|
+
* (included) and inside its worktree root.
|
|
558
|
+
*/
|
|
559
|
+
private *chain(dir: string): Generator<string, void, undefined> {
|
|
556
560
|
const { root, worktreeRoot = null } = this.options;
|
|
557
|
-
let current =
|
|
558
|
-
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
+
for (let current = dir; current; current = parentOf(current)) {
|
|
562
|
+
if (root && current !== root && !current.startsWith(root + '/')) return;
|
|
563
|
+
if (worktreeRoot !== null && !current.startsWith(worktreeRoot + '/')) return;
|
|
564
|
+
yield current;
|
|
565
|
+
if (current === root) return;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** The chain from `path` upward, to the first directory already owned. */
|
|
570
|
+
private walkChain(path: string, admit: boolean, tally: Tally): void {
|
|
571
|
+
for (const current of this.chain(path)) {
|
|
561
572
|
this.counters.ownershipVisits++;
|
|
562
573
|
if (this.ownedDirectories.has(current)) break;
|
|
563
574
|
if (admit) this.ownedDirectories.add(current);
|
|
@@ -566,8 +577,6 @@ export class WaveWriter<Meta = undefined> {
|
|
|
566
577
|
tally.pathBytes += utf8Length(current);
|
|
567
578
|
if (admit) this.owned.add(current);
|
|
568
579
|
}
|
|
569
|
-
if (current === root) break;
|
|
570
|
-
current = parentOf(current);
|
|
571
580
|
}
|
|
572
581
|
}
|
|
573
582
|
|
|
@@ -636,16 +645,14 @@ export class WaveWriter<Meta = undefined> {
|
|
|
636
645
|
}
|
|
637
646
|
|
|
638
647
|
/**
|
|
639
|
-
* Send the buffered wave once the one in flight has published.
|
|
640
|
-
*
|
|
648
|
+
* Send the buffered wave once the one in flight has published. Only a
|
|
649
|
+
* mutation cuts, and mutations run one at a time, so at most one wave is
|
|
650
|
+
* in flight and one buffers.
|
|
641
651
|
*/
|
|
642
|
-
private cut(): Promise<void> {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
private async cutNow(): Promise<void> {
|
|
652
|
+
private async cut(): Promise<void> {
|
|
653
|
+
// A later turn than the mutation that asks: a pin set or cleared by a
|
|
654
|
+
// microtask queued before the cut is the wave's.
|
|
655
|
+
await Promise.resolve();
|
|
649
656
|
if (this.inFlight) {
|
|
650
657
|
const waitStarted = Date.now();
|
|
651
658
|
await this.inFlight.catch(() => {});
|
|
@@ -848,16 +855,10 @@ export class WaveWriter<Meta = undefined> {
|
|
|
848
855
|
/** The directories the buffered records publish, shallowest first. */
|
|
849
856
|
private publishedDirectories(): string[] {
|
|
850
857
|
const dirs = new Set<string>();
|
|
851
|
-
const { root, worktreeRoot = null } = this.options;
|
|
852
858
|
const collect = (path: string): void => {
|
|
853
|
-
|
|
854
|
-
while (current) {
|
|
855
|
-
if (root && current !== root && !current.startsWith(root + '/')) break;
|
|
856
|
-
if (worktreeRoot !== null && !current.startsWith(worktreeRoot + '/')) break;
|
|
859
|
+
for (const current of this.chain(path)) {
|
|
857
860
|
if (dirs.has(current)) break;
|
|
858
861
|
dirs.add(current);
|
|
859
|
-
if (current === root) break;
|
|
860
|
-
current = parentOf(current);
|
|
861
862
|
}
|
|
862
863
|
};
|
|
863
864
|
for (const path of this.records.keys()) collect(parentOf(path));
|