@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.
@@ -146,8 +146,8 @@ export function promisifyAsyncIterable(iterable, joiner) {
146
146
  return joiner(chunks);
147
147
  })();
148
148
  }
149
- export async function parseGzippedJson(externals, buffer) {
150
- return JSON.parse(bufferToString(await externals.archive.gunzip(buffer)));
149
+ export async function parseGzippedJson(bytes) {
150
+ return JSON.parse(bufferToString(await decompressBytes(bytes, 'gzip')));
151
151
  }
152
152
  /**
153
153
  * @returns Is Plain Old JavaScript Object (POJO).
@@ -218,26 +218,33 @@ export function isIterable(value) {
218
218
  return !!value[Symbol.iterator];
219
219
  }
220
220
  // #region ESNext functions polyfill
221
- export function atArray(array, index) {
222
- return index >= 0 ? array?.[index] : array?.[array.length + index];
221
+ export function getOrInsert(map, key, defaultValue) {
222
+ if (!map.has(key)) {
223
+ map.set(key, defaultValue);
224
+ }
225
+ return map.get(key);
223
226
  }
224
- export function emplaceMap(map, key, handler) {
225
- if (map.has(key)) {
226
- let value = map.get(key);
227
- if (handler.update) {
228
- value = handler.update(value, key, map);
229
- map.set(key, value);
230
- }
231
- return value;
227
+ export function getOrInsertComputed(map, key, callbackFunction) {
228
+ if (!map.has(key)) {
229
+ map.set(key, callbackFunction(key));
230
+ }
231
+ return map.get(key);
232
+ }
233
+ /**
234
+ * TODO: replace with ESNext Uint8Array.prototype.toHex once it's widely supported
235
+ */
236
+ export function bytesToHex(bytes) {
237
+ if ('Buffer' in globalThis && bytes instanceof Buffer) {
238
+ return bytes.toString('hex');
232
239
  }
233
- else if (handler.insert) {
234
- const value = handler.insert(key, map);
235
- map.set(key, value);
236
- return value;
240
+ else if ('toHex' in Uint8Array.prototype && typeof Uint8Array.prototype.toHex === 'function') {
241
+ return Uint8Array.prototype.toHex.call(bytes);
237
242
  }
238
- else {
239
- throw new Error(`No key ${key} in map and no insert handler provided`);
243
+ let ans = '';
244
+ for (const v of bytes) {
245
+ ans += v.toString(16).padStart(2, '0');
240
246
  }
247
+ return ans;
241
248
  }
242
249
  // #endregion
243
250
  /**
@@ -267,6 +274,34 @@ export function normalizeUri(uri) {
267
274
  obj.pathname = normalizeUriPathname(obj.pathname);
268
275
  return obj.toString();
269
276
  }
277
+ export async function getSha1(data) {
278
+ if (typeof data === 'string') {
279
+ data = new TextEncoder().encode(data);
280
+ }
281
+ const hash = await crypto.subtle.digest('SHA-1', data.buffer);
282
+ return bytesToHex(new Uint8Array(hash));
283
+ }
284
+ export function compressBytes(bytes, algorithm) {
285
+ return streamToBytes(compressStream(bytesToStream(bytes), algorithm));
286
+ }
287
+ export function compressStream(stream, algorithm) {
288
+ return stream.pipeThrough(new CompressionStream(algorithm));
289
+ }
290
+ export function decompressBytes(bytes, algorithm) {
291
+ return streamToBytes(decompressStream(bytesToStream(bytes), algorithm));
292
+ }
293
+ export function decompressStream(stream, algorithm) {
294
+ return stream.pipeThrough(new DecompressionStream(algorithm));
295
+ }
296
+ export function bytesToStream(bytes) {
297
+ return new Blob([bytes]).stream();
298
+ }
299
+ export function streamToBytes(stream) {
300
+ return new Response(stream).bytes();
301
+ }
302
+ export function sleep(delayMs) {
303
+ return new Promise(resolve => setTimeout(resolve, delayMs));
304
+ }
270
305
  /**
271
306
  * Checks if the numeric value of a number or bigint is the same. Undefined is **not** the same as
272
307
  * 0 for this function.
@@ -2,7 +2,7 @@ import type { Parser } from '../parser/index.js';
2
2
  import type { ColorTokenType } from '../processor/index.js';
3
3
  import type { IndexMap, RangeLike } from '../source/index.js';
4
4
  import type { AstNode } from './AstNode.js';
5
- export declare const EscapeChars: readonly ["\"", "'", "\\", "b", "f", "n", "r", "s", "t"];
5
+ export declare const EscapeChars: readonly ['"', "'", '\\', 'b', 'f', 'n', 'r', 's', 't'];
6
6
  export type EscapeChar = (typeof EscapeChars)[number];
7
7
  export declare namespace EscapeChar {
8
8
  function is(expected: EscapeChar[] | undefined, c: string): c is EscapeChar;
@@ -6,11 +6,11 @@ import type { Parser, Result, Returnable } from './Parser.js';
6
6
  export declare function string(options: StringOptions): InfallibleParser<StringNode>;
7
7
  export declare function parseStringValue<T extends Returnable>(parser: Parser<T>, value: string, map: IndexMap, ctx: ParserContext): Result<T>;
8
8
  export declare const BrigadierUnquotableCharacters: readonly ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "_", ".", "+", "-"];
9
- export declare const BrigadierUnquotableCharacterSet: Set<"0" | "k" | "v" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "." | "e" | "E" | "i" | "p" | "-" | "+" | "a" | "b" | "c" | "d" | "f" | "g" | "h" | "j" | "l" | "m" | "n" | "o" | "q" | "r" | "s" | "t" | "u" | "w" | "x" | "y" | "z" | "_" | "A" | "B" | "C" | "D" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z">;
9
+ export declare const BrigadierUnquotableCharacterSet: Set<"+" | "-" | "." | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "_" | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z">;
10
10
  export declare const BrigadierUnquotablePattern: RegExp;
11
11
  export declare const BrigadierUnquotableOption: {
12
12
  allowEmpty: boolean;
13
- allowList: Set<"0" | "k" | "v" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "." | "e" | "E" | "i" | "p" | "-" | "+" | "a" | "b" | "c" | "d" | "f" | "g" | "h" | "j" | "l" | "m" | "n" | "o" | "q" | "r" | "s" | "t" | "u" | "w" | "x" | "y" | "z" | "_" | "A" | "B" | "C" | "D" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z">;
13
+ allowList: Set<"+" | "-" | "." | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "_" | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z">;
14
14
  };
15
15
  export declare const BrigadierStringOptions: StringOptions;
16
16
  export declare const brigadierString: InfallibleParser<StringNode>;
@@ -18,7 +18,7 @@ export declare namespace ColorToken {
18
18
  function fillGap(tokens: readonly ColorToken[], targetRange: Range, type: ColorTokenType, modifiers?: ColorTokenModifier[]): ColorToken[];
19
19
  }
20
20
  export declare const ColorTokenTypes: readonly ["comment", "enum", "enumMember", "escape", "function", "keyword", "modifier", "number", "property", "string", "struct", "type", "variable", "error", "literal", "operator", "resourceLocation", "vector"];
21
- export type ColorTokenType = typeof ColorTokenTypes[number];
21
+ export type ColorTokenType = (typeof ColorTokenTypes)[number];
22
22
  export declare const ColorTokenModifiers: readonly ["declaration", "defaultLibrary", "definition", "deprecated", "documentation", "modification", "readonly"];
23
- export type ColorTokenModifier = typeof ColorTokenModifiers[number];
23
+ export type ColorTokenModifier = (typeof ColorTokenModifiers)[number];
24
24
  //# sourceMappingURL=Colorizer.d.ts.map
@@ -1,4 +1,4 @@
1
- import { bigintJsonLosslessReplacer, bigintJsonLosslessReviver, Uri } from '../common/index.js';
1
+ import { bigintJsonLosslessReplacer, bigintJsonLosslessReviver, getSha1, Uri, } from '../common/index.js';
2
2
  import { SymbolTable } from '../symbol/index.js';
3
3
  import { ArchiveUriSupporter } from './FileService.js';
4
4
  import { fileUtil } from './fileUtil.js';
@@ -36,7 +36,7 @@ export class CacheService {
36
36
  }
37
37
  try {
38
38
  // TODO: Don't update this for every single change.
39
- this.checksums.files[doc.uri] = await this.project.externals.crypto.getSha1(doc.getText());
39
+ this.checksums.files[doc.uri] = await getSha1(doc.getText());
40
40
  }
41
41
  catch (e) {
42
42
  if (!this.project.externals.error.isKind(e, 'EISDIR')) {
@@ -72,7 +72,7 @@ export class CacheService {
72
72
  async getCacheFileUri() {
73
73
  if (!this.#cacheFilePath) {
74
74
  const sortedRoots = [...this.project.projectRoots].sort();
75
- const hash = await this.project.externals.crypto.getSha1(sortedRoots.join(':'));
75
+ const hash = await getSha1(sortedRoots.join(':'));
76
76
  this.#cacheFilePath = new Uri(`symbols/${hash}.json.gz`, this.cacheRoot).toString();
77
77
  }
78
78
  return this.#cacheFilePath;
@@ -196,8 +196,7 @@ export class CacheService {
196
196
  return false;
197
197
  }
198
198
  async hasFileChangedSinceCache(doc) {
199
- return (this.checksums.files[doc.uri]
200
- !== (await this.project.externals.crypto.getSha1(doc.getText())));
199
+ return (this.checksums.files[doc.uri] !== (await getSha1(doc.getText())));
201
200
  }
202
201
  reset() {
203
202
  this.#hasValidatedFiles = false;
@@ -1,5 +1,5 @@
1
- import type { DeepPartial, ExternalEventEmitter } from '../common/index.js';
2
- import { Arrayable } from '../common/index.js';
1
+ import type { DeepPartial } from '../common/index.js';
2
+ import { Arrayable, EventDispatcher } from '../common/index.js';
3
3
  import { ErrorSeverity } from '../source/index.js';
4
4
  import type { Project } from './Project.js';
5
5
  export interface Config {
@@ -239,19 +239,15 @@ type ErrorEvent = {
239
239
  error: unknown;
240
240
  uri: string;
241
241
  };
242
- export declare class ConfigService implements ExternalEventEmitter {
243
- #private;
242
+ export declare class ConfigService extends EventDispatcher<{
243
+ changed: ConfigEvent;
244
+ error: ErrorEvent;
245
+ }> {
244
246
  private readonly project;
245
247
  private readonly defaultConfig;
246
248
  static readonly ConfigFileNames: readonly ["spyglass.json", ".spyglassrc", ".spyglassrc.json"];
247
249
  private currentEditorConfiguration;
248
250
  constructor(project: Project, defaultConfig?: Config);
249
- on(event: 'changed', callbackFn: (data: ConfigEvent) => void): this;
250
- on(event: 'error', callbackFn: (data: ErrorEvent) => void): this;
251
- once(event: 'changed', callbackFn: (data: ConfigEvent) => void): this;
252
- once(event: 'error', callbackFn: (data: ErrorEvent) => void): this;
253
- emit(event: 'changed', data: ConfigEvent): boolean;
254
- emit(event: 'error', data: ErrorEvent): boolean;
255
251
  onEditorConfigurationUpdate(editorConfiguration: PartialConfig): Promise<void>;
256
252
  load(): Promise<Config>;
257
253
  static isConfigFile(this: void, uri: string): boolean;
@@ -1,5 +1,5 @@
1
1
  import rfdc from 'rfdc';
2
- import { Arrayable, bufferToString, merge, TypePredicates } from '../common/index.js';
2
+ import { Arrayable, bufferToString, EventDispatcher, merge, TypePredicates, } from '../common/index.js';
3
3
  import { ErrorSeverity } from '../source/index.js';
4
4
  import { DataFileCategories, RegistryCategories } from '../symbol/index.js';
5
5
  export var LinterSeverity;
@@ -296,16 +296,15 @@ export var PartialConfig;
296
296
  }
297
297
  PartialConfig.buildConfigFromEditorSettingsSafe = buildConfigFromEditorSettingsSafe;
298
298
  })(PartialConfig || (PartialConfig = {}));
299
- export class ConfigService {
299
+ export class ConfigService extends EventDispatcher {
300
300
  project;
301
301
  defaultConfig;
302
302
  static ConfigFileNames = Object.freeze(['spyglass.json', '.spyglassrc', '.spyglassrc.json']);
303
- #eventEmitter;
304
303
  currentEditorConfiguration = {};
305
304
  constructor(project, defaultConfig = VanillaConfig) {
305
+ super();
306
306
  this.project = project;
307
307
  this.defaultConfig = defaultConfig;
308
- this.#eventEmitter = new project.externals.event.EventEmitter();
309
308
  const handler = async ({ uri }) => {
310
309
  if (ConfigService.isConfigFile(uri)) {
311
310
  this.emit('changed', { config: await this.load() });
@@ -315,17 +314,6 @@ export class ConfigService {
315
314
  project.on('fileModified', handler);
316
315
  project.on('fileDeleted', handler);
317
316
  }
318
- on(event, callbackFn) {
319
- this.#eventEmitter.on(event, callbackFn);
320
- return this;
321
- }
322
- once(event, callbackFn) {
323
- this.#eventEmitter.once(event, callbackFn);
324
- return this;
325
- }
326
- emit(event, ...args) {
327
- return this.#eventEmitter.emit(event, ...args);
328
- }
329
317
  async onEditorConfigurationUpdate(editorConfiguration) {
330
318
  this.currentEditorConfiguration = editorConfiguration;
331
319
  this.emit('changed', { config: await this.load() });
@@ -1,5 +1,5 @@
1
1
  /* istanbul ignore file */
2
- import { Uri } from '../common/index.js';
2
+ import { getSha1, Uri } from '../common/index.js';
3
3
  import { TwoWayMap } from '../common/TwoWayMap.js';
4
4
  import { fileUtil } from './fileUtil.js';
5
5
  export var FileService;
@@ -77,7 +77,7 @@ export class FileServiceImpl {
77
77
  try {
78
78
  let mappedUri = this.map.getKey(virtualUri);
79
79
  if (mappedUri === undefined) {
80
- mappedUri = `${this.virtualUrisRoot}${await this.externals.crypto.getSha1(virtualUri)}/${fileUtil.basename(virtualUri)}`;
80
+ mappedUri = `${this.virtualUrisRoot}${await getSha1(virtualUri)}/${fileUtil.basename(virtualUri)}`;
81
81
  // Delete old mapped file if it exists. This makes sure the
82
82
  // readonly permission on the file is not removed by it being
83
83
  // overwritten.
@@ -183,7 +183,7 @@ export class ArchiveUriSupporter {
183
183
  }
184
184
  else {
185
185
  // Hash the corresponding file.
186
- return this.externals.crypto.getSha1(this.getDataInArchive(archiveName, pathInArchive));
186
+ return getSha1(this.getDataInArchive(archiveName, pathInArchive));
187
187
  }
188
188
  }
189
189
  async readFile(uri) {
@@ -258,7 +258,7 @@ export class ArchiveUriSupporter {
258
258
  const files = await externals.archive.decompressBall(bytes, { stripLevel: dependency.stripLevel ?? 0 });
259
259
  /// Debug message for #1609
260
260
  logger.info(`[ArchiveUriSupporter#create] Extracted ${files.length} files from ${archiveName}`);
261
- const hash = await externals.crypto.getSha1(bytes);
261
+ const hash = await getSha1(bytes);
262
262
  archiveHashes.set(archiveName, hash);
263
263
  entries.set(archiveName, new Map(files.map((f) => [f.path.replace(/\\/g, '/'), f])));
264
264
  }
@@ -270,6 +270,6 @@ export class ArchiveUriSupporter {
270
270
  }
271
271
  }
272
272
  async function hashFile(externals, uri) {
273
- return externals.crypto.getSha1(await externals.fs.readFile(uri));
273
+ return getSha1(await externals.fs.readFile(uri));
274
274
  }
275
275
  //# sourceMappingURL=FileService.js.map
@@ -1,21 +1,18 @@
1
- import type { UriStore } from '../common/index.js';
1
+ import type { EventDispatcher, UriStore } from '../common/index.js';
2
+ export type FileWatcherEventMap = {
3
+ ready: void;
4
+ add: string;
5
+ change: string;
6
+ unlink: string;
7
+ error: Error;
8
+ };
2
9
  /**
3
10
  * A file watcher that reports additions, changes, and deletions of files.
4
11
  * Changes to directories should not be reported.
5
12
  */
6
- export interface FileWatcher {
13
+ export interface FileWatcher extends EventDispatcher<FileWatcherEventMap> {
7
14
  get watchedFiles(): UriStore;
8
15
  ready(): Promise<void>;
9
- on(eventName: 'ready', listener: () => unknown): this;
10
- once(eventName: 'ready', listener: () => unknown): this;
11
- on(eventName: 'add', listener: (uri: string) => unknown): this;
12
- once(eventName: 'add', listener: (uri: string) => unknown): this;
13
- on(eventName: 'change', listener: (uri: string) => unknown): this;
14
- once(eventName: 'change', listener: (uri: string) => unknown): this;
15
- on(eventName: 'unlink', listener: (uri: string) => unknown): this;
16
- once(eventName: 'unlink', listener: (uri: string) => unknown): this;
17
- on(eventName: 'error', listener: (error: Error) => unknown): this;
18
- once(eventName: 'error', listener: (error: Error) => unknown): this;
19
16
  close(): Promise<void>;
20
17
  }
21
18
  //# sourceMappingURL=FileWatcher.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import type { TextDocumentContentChangeEvent } from 'vscode-languageserver-textdocument';
2
2
  import { TextDocument } from 'vscode-languageserver-textdocument';
3
- import type { ExternalEventEmitter, Externals } from '../common/index.js';
4
- import { Logger, UriStore } from '../common/index.js';
3
+ import type { Externals } from '../common/index.js';
4
+ import { EventDispatcher, Logger, UriStore } from '../common/index.js';
5
5
  import type { AstNode } from '../node/index.js';
6
6
  import { FileNode } from '../node/index.js';
7
7
  import type { PosRangeLanguageError } from '../source/index.js';
@@ -100,7 +100,18 @@ export type ProjectData = Pick<Project, 'cacheRoot' | 'config' | 'ensureBindingS
100
100
  *
101
101
  * After the READY process is complete, editing text documents as signaled by the client or the file watcher results in the file being re-processed.
102
102
  */
103
- export declare class Project implements ExternalEventEmitter {
103
+ export declare class Project extends EventDispatcher<{
104
+ documentErrored: DocumentErrorEvent;
105
+ documentUpdated: DocumentEvent;
106
+ documentRemoved: FileEvent;
107
+ fileCreated: FileEvent;
108
+ fileModified: FileEvent;
109
+ fileDeleted: FileEvent;
110
+ ready: EmptyEvent;
111
+ rootsUpdated: RootsEvent;
112
+ symbolRegistrarExecuted: SymbolRegistrarEvent;
113
+ configChanged: ConfigChangeEvent;
114
+ }> {
104
115
  #private;
105
116
  private static readonly RootSuffix;
106
117
  readonly cacheService: CacheService;
@@ -134,30 +145,6 @@ export declare class Project implements ExternalEventEmitter {
134
145
  */
135
146
  get cacheRoot(): RootUriString;
136
147
  private updateRoots;
137
- on(event: 'documentErrored', callbackFn: (data: DocumentErrorEvent) => void): this;
138
- on(event: 'documentUpdated', callbackFn: (data: DocumentEvent) => void): this;
139
- on(event: 'documentRemoved', callbackFn: (data: FileEvent) => void): this;
140
- on(event: `file${'Created' | 'Modified' | 'Deleted'}`, callbackFn: (data: FileEvent) => void): this;
141
- on(event: 'ready', callbackFn: (data: EmptyEvent) => void): this;
142
- on(event: 'rootsUpdated', callbackFn: (data: RootsEvent) => void): this;
143
- on(event: 'symbolRegistrarExecuted', callbackFn: (data: SymbolRegistrarEvent) => void): this;
144
- on(event: 'configChanged', callbackFn: (data: ConfigChangeEvent) => void): this;
145
- once(event: 'documentErrored', callbackFn: (data: DocumentErrorEvent) => void): this;
146
- once(event: 'documentUpdated', callbackFn: (data: DocumentEvent) => void): this;
147
- once(event: 'documentRemoved', callbackFn: (data: FileEvent) => void): this;
148
- once(event: `file${'Created' | 'Modified' | 'Deleted'}`, callbackFn: (data: FileEvent) => void): this;
149
- once(event: 'ready', callbackFn: (data: EmptyEvent) => void): this;
150
- once(event: 'rootsUpdated', callbackFn: (data: RootsEvent) => void): this;
151
- once(event: 'symbolRegistrarExecuted', callbackFn: (data: SymbolRegistrarEvent) => void): this;
152
- once(event: 'configChanged', callbackFn: (data: ConfigChangeEvent) => void): this;
153
- emit(event: 'documentErrored', data: DocumentErrorEvent): boolean;
154
- emit(event: 'documentUpdated', data: DocumentEvent): boolean;
155
- emit(event: 'documentRemoved', data: FileEvent): boolean;
156
- emit(event: `file${'Created' | 'Modified' | 'Deleted'}`, data: FileEvent): boolean;
157
- emit(event: 'ready', data: EmptyEvent): boolean;
158
- emit(event: 'rootsUpdated', data: RootsEvent): boolean;
159
- emit(event: 'symbolRegistrarExecuted', data: SymbolRegistrarEvent): boolean;
160
- emit(event: 'configChanged', data: ConfigChangeEvent): boolean;
161
148
  /**
162
149
  * Get all files that are tracked and supported.
163
150
  *
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import picomatch from 'picomatch';
8
8
  import { TextDocument } from 'vscode-languageserver-textdocument';
9
- import { bufferToString, Logger, normalizeUri, SingletonPromise, StateProxy, UriStore, } from '../common/index.js';
9
+ import { bufferToString, EventDispatcher, Logger, normalizeUri, SingletonPromise, StateProxy, UriStore, } from '../common/index.js';
10
10
  import { FileNode } from '../node/index.js';
11
11
  import { file } from '../parser/index.js';
12
12
  import { traversePreOrder } from '../processor/index.js';
@@ -60,7 +60,7 @@ const CacheAutoSaveInterval = 600_000; // 10 Minutes.
60
60
  *
61
61
  * After the READY process is complete, editing text documents as signaled by the client or the file watcher results in the file being re-processed.
62
62
  */
63
- export class Project {
63
+ export class Project extends EventDispatcher {
64
64
  static RootSuffix = '/pack.mcmeta';
65
65
  /** Prevent circular binding. */
66
66
  #bindingInProgressUris = new Set();
@@ -71,7 +71,6 @@ export class Project {
71
71
  #clientManagedDocAndNodes = new Map();
72
72
  #configService;
73
73
  #symbolUpToDateUris = new Set();
74
- #eventEmitter;
75
74
  #initializers;
76
75
  #watcher;
77
76
  get watchedFiles() {
@@ -134,17 +133,6 @@ export class Project {
134
133
  this.#roots = [...ans].sort((a, b) => b.length - a.length);
135
134
  this.emit('rootsUpdated', { roots: this.#roots });
136
135
  }
137
- on(event, callbackFn) {
138
- this.#eventEmitter.on(event, callbackFn);
139
- return this;
140
- }
141
- once(event, callbackFn) {
142
- this.#eventEmitter.once(event, callbackFn);
143
- return this;
144
- }
145
- emit(event, ...args) {
146
- return this.#eventEmitter.emit(event, ...args);
147
- }
148
136
  /**
149
137
  * Get all files that are tracked and supported.
150
138
  *
@@ -157,8 +145,8 @@ export class Project {
157
145
  return supportedFiles;
158
146
  }
159
147
  constructor({ cacheRoot, defaultConfig, externals, fs = FileService.create(externals, cacheRoot), initializers = [], isDebugging = false, logger = Logger.create(), profilers = ProfilerFactory.noop(), projectRoots, }) {
148
+ super();
160
149
  this.#cacheRoot = cacheRoot;
161
- this.#eventEmitter = new externals.event.EventEmitter();
162
150
  this.externals = externals;
163
151
  this.fs = fs;
164
152
  this.#initializers = initializers;
@@ -168,7 +156,7 @@ export class Project {
168
156
  this.projectRoots = projectRoots;
169
157
  this.cacheService = new CacheService(cacheRoot, this);
170
158
  this.#configService = new ConfigService(this, defaultConfig);
171
- this.symbols = new SymbolUtil({}, externals.event.EventEmitter);
159
+ this.symbols = new SymbolUtil({});
172
160
  this.#ctx = {};
173
161
  this.logger.info(`[Project] [init] cacheRoot = ${cacheRoot}`);
174
162
  this.logger.info(`[Project] [init] projectRoots = ${projectRoots.join(' ')}`);
@@ -250,7 +238,7 @@ export class Project {
250
238
  };
251
239
  const __profiler = this.profilers.get('project#init');
252
240
  const { symbols } = await this.cacheService.load();
253
- this.symbols = new SymbolUtil(symbols, this.externals.event.EventEmitter);
241
+ this.symbols = new SymbolUtil(symbols);
254
242
  this.symbols.buildCache();
255
243
  __profiler.task('Load Cache');
256
244
  this.config = await this.#configService.load();
@@ -425,7 +413,7 @@ export class Project {
425
413
  }
426
414
  // Reset cache.
427
415
  const { symbols } = this.cacheService.reset();
428
- this.symbols = new SymbolUtil(symbols, this.externals.event.EventEmitter);
416
+ this.symbols = new SymbolUtil(symbols);
429
417
  this.symbols.buildCache();
430
418
  return this.restart();
431
419
  }
@@ -1,3 +1,10 @@
1
1
  import type { Externals, Logger } from '../common/index.js';
2
- export declare function fetchWithCache({ web }: Externals, logger: Logger, input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
2
+ export interface FetcherOptions {
3
+ retryBaseMs?: number;
4
+ retryMaxAttempts?: number;
5
+ perTryTimeoutMs?: number;
6
+ totalTimeoutMs?: number;
7
+ }
8
+ export declare function fetchWithCache({ web }: Externals, logger: Logger, input: RequestInfo | URL, init?: RequestInit, options?: FetcherOptions): Promise<Response>;
9
+ export declare function isStaleFetcherResponse(response: Response): boolean;
3
10
  //# sourceMappingURL=fetcher.d.ts.map
@@ -1,16 +1,24 @@
1
- import { Dev } from '../common/index.js';
2
- const FETCH_TIMEOUT_MS = 30_000;
3
- export async function fetchWithCache({ web }, logger, input, init) {
1
+ import { Dev, sleep } from '../common/index.js';
2
+ const FETCH_RETRY_BASE_MS = 1_000;
3
+ const FETCH_RETRY_MAX_ATTEMPTS = 3;
4
+ const FETCH_PER_TRY_TIMEOUT_MS = 10_000;
5
+ const FETCH_TOTAL_TIMEOUT_MS = 15_000;
6
+ const STALE_HEADER = 'spyglassmc-is-stale';
7
+ export async function fetchWithCache({ web }, logger, input, init, options) {
4
8
  const cache = await web.getCache();
5
9
  const request = new Request(input, init);
6
10
  const cachedResponse = await cache.match(request);
7
- const cachedEtag = cachedResponse?.headers.get('ETag');
11
+ const cachedEtag = cachedResponse?.headers.get('etag');
12
+ const cachedLastModified = cachedResponse?.headers.get('last-modified');
8
13
  if (cachedEtag) {
9
- request.headers.set('If-None-Match', cachedEtag);
14
+ request.headers.set('if-none-match', cachedEtag);
10
15
  }
11
- request.headers.set('User-Agent', 'SpyglassMC (+https://spyglassmc.com)');
16
+ else if (cachedLastModified) {
17
+ request.headers.set('if-modified-since', cachedLastModified);
18
+ }
19
+ request.headers.set('user-agent', 'SpyglassMC (+https://spyglassmc.com)');
12
20
  try {
13
- const response = await web.fetch(request, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
21
+ const response = await fetchWithRetries(request, options);
14
22
  if (response.status === 304) {
15
23
  Dev.assertDefined(cachedResponse);
16
24
  logger.info(`[fetchWithCache] reusing cache for ${request.url}`);
@@ -19,7 +27,7 @@ export async function fetchWithCache({ web }, logger, input, init) {
19
27
  else if (!response.ok) {
20
28
  let message = response.statusText;
21
29
  try {
22
- message = (await response.json()).message;
30
+ message = await response.text();
23
31
  }
24
32
  catch { }
25
33
  throw new TypeError(`${response.status} ${message}`);
@@ -32,6 +40,12 @@ export async function fetchWithCache({ web }, logger, input, init) {
32
40
  catch (e) {
33
41
  logger.warn('[fetchWithCache] put cache', e);
34
42
  }
43
+ try {
44
+ await cachedResponse?.body?.cancel();
45
+ }
46
+ catch (e) {
47
+ logger.warn('[fetchWithCache] failed cancelling cachedResponse body stream', e);
48
+ }
35
49
  return response;
36
50
  }
37
51
  }
@@ -39,10 +53,49 @@ export async function fetchWithCache({ web }, logger, input, init) {
39
53
  logger.warn('[fetchWithCache] fetch', e);
40
54
  if (cachedResponse) {
41
55
  logger.info(`[fetchWithCache] falling back to cache for ${request.url}`);
42
- return cachedResponse;
56
+ // Set the stale header when fallback is used
57
+ const newHeaders = new Headers(cachedResponse.headers);
58
+ newHeaders.set(STALE_HEADER, '1');
59
+ return new Response(cachedResponse.body, {
60
+ status: cachedResponse.status,
61
+ statusText: cachedResponse.statusText,
62
+ headers: newHeaders,
63
+ });
43
64
  }
44
65
  throw e;
45
66
  }
46
67
  }
68
+ export function isStaleFetcherResponse(response) {
69
+ return response.headers.has(STALE_HEADER);
70
+ }
71
+ async function fetchWithRetries(request, { perTryTimeoutMs = FETCH_PER_TRY_TIMEOUT_MS, retryBaseMs = FETCH_RETRY_BASE_MS, retryMaxAttempts = FETCH_RETRY_MAX_ATTEMPTS, totalTimeoutMs = FETCH_TOTAL_TIMEOUT_MS, } = {}) {
72
+ let lastResult;
73
+ const totalSignal = AbortSignal.timeout(totalTimeoutMs);
74
+ Dev.assertTrue(retryMaxAttempts > 0, 'Number of attempts must be greater than 0');
75
+ for (let i = 0; i < retryMaxAttempts; i++) {
76
+ const isLastAttempt = i === retryMaxAttempts - 1;
77
+ try {
78
+ lastResult = await fetch(request.clone(), {
79
+ signal: AbortSignal.any([
80
+ totalSignal,
81
+ AbortSignal.timeout(perTryTimeoutMs),
82
+ ]),
83
+ });
84
+ if (lastResult.status < 500) {
85
+ return lastResult;
86
+ }
87
+ }
88
+ catch (e) {
89
+ lastResult = e;
90
+ }
91
+ if (!isLastAttempt) {
92
+ await sleep(Math.round(Math.random() * (2 ** i) * retryBaseMs));
93
+ }
94
+ }
95
+ if (lastResult instanceof Response) {
96
+ return lastResult;
97
+ }
98
+ throw lastResult;
99
+ }
47
100
  // Fetchr? I hardly know her: https://github.com/NeunEinser/bingo
48
101
  //# sourceMappingURL=fetcher.js.map
@@ -1,4 +1,4 @@
1
- import { bigintJsonNumberReplacer, bigintJsonNumberReviver, bufferToString, Uri, } from '../common/index.js';
1
+ import { bigintJsonNumberReplacer, bigintJsonNumberReviver, bufferToString, compressBytes, decompressBytes, Uri, } from '../common/index.js';
2
2
  export var fileUtil;
3
3
  (function (fileUtil) {
4
4
  /**
@@ -245,7 +245,7 @@ export var fileUtil;
245
245
  * @throws
246
246
  */
247
247
  async function readGzippedFile(externals, path) {
248
- return externals.archive.gunzip(await readFile(externals, path));
248
+ return decompressBytes(await readFile(externals, path), 'gzip');
249
249
  }
250
250
  fileUtil.readGzippedFile = readGzippedFile;
251
251
  /**
@@ -255,7 +255,7 @@ export var fileUtil;
255
255
  if (typeof buffer === 'string') {
256
256
  buffer = new TextEncoder().encode(buffer);
257
257
  }
258
- return writeFile(externals, path, await externals.archive.gzip(buffer));
258
+ return writeFile(externals, path, await compressBytes(buffer, 'gzip'));
259
259
  }
260
260
  fileUtil.writeGzippedFile = writeGzippedFile;
261
261
  /**