@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.
- package/lib/common/Dev.d.ts +1 -1
- package/lib/common/EventDispatcher.d.ts +6 -0
- package/lib/common/EventDispatcher.js +15 -0
- package/lib/common/ReadonlyProxy.js +2 -2
- package/lib/common/StateProxy.js +2 -2
- package/lib/common/externals/BrowserExternals.d.ts +1 -9
- package/lib/common/externals/BrowserExternals.js +0 -59
- package/lib/common/externals/NodeJsExternals.d.ts +7 -23
- package/lib/common/externals/NodeJsExternals.js +192 -46
- package/lib/common/externals/index.d.ts +0 -17
- package/lib/common/index.d.ts +1 -0
- package/lib/common/index.js +1 -0
- package/lib/common/util.d.ts +15 -7
- package/lib/common/util.js +53 -18
- package/lib/node/StringNode.d.ts +1 -1
- package/lib/parser/string.d.ts +2 -2
- package/lib/processor/colorizer/Colorizer.d.ts +2 -2
- package/lib/service/CacheService.js +4 -5
- package/lib/service/Config.d.ts +6 -10
- package/lib/service/Config.js +3 -15
- package/lib/service/FileService.js +5 -5
- package/lib/service/FileWatcher.d.ts +9 -12
- package/lib/service/Project.d.ts +14 -27
- package/lib/service/Project.js +6 -18
- package/lib/service/fetcher.d.ts +8 -1
- package/lib/service/fetcher.js +62 -9
- package/lib/service/fileUtil.js +3 -3
- package/lib/symbol/Symbol.d.ts +10 -10
- package/lib/symbol/Symbol.js +15 -7
- package/lib/symbol/SymbolUtil.d.ts +9 -18
- package/lib/symbol/SymbolUtil.js +5 -19
- package/package.json +4 -5
package/lib/common/Dev.d.ts
CHANGED
|
@@ -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:
|
|
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 {
|
|
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
|
|
14
|
+
return getOrInsertComputed(this.map, p, () => ReadonlyProxy.create(value));
|
|
15
15
|
}
|
|
16
16
|
return value;
|
|
17
17
|
}
|
package/lib/common/StateProxy.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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 {
|
|
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
|
|
2
|
-
import
|
|
3
|
-
import type
|
|
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
|
|
5
|
-
import
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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 =
|
|
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.#
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
188
|
+
const lastModified = response.headers.get('last-modified');
|
|
189
|
+
if (!(etag || lastModified)) {
|
|
156
190
|
return;
|
|
157
191
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
#
|
|
166
|
-
|
|
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`).
|
package/lib/common/index.d.ts
CHANGED
package/lib/common/index.js
CHANGED
package/lib/common/util.d.ts
CHANGED
|
@@ -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(
|
|
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
|
|
114
|
-
export declare function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|