@daugasauron/dolly 0.1.4 → 0.1.5

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.
Files changed (60) hide show
  1. package/README.md +134 -14
  2. package/coi-serviceworker.js +28 -0
  3. package/dist/dolly-build-id.mjs +1 -1
  4. package/dist/dolly-dso-0.wasm +0 -0
  5. package/dist/dolly-image-build-id.mjs +1 -1
  6. package/dist/dolly-process-0.wasm +0 -0
  7. package/dist/dolly-process-abi.mjs +1 -1
  8. package/dist/dolly-process-dso.mjs +382 -53
  9. package/dist/dolly-process-gate-0.wasm +0 -0
  10. package/dist/dolly-process-worker.mjs +164 -35
  11. package/dist/dolly.wasm +0 -0
  12. package/host/dso/abi.mjs +3 -1
  13. package/host/dso/dso.mjs +1 -2
  14. package/host/http/abi.mjs +2 -2
  15. package/host/http/broker.mjs +18 -8
  16. package/host/http/http.mjs +0 -1
  17. package/host/http/policy.mjs +8 -4
  18. package/host/input/abi.mjs +47 -0
  19. package/host/modules.mjs +4 -3
  20. package/host/nena/abi.mjs +1 -1
  21. package/host/nena/nena.mjs +16 -18
  22. package/host/packages/service.mjs +35 -15
  23. package/host/packages/worker.mjs +21 -0
  24. package/host/runtime/abi.mjs +1 -0
  25. package/host/runtime/boot-files.mjs +8 -2
  26. package/host/runtime/grower.mjs +11 -8
  27. package/host/runtime/lease-bridge.mjs +8 -5
  28. package/host/runtime/module.json +1 -1
  29. package/host/runtime/runtime.mjs +37 -18
  30. package/host/snapshot/abi.mjs +7 -9
  31. package/host/snapshot/snapshot.mjs +14 -12
  32. package/host/snapshot/transport.mjs +4 -12
  33. package/host/snapshot/ui.mjs +17 -7
  34. package/host/stack/stack.mjs +1 -2
  35. package/host/threads/abi.mjs +1 -1
  36. package/host/threads/threads.mjs +2 -1
  37. package/package.json +1 -1
  38. package/src/browser.mjs +34 -37
  39. package/src/build-log.mjs +13 -2
  40. package/src/build-worker.mjs +182 -0
  41. package/src/custom-image.mjs +59 -0
  42. package/src/dollyfile-graph.mjs +127 -0
  43. package/src/dollyfile-view.mjs +335 -0
  44. package/src/image-artifact.mjs +248 -0
  45. package/src/image-build.mjs +89 -0
  46. package/src/image-builder.mjs +63 -0
  47. package/src/images.mjs +13 -2
  48. package/src/pack-store.mjs +141 -0
  49. package/src/packaged-snapshot.mjs +263 -91
  50. package/src/page-chords.mjs +4 -2
  51. package/src/process-constants.mjs +4 -2
  52. package/src/process-supervisor.mjs +209 -122
  53. package/src/runtime-worker.mjs +9 -10
  54. package/src/session-store.mjs +5 -7
  55. package/src/sha256.mjs +65 -0
  56. package/src/snapshot-records.mjs +135 -23
  57. package/src/static-asset.mjs +29 -2
  58. package/src/terminal-text.mjs +8 -5
  59. package/src/version.mjs +1 -1
  60. package/src/worker-image.mjs +3 -5
@@ -1,9 +1,10 @@
1
1
  import { alphabetical } from "./alphabetical.mjs";
2
2
  import { validName } from "./image-names.mjs";
3
3
  import { imageInputs } from "./image-inputs.mjs";
4
+ import { MAX_SNAPSHOT_BYTES } from "./snapshot-records.mjs";
4
5
  import { DOLLY_VERSION } from "./version.mjs";
5
6
 
6
- export const DOLLY_SESSION_FORMAT_VERSION = 2;
7
+ export const DOLLY_SESSION_FORMAT_VERSION = 3;
7
8
  export const DOLLY_SESSION_MAX_BYTES = 512 * 1024 * 1024;
