@spyglassmc/core 0.4.46 → 0.4.48

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.
@@ -1,7 +1,7 @@
1
1
  export declare namespace Dev {
2
2
  function assertDefined<T>(value: T): asserts value is Exclude<T, undefined>;
3
3
  function assertNever(value: never): never;
4
- function assertTrue(value: boolean, message: string): void;
4
+ function assertTrue(value: unknown, message: string): asserts value;
5
5
  /**
6
6
  * @returns An estimate of the memory taken by the given value, assuming objects are stored as array-like structures instead of dictionaries in the V8 engine.
7
7
  */
@@ -0,0 +1,6 @@
1
+ export declare class EventDispatcher<TEvents extends Record<string, unknown>> {
2
+ #private;
3
+ emit<K extends keyof TEvents & string>(name: K, data: TEvents[K]): void;
4
+ on<K extends keyof TEvents & string>(name: K, listener: (data: TEvents[K]) => unknown, options?: AddEventListenerOptions): this;
5
+ }
6
+ //# sourceMappingURL=EventDispatcher.d.ts.map
@@ -0,0 +1,15 @@
1
+ import { Dev } from './Dev.js';
2
+ export class EventDispatcher {
3
+ #target = new EventTarget();
4
+ emit(name, data) {
5
+ this.#target.dispatchEvent(new CustomEvent(name, { detail: data }));
6
+ }
7
+ on(name, listener, options) {
8
+ this.#target.addEventListener(name, (event) => {
9
+ Dev.assertTrue(event instanceof CustomEvent, 'event must be an instance of CustomEvent');
10
+ listener(event.detail);
11
+ }, options);
12
+ return this;
13
+ }
14
+ }
15
+ //# sourceMappingURL=EventDispatcher.js.map
@@ -1,4 +1,4 @@
1
- import { emplaceMap, isObject } from './util.js';
1
+ import { getOrInsertComputed, isObject } from './util.js';
2
2
  export var ReadonlyProxy;