8
9
  export const DOLLY_SESSION_METADATA_MAX_BYTES = 1024 * 1024;
9
10
 
@@ -31,7 +32,7 @@ export function customSessionIdentity(custom) {
31
32
  typeof artifact.recipeSha256 !== "string" || typeof artifact.sha256 !== "string" ||
32
33
  !/^[0-9a-f]{64}$/.test(artifact.recipeSha256) || !/^[0-9a-f]{64}$/.test(artifact.sha256) ||
33
34
  !Number.isSafeInteger(artifact.byteLength) || artifact.byteLength <= 0 ||
34
- artifact.byteLength > DOLLY_SESSION_MAX_BYTES ||
35
+ artifact.byteLength > MAX_SNAPSHOT_BYTES ||
35
36
  (policies !== undefined && (!Array.isArray(policies) || policies.length === 0 || policies.length > 16 ||
36
37
  new TextEncoder().encode(JSON.stringify(policies)).byteLength > 65536))) {
37
38
  throw new TypeError("invalid custom session base");
@@ -111,11 +112,9 @@ export async function encodeSessionStream(produce) {
111
112
  export async function decodeSessionSnapshot(record) {
112
113
  if (record === null || typeof record !== "object" ||
113
114
  !(record.bytes instanceof ArrayBuffer) || record.bytes.byteLength === 0 ||
114
- record.bytes.byteLength > DOLLY_SESSION_MAX_BYTES ||
115
- !["gzip", "identity"].includes(record.encoding)) {
115
+ record.bytes.byteLength > DOLLY_SESSION_MAX_BYTES || record.encoding !== "gzip") {
116
116
  throw new Error("Stored Dolly session is invalid");
117
117
  }
118
- if (record.encoding === "identity") return record.bytes.slice(0);
119
118
  let offset = 0;
120
119
  const stream = new ReadableStream({
121
120
  pull(controller) {
@@ -178,8 +177,7 @@ export function validateSessionRecord(record) {
178
177
  typeof record.imageIdentity !== "string" || record.imageIdentity.length > 256 ||
179
178
  !Number.isSafeInteger(record.updatedAt) ||
180
179
  !(record.bytes instanceof ArrayBuffer) || record.bytes.byteLength === 0 ||
181
- record.bytes.byteLength > DOLLY_SESSION_MAX_BYTES ||
182
- !["gzip", "identity"].includes(record.encoding)) {
180
+ record.bytes.byteLength > DOLLY_SESSION_MAX_BYTES || record.encoding !== "gzip") {
183
181
  throw new TypeError("invalid Dolly session record");
184
182
  }
185
183
  if (record.image === "custom" ? record.imageIdentity !== customSessionIdentity(record.customImage)
package/src/sha256.mjs ADDED
@@ -0,0 +1,65 @@
1
+ // SHA-256 over bytes given in pieces, for a digest of more bytes than the page
2
+ // should hold at once; WebCrypto digests only a whole buffer.
3
+ const K = Int32Array.of(
4
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
5
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
6
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
7
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
8
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
9
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
10
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
11
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2);
12
+
13
+ export class Sha256 {
14
+ state = Int32Array.of(0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19);
15
+ block = new Uint8Array(64);
16
+ used = 0;
17
+ length = 0;
18
+ words = new Int32Array(64);
19
+
20
+ compress(view, offset) {
21
+ const w = this.words, s = this.state;
22
+ for (let i = 0; i < 16; i++) w[i] = view.getInt32(offset + 4 * i);
23
+ for (let i = 16; i < 64; i++) {
24
+ const x = w[i - 15], y = w[i - 2];
25
+ w[i] = (((x >>> 7 | x << 25) ^ (x >>> 18 | x << 14) ^ x >>> 3) + w[i - 7] +
26
+ ((y >>> 17 | y << 15) ^ (y >>> 19 | y << 13) ^ y >>> 10) + w[i - 16]) | 0;
27
+ }
28
+ let a = s[0], b = s[1], c = s[2], d = s[3], e = s[4], f = s[5], g = s[6], h = s[7];
29
+ for (let i = 0; i < 64; i++) {
30
+ const t1 = (h + ((e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7)) + (e & f ^ ~e & g) + K[i] + w[i]) | 0;
31
+ const t2 = (((a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10)) + (a & b ^ a & c ^ b & c)) | 0;
32
+ h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
33
+ }
34
+ s[0] += a; s[1] += b; s[2] += c; s[3] += d; s[4] += e; s[5] += f; s[6] += g; s[7] += h;
35
+ }
36
+
37
+ update(bytes) {
38
+ this.length += bytes.length;
39
+ let offset = 0;
40
+ if (this.used !== 0) {
41
+ offset = Math.min(64 - this.used, bytes.length);
42
+ this.block.set(bytes.subarray(0, offset), this.used);
43
+ this.used += offset;
44
+ if (this.used < 64) return;
45
+ this.compress(new DataView(this.block.buffer), 0);
46
+ this.used = 0;
47
+ }
48
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
49
+ for (; offset + 64 <= bytes.length; offset += 64) this.compress(view, offset);
50
+ this.block.set(bytes.subarray(offset));
51
+ this.used = bytes.length - offset;
52
+ }
53
+
54
+ // The lowercase hex digest; the hash takes no more bytes after it.
55
+ hex() {
56
+ const bits = this.length * 8;
57
+ this.update(Uint8Array.of(0x80));
58
+ this.update(new Uint8Array((120 - this.used) % 64));
59
+ const view = new DataView(this.block.buffer);
60
+ view.setUint32(56, Math.floor(bits / 2 ** 32));
61
+ view.setUint32(60, bits >>> 0);
62
+ this.compress(view, 0);
63
+ return [...this.state].map(word => (word >>> 0).toString(16).padStart(8, "0")).join("");
64
+ }
65
+ }
@@ -1,6 +1,9 @@
1
1
  import { unretainedPath } from "./image-names.mjs";
2
2
  const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
3
3
  export const MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024 * 1024;
4
+ // Packs average 2 MiB or more (scripts/share-pages-snapshots.mjs): an image of
5
+ // MAX_SNAPSHOT_BYTES needs about 1,024, which this bound leaves room for.
6
+ const MAX_SNAPSHOT_PACKS = MAX_SNAPSHOT_BYTES / (1024 * 1024);
4
7
 
5
8
  function compareBytes(left, right) {
6
9
  for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
@@ -9,47 +12,156 @@ function compareBytes(left, right) {
9
12
  return left.length - right.length;
10
13
  }
11
14
 
12
- export function decodeSnapshotRecords(input) {
13
- const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
15
+ // The 16-byte snapshot header: its record count.
16
+ function decodeHeader(bytes) {
14
17
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
15
- if (bytes.length < 16 || bytes.length > MAX_SNAPSHOT_BYTES ||
16
- decoder.decode(bytes.subarray(0, 8)) !== "DOLLYSNP" || view.getUint32(8, true) !== 2) {
18
+ if (bytes.length < 16 || decoder.decode(bytes.subarray(0, 8)) !== "DOLLYSNP" || view.getUint32(8, true) !== 2) {
17
19
  throw new Error("invalid Dolly snapshot header");
18
20
  }
19
21
  const count = view.getUint32(12, true);
20
22
  if (count === 0 || count > 100_000) throw new Error("invalid Dolly snapshot record count");
23
+ return count;
24
+ }
25
+
26
+ // A record's 16-byte header: its kind, path length and data size.
27
+ function decodeRecordHeader(bytes) {
28
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
29
+ const kind = view.getUint32(0, true), pathLength = view.getUint32(4, true), size = view.getBigUint64(8, true);
30
+ if (kind < 1 || kind > 3 || pathLength < 2 || pathLength >= 4096 ||
31
+ size > BigInt(MAX_SNAPSHOT_BYTES) || (kind === 1 && size !== 0n) ||
32
+ (kind === 3 && (size === 0n || size >= 4096n))) throw new Error("invalid snapshot record size");
33
+ return { kind, pathLength, size: Number(size) };
34
+ }
35
+
36
+ // The path of PATHBYTES, valid and after PREVIOUS in byte order.
37
+ function decodePath(pathBytes, previous) {
38
+ const path = decoder.decode(pathBytes);
39
+ if (!path.startsWith("/") || /[\0\\\r\n]/.test(path) ||
40
+ path.slice(1).split("/").some(part => !part || part === "." || part === "..") ||
41
+ unretainedPath(path) ||
42
+ previous && compareBytes(previous, pathBytes) >= 0) throw new Error(`invalid snapshot path ${path}`);
43
+ return path;
44
+ }
45
+
46
+ function checkParents(records) {
47
+ for (const path of records.keys()) {
48
+ for (let slash = path.indexOf("/", 1); slash !== -1; slash = path.indexOf("/", slash + 1)) {
49
+ const parent = records.get(path.slice(0, slash));
50
+ if (parent && parent.kind !== 1) throw new Error(`snapshot parent is not a directory: ${path}`);
51
+ }
52
+ }
53
+ }
54
+
55
+ export function decodeSnapshotRecords(input) {
56
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
57
+ if (bytes.length > MAX_SNAPSHOT_BYTES) throw new Error("invalid Dolly snapshot header");
58
+ const count = decodeHeader(bytes.subarray(0, 16));
21
59
  const records = new Map();
22
60
  let offset = 16, previous;
23
61
  for (let index = 0; index < count; index += 1) {
24
62
  if (offset > bytes.length - 16) throw new Error("truncated snapshot record");
25
- const kind = view.getUint32(offset, true), pathLength = view.getUint32(offset + 4, true);
26
- const size = view.getBigUint64(offset + 8, true);
63
+ const { kind, pathLength, size } = decodeRecordHeader(bytes.subarray(offset, offset + 16));
27
64
  offset += 16;
28
- if (kind < 1 || kind > 3 || pathLength < 2 || pathLength >= 4096 ||
29
- size > BigInt(MAX_SNAPSHOT_BYTES) || (kind === 1 && size !== 0n) ||
30
- (kind === 3 && (size === 0n || size >= 4096n))) throw new Error("invalid snapshot record size");
31
- if (offset > bytes.length - pathLength - Number(size)) throw new Error("truncated snapshot record");
65
+ if (offset > bytes.length - pathLength - size) throw new Error("truncated snapshot record");
32
66
  const pathBytes = bytes.subarray(offset, offset + pathLength);
33
- const path = decoder.decode(pathBytes);
34
- if (!path.startsWith("/") || /[\0\\\r\n]/.test(path) ||
35
- path.slice(1).split("/").some(part => !part || part === "." || part === "..") ||
36
- unretainedPath(path) ||
37
- previous && compareBytes(previous, pathBytes) >= 0) throw new Error(`invalid snapshot path ${path}`);
67
+ const path = decodePath(pathBytes, previous);
38
68
  previous = pathBytes;
39
69
  offset += pathLength;
40
- const data = bytes.subarray(offset, offset + Number(size));
70
+ const data = bytes.subarray(offset, offset + size);
41
71
  if (kind === 3 && data.includes(0)) throw new Error("invalid snapshot symlink");
42
72
  records.set(path, { kind, data });
43
- offset += Number(size);
73
+ offset += size;
44
74
  }
45
75
  if (offset !== bytes.length) throw new Error("trailing snapshot bytes");
46
- for (const path of records.keys()) {
47
- for (let slash = path.indexOf("/", 1); slash !== -1; slash = path.indexOf("/", slash + 1)) {
48
- const parent = records.get(path.slice(0, slash));
49
- if (parent && parent.kind !== 1) throw new Error(`snapshot parent is not a directory: ${path}`);
76
+ checkParents(records);
77
+ return records;
78
+ }
79
+
80
+ // Bytes that arrive in pieces (an iterable of Uint8Array), read in order.
81
+ class PieceReader {
82
+ constructor(pieces) {
83
+ this.pieces = pieces[Symbol.asyncIterator]?.() ?? pieces[Symbol.iterator]();
84
+ this.chunk = new Uint8Array(0);
85
+ this.position = 0;
86
+ }
87
+
88
+ async #fill() {
89
+ const { done, value } = await this.pieces.next();
90
+ if (done) return false;
91
+ if (!(value instanceof Uint8Array)) throw new TypeError("invalid snapshot piece");
92
+ this.chunk = value;
93
+ return true;
94
+ }
95
+
96
+ #advance(length) {
97
+ this.position += length;
98
+ if (this.position > MAX_SNAPSHOT_BYTES) throw new Error("snapshot exceeds its size limit");
99
+ }
100
+
101
+ // The next LENGTH bytes, a view into the current piece when they lie in one.
102
+ async take(length) {
103
+ if (length === 0) return new Uint8Array(0);
104
+ if (this.chunk.length === 0 && !await this.#fill()) throw new Error("truncated snapshot record");
105
+ if (this.chunk.length >= length) {
106
+ const bytes = this.chunk.subarray(0, length);
107
+ this.chunk = this.chunk.subarray(length);
108
+ this.#advance(length);
109
+ return bytes;
50
110
  }
111
+ const bytes = new Uint8Array(length);
112
+ for (let filled = 0; filled < length;) {
113
+ if (this.chunk.length === 0 && !await this.#fill()) throw new Error("truncated snapshot record");
114
+ const taken = Math.min(this.chunk.length, length - filled);
115
+ bytes.set(this.chunk.subarray(0, taken), filled);
116
+ this.chunk = this.chunk.subarray(taken);
117
+ filled += taken;
118
+ }
119
+ this.#advance(length);
120
+ return bytes;
51
121
  }
52
- return records;
122
+
123
+ async skip(length) {
124
+ while (length !== 0) {
125
+ if (this.chunk.length === 0 && !await this.#fill()) throw new Error("truncated snapshot record");
126
+ const taken = Math.min(this.chunk.length, length);
127
+ this.chunk = this.chunk.subarray(taken);
128
+ this.#advance(taken);
129
+ length -= taken;
130
+ }
131
+ }
132
+
133
+ async ended() {
134
+ while (this.chunk.length === 0) if (!await this.#fill()) return true;
135
+ return false;
136
+ }
137
+ }
138
+
139
+ // The records of a snapshot that arrives in pieces, checked as
140
+ // decodeSnapshotRecords checks them without holding the snapshot: every path
141
+ // with its kind, where its data lies (offset, size), and the data of the paths
142
+ // KEEP selects (symlink targets always). Resolves to the records and the
143
+ // snapshot's size.
144
+ export async function scanSnapshotRecords(pieces, keep) {
145
+ const reader = new PieceReader(pieces);
146
+ const count = decodeHeader(await reader.take(16));
147
+ const records = new Map();
148
+ let previous;
149
+ for (let index = 0; index < count; index += 1) {
150
+ const { kind, pathLength, size } = decodeRecordHeader(await reader.take(16));
151
+ const pathBytes = await reader.take(pathLength);
152
+ const path = decodePath(pathBytes, previous);
153
+ previous = pathBytes;
154
+ const offset = reader.position;
155
+ let data;
156
+ if (kind === 3 || (kind === 2 && keep(path))) {
157
+ data = await reader.take(size);
158
+ if (kind === 3 && data.includes(0)) throw new Error("invalid snapshot symlink");
159
+ } else await reader.skip(size);
160
+ records.set(path, { kind, data, offset, size });
161
+ }
162
+ if (!await reader.ended()) throw new Error("trailing snapshot bytes");
163
+ checkParents(records);
164
+ return { records, byteLength: reader.position };
53
165
  }
54
166
 
55
167
  export function encodeSnapshotRecords(records) {
@@ -91,7 +203,7 @@ export function mergeSnapshotRecords(parts) {
91
203
  }
92
204
 
93
205
  export function validateSnapshotPacks(metadata) {
94
- if (!Array.isArray(metadata.packs) || metadata.packs.length === 0 || metadata.packs.length > 256) throw new Error("invalid snapshot packs");
206
+ if (!Array.isArray(metadata.packs) || metadata.packs.length === 0 || metadata.packs.length > MAX_SNAPSHOT_PACKS) throw new Error("invalid snapshot packs");
95
207
  const hashes = new Set();
96
208
  let size = 16;
97
209
  for (const pack of metadata.packs) {
@@ -55,12 +55,39 @@ export async function boundedAssetBody(response, limit, signal) {
55
55
  return bytes;
56
56
  }
57
57
 
58
+ // A file a host stores in parts is a JSON manifest at its path that begins
59
+ // with these bytes, and no file stored whole does (the Pages exporter checks),
60
+ // so the page knows a manifest by its content: a host needs no header for it.
61
+ // Pages before v0.1.5 read the X-Dolly-Parts header instead.
62
+ export const PARTS_MANIFEST_START = '{"byteLength":';
63
+
64
+ // Whether a whole response's body begins as a manifest, read from a copy of it.
65
+ async function isPartsManifest(response) {
66
+ if (response.status !== 200 || !response.body) return false;
67
+ const start = new TextEncoder().encode(PARTS_MANIFEST_START), reader = response.clone().body.getReader();
68
+ const head = new Uint8Array(start.length);
69
+ let size = 0;
70
+ try {
71
+ while (size < start.length) {
72
+ const { done, value } = await reader.read();
73
+ if (done) break;
74
+ const taken = value.subarray(0, start.length - size);
75
+ head.set(taken, size);
76
+ size += taken.length;
77
+ }
78
+ } finally {
79
+ // Not awaited: cancelling a copy settles only once the response is read too.
80
+ reader.cancel().catch(() => {});
81
+ }
82
+ return size === start.length && head.every((byte, index) => byte === start[index]);
83
+ }
84
+
58
85
  // Streams verified parts, holding at most one 20 MiB part per request. Each
59
86
  // part matches the manifest hash before any of its bytes are delivered.
60
87
  export async function decodeStaticAsset(response, target, init, maximumBytes, fetchRequest = globalThis.fetch.bind(globalThis)) {
61
- if (response.headers.get("x-dolly-parts") !== "1") return response;
88
+ if (!await isPartsManifest(response)) return response;
62
89
  const url = new URL(target);
63
- if (response.status !== 200 || (init.method ?? "GET") !== "GET" || url.search || url.hash ||
90
+ if ((init.method ?? "GET") !== "GET" || url.search || url.hash ||
64
91
  url.username || url.password || !/^https?:$/.test(url.protocol)) throw new Error("invalid multipart asset request");
65
92
  const manifest = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(await boundedAssetBody(response, 65536, init.signal)));
66
93
  if (!Number.isSafeInteger(manifest.byteLength) || manifest.byteLength <= 0 ||
@@ -1,3 +1,6 @@
1
+ import { DOLLY_POINTER_ACTION_DRAG as drag, DOLLY_POINTER_ACTION_PRESS as press,
2
+ DOLLY_POINTER_ACTION_RELEASE as release } from "../host/input/abi.mjs";
3
+
1
4
  // How the page's handle (docs/embedding.md) reads the terminal, over the page APIs of
2
5
  // display@0, input@0 and the runtime: the screen's text is the terminal's own
3
6
  // selection of every cell, made with pointer records.
@@ -36,10 +39,10 @@ export function terminalText(page) {
36
39
  const y = geometry.paddingY + Math.floor(geometry.cellHeight / 2);
37
40
  const endX = x + (dimensions.cols - 1) * geometry.cellWidth, endY = y + (dimensions.rows - 1) * geometry.cellHeight;
38
41
  let selected;
39
- await inputTransport.pushPointer(x, y, 1, shifted);
42
+ await inputTransport.pushPointer(x, y, press, shifted);
40
43
  await waitFor(async () => await transport.copySelection() === null, "terminal selection reset");
41
- await inputTransport.pushPointer(endX, endY, 2, shifted);
42
- await inputTransport.pushPointer(endX, endY, 0, shifted);
44
+ await inputTransport.pushPointer(endX, endY, drag, shifted);
45
+ await inputTransport.pushPointer(endX, endY, release, shifted);
43
46
  if (await waitFor(async () => (selected = await transport.copySelection()) !== null, null, attempts)) {
44
47
  return selected.split("\n").slice(-dimensions.rows).join("\n");
45
48
  }
@@ -62,8 +65,8 @@ export function terminalText(page) {
62
65
  const geometry = transport.geometry();
63
66
  const x = geometry.paddingX + Math.floor(geometry.cellWidth / 2);
64
67
  const y = geometry.paddingY + Math.floor(geometry.cellHeight / 2);
65
- await inputTransport.pushPointer(x, y, 1, shifted);
66
- await inputTransport.pushPointer(x, y, 0, shifted);
68
+ await inputTransport.pushPointer(x, y, press, shifted);
69
+ await inputTransport.pushPointer(x, y, release, shifted);
67
70
  await waitFor(() => inputTransport.inputIdle(), "terminal selection cleanup");
68
71
  return pid;
69
72
  }
package/src/version.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  // The version being developed: the release tag vX.Y.Z, the site path /vX.Y.Z/
2
2
  // and the prefix of every recipe reference. package.json holds the same value.
3
- export const DOLLY_VERSION = "0.1.4";
3
+ export const DOLLY_VERSION = "0.1.5";
@@ -3,7 +3,6 @@
3
3
  // image into that kernel, and runs its ENTRY. runtime-worker.mjs restores an
4
4
  // image of the image directory; the site's build-worker.mjs builds one or
5
5
  // restores a custom one.
6
- import { DOLLY_BUILD_ID } from "../dist/dolly-build-id.mjs";
7
6
  import { loadImages } from "./images.mjs";
8
7
  import { decodeImageEntry } from "./image-entry.mjs";
9
8
  import { MAX_SNAPSHOT_BYTES } from "./snapshot-records.mjs";
@@ -28,7 +27,7 @@ const configured = new Promise((resolve, reject) => {
28
27
  const { hostRegistry } = await import("../host/modules.mjs");
29
28
 
30
29
  // The runtime's own files: the kernel and the contracts beside this module.
31
- export const runtimeBase = new URL("../", import.meta.url);
30
+ const runtimeBase = new URL("../", import.meta.url);
32
31
  export const kernelArtifact = path => {
33
32
  if (path !== "dolly.wasm" && path !== "dolly.data") throw new Error("unknown fixed kernel artifact");
34
33
  return new URL(`dist/${path}`, runtimeBase).href;
@@ -45,7 +44,7 @@ export function checkedMemoryRange(memory, addressValue, sizeValue) {
45
44
  return { address, size };
46
45
  }
47
46
 
48
- const stage = text => self.postMessage({ type: "bootstrap", text: `${text}\n`, error: false });
47
+ const stage = text => self.postMessage({ type: "bootstrap", text: `${text}\n` });
49
48
 
50
49
  function acknowledged(type, failure) {
51
50
  return new Promise((resolve, reject) => {
@@ -111,8 +110,7 @@ export async function runImage(image) {
111
110
  }
112
111
  stage("starting image entry...");
113
112
  const entryReady = acknowledged("entry-ready-ack", "browser did not acknowledge image startup");
114
- self.postMessage({ type: "ready", image: runtimeImage, routeImage: steps.name, buildId: DOLLY_BUILD_ID,
115
- hostModules: host.enabled });
113
+ self.postMessage({ type: "ready", image: runtimeImage });
116
114
  await entryReady;
117
115
 
118
116
  // The image ends with its ENTRY process, however that ended: a program that