3
3
  (function (ReadonlyProxy) {
4
4
  function create(obj) {
@@ -11,7 +11,7 @@ class ReadonlyProxyHandler {
11
11
  get(target, p, receiver) {
12
12
  const value = Reflect.get(target, p, receiver);
13
13
  if (p !== 'prototype' && isObject(value)) {
14
- return emplaceMap(this.map, p, { insert: () => ReadonlyProxy.create(value) });
14
+ return getOrInsertComputed(this.map, p, () => ReadonlyProxy.create(value));
15
15
  }
16
16
  return value;
17
17
  }
@@ -1,4 +1,4 @@
1
- import { emplaceMap, isObject } from '../common/index.js';
1
+ import { getOrInsertComputed, isObject } from '../common/index.js';
2
2
  import { Operations } from './Operations.js';
3
3
  const BranchOff = Symbol('StateBranchOff');
4
4
  const Is = Symbol('IsStateProxy');
@@ -63,7 +63,7 @@ class StateProxyHandler {
63
63
  }
64
64
  const value = Reflect.get(target, p, receiver);
65
65
  if (p !== 'prototype' && isObject(value)) {
66
- return emplaceMap(this.map, p, { insert: () => _createStateProxy(value, this.rootOps) });
66
+ return getOrInsertComputed(this.map, p, () => _createStateProxy(value, this.rootOps));
67
67
  }
68
68
  return value;
69
69
  }
@@ -1,11 +1,3 @@
1
- import type { ExternalEventEmitter, Externals } from './index.js';
2
- type Listener = (...args: unknown[]) => unknown;
3
- export declare class BrowserEventEmitter implements ExternalEventEmitter {
4
- #private;
5
- emit(eventName: string, ...args: unknown[]): boolean;
6
- on(eventName: string, listener: Listener): this;
7
- once(eventName: string, listener: Listener): this;
8
- }
1
+ import type { Externals } from './index.js';
9
2
  export declare const BrowserExternals: Externals;
10
- export {};
11
3
  //# sourceMappingURL=BrowserExternals.d.ts.map
@@ -1,40 +1,5 @@
1
1
  import { decode as arrayBufferFromBase64, encode as arrayBufferToBase64 } from 'base64-arraybuffer';
2
- import pako from 'pako';
3
2
  import { fileUtil } from '../../service/fileUtil.js';
4
- export class BrowserEventEmitter {
5
- #listeners = new Map();
6
- emit(eventName, ...args) {
7
- const listeners = this.#listeners.get(eventName);
8
- if (!listeners?.all?.size) {
9
- return false;
10
- }
11
- for (const listener of listeners.all) {
12
- listener(...args);
13
- if (listeners.once.has(listener)) {
14
- listeners.all.delete(listener);
15
- listeners.once.delete(listener);
16
- }
17
- }
18
- return false;
19
- }
20
- on(eventName, listener) {
21
- if (!this.#listeners.has(eventName)) {
22
- this.#listeners.set(eventName, { all: new Set(), once: new Set() });
23
- }
24
- const listeners = this.#listeners.get(eventName);
25
- listeners.all.add(listener);
26
- return this;
27
- }
28
- once(eventName, listener) {
29
- if (!this.#listeners.has(eventName)) {
30
- this.#listeners.set(eventName, { all: new Set(), once: new Set() });
31
- }
32
- const listeners = this.#listeners.get(eventName);
33
- listeners.all.add(listener);
34
- listeners.once.add(listener);
35
- return this;
36
- }
37
- }
38
3
  // TODO: Use Origin Private File System (OPFS) instead
39
4
  class BrowserFileSystem {
40
5
  static LocalStorageKey = 'spyglassmc-browser-fs';
@@ -113,21 +78,6 @@ export const BrowserExternals = {
113
78
  decompressBall(_buffer, _options) {
114
79
  throw new Error('decompressBall not supported on browser.');
115
80
  },
116
- async gunzip(buffer) {
117
- return pako.inflate(buffer);
118
- },
119
- async gzip(buffer) {
120
- return pako.gzip(buffer);
121
- },
122
- },
123
- crypto: {
124
- async getSha1(data) {
125
- if (typeof data === 'string') {
126
- data = new TextEncoder().encode(data);
127
- }
128
- const hash = await crypto.subtle.digest('SHA-1', data.buffer);
129
- return uint8ArrayToHex(new Uint8Array(hash));
130
- },
131
81
  },
132
82
  error: {
133
83
  createKind(kind, message) {
@@ -137,19 +87,10 @@ export const BrowserExternals = {
137
87
  return e instanceof Error && e.message.startsWith(kind);
138
88
  },
139
89
  },
140
- event: { EventEmitter: BrowserEventEmitter },
141
90
  fs: new BrowserFileSystem(),
142
91
  web: {
143
- fetch,
144
92
  getCache: () => window.caches.open('spyglassmc'),
145
93
  },
146
94
  };
147
- function uint8ArrayToHex(array) {
148
- let ans = '';
149
- for (const v of array) {
150
- ans += v.toString(16).padStart(2, '0');
151
- }
152
- return ans;
153
- }
154
95
  Object.freeze(BrowserExternals);
155
96
  //# sourceMappingURL=BrowserExternals.js.map
@@ -1,28 +1,22 @@
1
- import { EventEmitter } from 'node:events';
2
- import fs, { promises as fsp } from 'node:fs';
3
- import type { DecompressedFile, RootUriString } from '../../index.js';
1
+ import type fs from 'node:fs';
2
+ import fsp from 'node:fs/promises';
3
+ import { type DecompressedFile, type RootUriString } from '../../index.js';
4
+ import { Logger } from '../Logger.js';
4
5
  import type { FsLocation } from './index.js';
5
- export declare function getNodeJsExternals({ cacheRoot, nodeFsp }?: {
6
+ export declare function getNodeJsExternals({ cacheRoot, logger, nodeFsp }?: {
6
7
  cacheRoot?: RootUriString;
8
+ logger?: Logger;
7
9
  nodeFsp?: typeof fsp;
8
10
  }): Readonly<{
9
11
  archive: {
10
12
  decompressBall(buffer: Uint8Array<ArrayBuffer>, options: {
11
13
  stripLevel?: number;
12
14
  } | undefined): Promise<DecompressedFile[]>;
13
- gunzip(buffer: Uint8Array<ArrayBuffer>): Promise<NonSharedBuffer>;
14
- gzip(buffer: Uint8Array<ArrayBuffer>): Promise<NonSharedBuffer>;
15
- };
16
- crypto: {
17
- getSha1(data: string | Uint8Array<ArrayBuffer>): Promise<string>;
18
15
  };
19
16
  error: {
20
17
  createKind(kind: import("./index.js").ExternalErrorKind, message: string): Error;
21
18
  isKind(e: unknown, kind: import("./index.js").ExternalErrorKind): boolean;
22
19
  };
23
- event: {
24
- EventEmitter: typeof EventEmitter;
25
- };
26
20
  fs: {
27
21
  chmod(location: FsLocation, mode: number): Promise<void>;
28
22
  mkdir(location: FsLocation, options: {
@@ -42,7 +36,6 @@ export declare function getNodeJsExternals({ cacheRoot, nodeFsp }?: {
42
36
  } | undefined): Promise<void>;
43
37
  };
44
38
  web: {
45
- fetch: typeof fetch;
46
39
  getCache: () => Promise<HttpCache>;
47
40
  };
48
41
  }>;
@@ -51,19 +44,11 @@ export declare const NodeJsExternals: Readonly<{
51
44
  decompressBall(buffer: Uint8Array<ArrayBuffer>, options: {
52
45
  stripLevel?: number;
53
46
  } | undefined): Promise<DecompressedFile[]>;
54
- gunzip(buffer: Uint8Array<ArrayBuffer>): Promise<NonSharedBuffer>;
55
- gzip(buffer: Uint8Array<ArrayBuffer>): Promise<NonSharedBuffer>;
56
- };
57
- crypto: {
58
- getSha1(data: string | Uint8Array<ArrayBuffer>): Promise<string>;
59
47
  };
60
48
  error: {
61
49
  createKind(kind: import("./index.js").ExternalErrorKind, message: string): Error;
62
50
  isKind(e: unknown, kind: import("./index.js").ExternalErrorKind): boolean;
63
51
  };
64
- event: {
65
- EventEmitter: typeof EventEmitter;
66
- };
67
52
  fs: {
68
53
  chmod(location: FsLocation, mode: number): Promise<void>;
69
54
  mkdir(location: FsLocation, options: {
@@ -83,7 +68,6 @@ export declare const NodeJsExternals: Readonly<{
83
68
  } | undefined): Promise<void>;
84
69
  };
85
70
  web: {
86
- fetch: typeof fetch;
87
71
  getCache: () => Promise<HttpCache>;
88
72
  };
89
73
  }>;
@@ -93,7 +77,7 @@ export declare const NodeJsExternals: Readonly<{
93
77
  */
94
78
  declare class HttpCache implements Cache {
95
79
  #private;
96
- constructor(cacheRoot: RootUriString | undefined);
80
+ constructor(cacheRoot: RootUriString | undefined, logger: Logger, nodeFsp: typeof fsp);
97
81
  match(request: RequestInfo | URL, _options?: CacheQueryOptions | undefined): Promise<Response | undefined>;
98
82
  put(request: RequestInfo | URL, response: Response): Promise<void>;
99
83
  add(): Promise<void>;
@@ -1,18 +1,17 @@
1
1
  import decompress from 'decompress';
2
2
  import { Buffer } from 'node:buffer';
3
3
  import cp from 'node:child_process';
4
- import crypto from 'node:crypto';
5
- import { EventEmitter } from 'node:events';
6
- import fs, { promises as fsp } from 'node:fs';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import fsp from 'node:fs/promises';
7
6
  import os from 'node:os';
8
7
  import process from 'node:process';
9
8
  import stream from 'node:stream';
9
+ import { pipeline } from 'node:stream/promises';
10
10
  import url from 'node:url';
11
11
  import { promisify } from 'node:util';
12
- import zlib from 'node:zlib';
13
- const gunzip = promisify(zlib.gunzip);
14
- const gzip = promisify(zlib.gzip);
15
- export function getNodeJsExternals({ cacheRoot, nodeFsp = fsp } = {}) {
12
+ import { Dev, isObject } from '../../index.js';
13
+ import { Logger } from '../Logger.js';
14
+ export function getNodeJsExternals({ cacheRoot, logger = Logger.create(), nodeFsp = fsp } = {}) {
16
15
  return Object.freeze({
17
16
  archive: {
18
17
  decompressBall(buffer, options) {
@@ -21,19 +20,6 @@ export function getNodeJsExternals({ cacheRoot, nodeFsp = fsp } = {}) {
21
20
  }
22
21
  return decompress(buffer, { strip: options?.stripLevel });
23
22
  },
24
- gunzip(buffer) {
25
- return gunzip(buffer);
26
- },
27
- gzip(buffer) {
28
- return gzip(buffer);
29
- },
30
- },
31
- crypto: {
32
- async getSha1(data) {
33
- const hash = crypto.createHash('sha1');
34
- hash.update(data);
35
- return hash.digest('hex');
36
- },
37
23
  },
38
24
  error: {
39
25
  createKind(kind, message) {
@@ -45,7 +31,6 @@ export function getNodeJsExternals({ cacheRoot, nodeFsp = fsp } = {}) {
45
31
  return e instanceof Error && e.code === kind;
46
32
  },
47
33
  },
48
- event: { EventEmitter },
49
34
  fs: {
50
35
  chmod(location, mode) {
51
36
  return nodeFsp.chmod(toFsPathLike(location), mode);
@@ -92,9 +77,8 @@ export function getNodeJsExternals({ cacheRoot, nodeFsp = fsp } = {}) {
92
77
  },
93
78
  },
94
79
  web: {
95
- fetch,
96
80
  getCache: async () => {
97
- return new HttpCache(cacheRoot);
81
+ return new HttpCache(cacheRoot, logger, nodeFsp);
98
82
  },
99
83
  },
100
84
  });
@@ -116,55 +100,217 @@ function toPath(path) {
116
100
  return uriToPath(path);
117
101
  }
118
102
  const uriToPath = (uri) => url.fileURLToPath(uri);
103
+ var CacheIndex;
104
+ (function (CacheIndex) {
105
+ function assert(val) {
106
+ if (!isObject(val)) {
107
+ throw new Error('Expected an object');
108
+ }
109
+ if (!('index' in val && isObject(val.index))) {
110
+ throw new Error("Expected 'index' to exist as an object");
111
+ }
112
+ if (!(Object.values(val.index).every((i) => isObject(i)
113
+ && Object.values(i).every((v) => isObject(v)
114
+ && 'status' in v && typeof v.status === 'number'
115
+ && 'statusText' in v && typeof v.statusText === 'string'
116
+ && 'headers' in v && isObject(v.headers)
117
+ && Object.values(v.headers).every((s) => typeof s === 'string')
118
+ && 'sha1' in v && typeof v.sha1 === 'string' && /^[0-9a-f]{40}$/.test(v.sha1)
119
+ && 'cacheTime' in v && typeof v.cacheTime === 'number')))) {
120
+ throw new Error('Malformed index structure');
121
+ }
122
+ }
123
+ CacheIndex.assert = assert;
124
+ })(CacheIndex || (CacheIndex = {}));
119
125
  /**
120
126
  * A non-spec-compliant, non-complete implementation of the Cache Web API for use in Spyglass.
121
127
  * This class stores the cached response on the file system under the cache root.
122
128
  */
123
129
  class HttpCache {
124
130
  #cacheRoot;
125
- constructor(cacheRoot) {
131
+ #httpRoot;
132
+ #indexUri;
133
+ #objectsRoot;
134
+ #tempRoot;
135
+ #logger;
136
+ #fsp;
137
+ #index;
138
+ #loadIndexPromise;
139
+ constructor(cacheRoot, logger, nodeFsp) {
126
140
  if (cacheRoot) {
127
- this.#cacheRoot = `${cacheRoot}http/`;
141
+ this.#cacheRoot = cacheRoot;
142
+ this.#httpRoot = `${this.#cacheRoot}http/`;
143
+ this.#indexUri = `${this.#httpRoot}index.json`;
144
+ this.#objectsRoot = `${this.#httpRoot}objects/`;
145
+ this.#tempRoot = `${this.#httpRoot}temp/`;
128
146
  }
147
+ this.#logger = logger;
148
+ this.#fsp = nodeFsp;
129
149
  }
130
150
  async match(request, _options) {
131
- if (!this.#cacheRoot) {
151
+ if (!(this.#indexUri && this.#objectsRoot && this.#tempRoot)) {
152
+ return undefined;
153
+ }
154
+ const index = await this.#loadIndex(this.#indexUri);
155
+ const requestUri = request instanceof Request ? request.url : request.toString();
156
+ const requestRange = request instanceof Request ? request.headers.get('range') : undefined;
157
+ const indexEntry = index.index[requestUri]?.[requestRange ?? ''];
158
+ if (!indexEntry) {
132
159
  return undefined;
133
160
  }
134
- const fileName = this.#getFileName(request);
135
161
  try {
136
- const etag = (await fsp.readFile(new URL(`${fileName}.etag`, this.#cacheRoot), 'utf8'))
137
- .trim();
138
- const bodyStream = fs.createReadStream(new URL(`${fileName}.bin`, this.#cacheRoot));
139
- return new Response(stream.Readable.toWeb(bodyStream),
140
- // \___/
141
- // stream Readable -> stream/web ReadableStream
142
- // \_______________/
143
- // stream/web ReadableStream -> DOM ReadableStream
144
- { headers: { etag } });
162
+ const objectFileHandle = await this.#fsp.open(this.#getObjectUri(this.#objectsRoot, indexEntry.sha1));
163
+ const bodyStream = objectFileHandle.createReadStream({ autoClose: true });
164
+ return new Response(stream.Readable.toWeb(bodyStream), {
165
+ headers: indexEntry.headers,
166
+ status: indexEntry.status,
167
+ statusText: indexEntry.statusText,
168
+ });
145
169
  }
146
170
  catch (e) {
147
171
  if (e?.code === 'ENOENT') {
172
+ this.#logger.warn(`Object file for ${JSON.stringify(indexEntry)} does not exist`, e);
173
+ delete index.index[requestUri]?.[requestRange ?? ''];
174
+ if (Object.values(index.index[requestUri]).length === 0) {
175
+ delete index.index[requestUri];
176
+ }
177
+ await this.#saveIndex(this.#indexUri, this.#tempRoot);
148
178
  return undefined;
149
179
  }
150
180
  throw e;
151
181
  }
152
182
  }
153
183
  async put(request, response) {
184
+ if (!(this.#tempRoot && this.#objectsRoot && this.#indexUri && response.body)) {
185
+ return;
186
+ }
154
187
  const etag = response.headers.get('etag');
155
- if (!(this.#cacheRoot && response.body && etag)) {
188
+ const lastModified = response.headers.get('last-modified');
189
+ if (!(etag || lastModified)) {
156
190
  return;
157
191
  }
158
- const fileName = this.#getFileName(request);
159
- await fsp.mkdir(new URL(this.#cacheRoot), { recursive: true });
160
- await Promise.all([
161
- fsp.writeFile(new URL(`${fileName}.bin`, this.#cacheRoot), stream.Readable.fromWeb(response.body)),
162
- fsp.writeFile(new URL(`${fileName}.etag`, this.#cacheRoot), `${etag}${os.EOL}`),
163
- ]);
192
+ const requestUri = request instanceof Request ? request.url : request.toString();
193
+ const requestRange = request instanceof Request ? request.headers.get('range') : undefined;
194
+ // Save response body in a temp file and computes its SHA-1 digest
195
+ const { bodySha1, bodyTempUri } = await this.#saveResponseBody(response.body, this.#tempRoot);
196
+ // Update the index
197
+ const index = await this.#loadIndex(this.#indexUri);
198
+ index.index[requestUri] ??= {};
199
+ const previousEntry = index.index[requestUri][requestRange ?? ''];
200
+ index.index[requestUri][requestRange ?? ''] = {
201
+ status: response.status,
202
+ statusText: response.statusText,
203
+ headers: Object.fromEntries(response.headers),
204
+ sha1: bodySha1,
205
+ cacheTime: Date.now(),
206
+ };
207
+ await this.#saveIndex(this.#indexUri, this.#tempRoot);
208
+ if (previousEntry) {
209
+ await this.#cleanUpDanglingObject(this.#objectsRoot, index, previousEntry.sha1);
210
+ }
211
+ // Rename the temp body file to its final location in the content-addressable object store
212
+ // match() would gracefully handle missing object if this step fails for any reason
213
+ const objectUri = this.#getObjectUri(this.#objectsRoot, bodySha1);
214
+ await this.#fsp.mkdir(new URL('.', objectUri), { recursive: true, mode: 0o755 });
215
+ await this.#fsp.rename(bodyTempUri, objectUri);
216
+ }
217
+ async #saveResponseBody(body, tempRoot) {
218
+ const bodyStream = stream.Readable.fromWeb(body);
219
+ const bodyHash = createHash('sha1');
220
+ // Tee the body stream to both a temporary file write stream and the SHA-1 hash stream
221
+ const tempUri = new URL(`body.${randomUUID()}.tmp`, tempRoot);
222
+ await this.#fsp.mkdir(new URL(tempRoot), { recursive: true, mode: 0o755 });
223
+ const tempHandle = await this.#fsp.open(tempUri, 'w', 0o644);
224
+ const writeStream = tempHandle.createWriteStream({ autoClose: true });
225
+ bodyStream.pipe(bodyHash);
226
+ await pipeline(bodyStream, writeStream);
227
+ const bodySha1 = bodyHash.digest('hex');
228
+ return { bodySha1, bodyTempUri: tempUri };
229
+ }
230
+ async #loadIndex(indexUri) {
231
+ await (this.#loadIndexPromise ??= (async () => {
232
+ try {
233
+ const parsedIndex = JSON.parse(await this.#fsp.readFile(new URL(indexUri), 'utf8'));
234
+ CacheIndex.assert(parsedIndex);
235
+ this.#index = parsedIndex;
236
+ }
237
+ catch (e) {
238
+ if (e.code === 'ENOENT') {
239
+ // Triger legacy cache clean up if no index file for the new cache scheme exists.
240
+ this.#logger.info('[HttpCache] No cache index found; cleaning up legacy cache files');
241
+ await this.#cleanUpLegacyCache();
242
+ }
243
+ else {
244
+ this.#logger.warn('[HttpCache] Corrupted cache index', e);
245
+ }
246
+ this.#index = { index: {} };
247
+ }
248
+ })());
249
+ return this.#index;
250
+ }
251
+ async #saveIndex(indexUri, tempRoot) {
252
+ try {
253
+ Dev.assertDefined(this.#index);
254
+ const tempUri = new URL(`index.${randomUUID()}.tmp`, tempRoot);
255
+ await this.#fsp.mkdir(new URL(tempRoot), { recursive: true, mode: 0o755 });
256
+ await this.#fsp.writeFile(tempUri, `${JSON.stringify(this.#index)}${os.EOL}`, {
257
+ mode: 0o644,
258
+ });
259
+ await this.#fsp.rename(tempUri, new URL(indexUri));
260
+ }
261
+ catch (e) {
262
+ this.#logger.warn('[HttpCache] Failed saving index', e);
263
+ }
264
+ }
265
+ /**
266
+ * Remove cache files used by older versions of Spyglass.
267
+ * - until v2026.5.8+8c7f6e, `${cacheRoot}downloader/`
268
+ * - until v2026.5.16+9c4fa2, `${cacheRoot}http/${base64UrlEncoded}.${'bin' | 'etag'}`
269
+ */
270
+ async #cleanUpLegacyCache() {
271
+ if (!this.#httpRoot) {
272
+ return;
273
+ }
274
+ try {
275
+ await this.#fsp.rm(new URL('downloader/', this.#cacheRoot), { recursive: true });
276
+ }
277
+ catch (e) {
278
+ if (e.code !== 'ENOENT') {
279
+ this.#logger.warn('[HttpCache] Failed cleaning up downloader/ dir', e);
280
+ }
281
+ }
282
+ try {
283
+ const httpDir = await this.#fsp.opendir(new URL(this.#httpRoot));
284
+ for await (const entry of httpDir) {
285
+ if (entry.isFile() && (entry.name.endsWith('.bin') || entry.name.endsWith('.etag'))) {
286
+ await this.#fsp.rm(new URL(entry.name, this.#httpRoot));
287
+ }
288
+ }
289
+ }
290
+ catch (e) {
291
+ if (e.code !== 'ENOENT') {
292
+ this.#logger.warn('[HttpCache] Failed cleaning up legacy cache files', e);
293
+ }
294
+ }
295
+ }
296
+ async #cleanUpDanglingObject(objectsRoot, index, sha1) {
297
+ if (Object.values(index.index).some((i) => Object.values(i).some((v) => v.sha1 === sha1))) {
298
+ // The object is still referenced
299
+ return false;
300
+ }
301
+ try {
302
+ await this.#fsp.rm(this.#getObjectUri(objectsRoot, sha1));
303
+ return true;
304
+ }
305
+ catch (e) {
306
+ if (e.code !== 'ENOENT') {
307
+ this.#logger.warn('[HttpCache] Failed cleaning up dangling object', e);
308
+ }
309
+ }
310
+ return false;
164
311
  }
165
- #getFileName(request) {
166
- const uriString = request instanceof Request ? request.url : request.toString();
167
- return Buffer.from(uriString, 'utf8').toString('base64url');
312
+ #getObjectUri(objectsRoot, sha1) {
313
+ return new URL(`${sha1.slice(0, 2)}/${sha1}`, objectsRoot);
168
314
  }
169
315
  async add() {
170
316
  throw new Error('Method not implemented.');
@@ -4,14 +4,6 @@ export interface Externals {
4
4
  decompressBall: (buffer: Uint8Array<ArrayBuffer>, options?: {
5
5
  stripLevel?: number;
6
6
  }) => Promise<DecompressedFile[]>;
7
- gzip: (buffer: Uint8Array<ArrayBuffer>) => Promise<Uint8Array<ArrayBuffer>>;
8
- gunzip: (buffer: Uint8Array<ArrayBuffer>) => Promise<Uint8Array<ArrayBuffer>>;
9
- };
10
- crypto: {
11
- /**
12
- * @returns SHA-1 digest of the given data in hexadecimal format.
13
- */
14
- getSha1: (data: string | Uint8Array<ArrayBuffer>) => Promise<string>;
15
7
  };
16
8
  error: {
17
9
  /**
@@ -23,12 +15,8 @@ export interface Externals {
23
15
  */
24
16
  isKind: (e: unknown, kind: ExternalErrorKind) => boolean;
25
17
  };
26
- event: {
27
- EventEmitter: new () => ExternalEventEmitter;
28
- };
29
18
  fs: ExternalFileSystem;
30
19
  web: {
31
- fetch: typeof fetch;
32
20
  getCache: () => Promise<Cache>;
33
21
  };
34
22
  }
@@ -40,11 +28,6 @@ export interface DecompressedFile {
40
28
  type: string;
41
29
  }
42
30
  export type ExternalErrorKind = 'EEXIST' | 'EISDIR' | 'ENOENT';
43
- export interface ExternalEventEmitter {
44
- emit(eventName: string, ...args: unknown[]): boolean;
45
- on(eventName: string, listener: (...args: unknown[]) => unknown): this;
46
- once(eventName: string, listener: (...args: unknown[]) => unknown): this;
47
- }
48
31
  export interface ExternalFileSystem {
49
32
  /**
50
33
  * @param mode File mode bit mask (e.g. `0o775`).
@@ -1,4 +1,5 @@
1
1
  export * from './Dev.js';
2
+ export * from './EventDispatcher.js';
2
3
  export * from './externals/index.js';
3
4
  export * from './json.js';
4
5
  export * from './Logger.js';
@@ -1,4 +1,5 @@
1
1
  export * from './Dev.js';
2
+ export * from './EventDispatcher.js';
2
3
  export * from './externals/index.js';
3
4
  export * from './json.js';
4
5
  export * from './Logger.js';
@@ -1,7 +1,6 @@
1
1
  import externalBinarySearch from 'binary-search';
2
2
  import type { AstNode } from '../node/index.js';
3
3
  import type { ProcessorContext } from '../service/index.js';
4
- import type { Externals } from './externals/index.js';
5
4
  import type { DeepReadonly, ReadWrite } from './ReadonlyProxy.js';
6
5
  export declare const Uri: {
7
6
  new (url: string | URL, base?: string | URL): URL;
@@ -77,7 +76,7 @@ export declare namespace TypePredicates {
77
76
  function isString(value: unknown): value is string;
78
77
  }
79
78
  export declare function promisifyAsyncIterable<T, U>(iterable: AsyncIterable<T>, joiner: (chunks: T[]) => U): Promise<U>;
80
- export declare function parseGzippedJson(externals: Externals, buffer: Uint8Array<ArrayBuffer>): Promise<unknown>;
79
+ export declare function parseGzippedJson(bytes: Uint8Array<ArrayBuffer>): Promise<unknown>;
81
80
  /**
82
81
  * @returns Is Plain Old JavaScript Object (POJO).
83
82
  */
@@ -110,17 +109,26 @@ export declare namespace Lazy {
110
109
  export declare function getStates(category: 'block' | 'fluid', ids: readonly string[], ctx: ProcessorContext): Record<string, string[]>;
111
110
  export declare const binarySearch: typeof externalBinarySearch;
112
111
  export declare function isIterable(value: unknown): value is Iterable<unknown>;
113
- export declare function atArray<T>(array: readonly T[] | undefined, index: number): T | undefined;
114
- export declare function emplaceMap<K, V>(map: Map<K, V>, key: K, handler: {
115
- insert?: (key: K, map: Map<K, V>) => V;
116
- update?: (existing: V, key: K, map: Map<K, V>) => V;
117
- }): V;
112
+ export declare function getOrInsert<K, V>(map: Map<K, V>, key: K, defaultValue: V): V;
113
+ export declare function getOrInsertComputed<K, V>(map: Map<K, V>, key: K, callbackFunction: (key: K) => V): V;
114
+ /**
115
+ * TODO: replace with ESNext Uint8Array.prototype.toHex once it's widely supported
116
+ */
117
+ export declare function bytesToHex(bytes: Uint8Array): string;
118
118
  /**
119
119
  * @returns If `val` is an non-null object or a callable object (i.e. function).
120
120
  */
121
121
  export declare function isObject(val: unknown): val is object;
122
122
  export declare function normalizeUriPathname(pathname: string): string;
123
123
  export declare function normalizeUri(uri: string): string;
124
+ export declare function getSha1(data: string | Uint8Array<ArrayBuffer>): Promise<string>;
125
+ export declare function compressBytes(bytes: Uint8Array<ArrayBuffer>, algorithm: CompressionFormat): Promise<Uint8Array<ArrayBuffer>>;
126
+ export declare function compressStream(stream: ReadableStream<Uint8Array<ArrayBuffer>>, algorithm: CompressionFormat): ReadableStream<Uint8Array<ArrayBuffer>>;
127
+ export declare function decompressBytes(bytes: Uint8Array<ArrayBuffer>, algorithm: CompressionFormat): Promise<Uint8Array<ArrayBuffer>>;
128
+ export declare function decompressStream(stream: ReadableStream<Uint8Array<ArrayBuffer>>, algorithm: CompressionFormat): ReadableStream<Uint8Array<ArrayBuffer>>;
129
+ export declare function bytesToStream(bytes: Uint8Array<ArrayBuffer>): ReadableStream<Uint8Array<ArrayBuffer>>;
130
+ export declare function streamToBytes(stream: ReadableStream<Uint8Array<ArrayBuffer>>): Promise<Uint8Array<ArrayBuffer>>;
131
+ export declare function sleep(delayMs: number): Promise<void>;
124
132
  /**
125
133
  * Return a read-write TARGET type if the INPUT type is read-write, and a
126
134
  * readonly TARGET type if the INPUT type is readonly, and `never` if the INPUT