diff --git a/package.json b/package.json index 1eddeecb9a..f905a612f2 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "safe-stable-stringify": "^2.3.1", "secure-json-parse": "^2.4.0", "sorted-btree": "^1.8.1", - "thingies": "^1.11.1", + "thingies": "^1.14.1", "tinybench": "^2.4.0", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", diff --git a/src/json-crdt/__demos__/getting-started.ts b/src/json-crdt/__demos__/getting-started.ts index 5f57e081a1..fea94c14d8 100644 --- a/src/json-crdt/__demos__/getting-started.ts +++ b/src/json-crdt/__demos__/getting-started.ts @@ -10,7 +10,7 @@ import {Model, n} from '..'; import {vec} from '../../json-crdt-patch'; // Create a new JSON CRDT document, 1234 is the session ID. -const model = Model.withLogicalClock(1234) as Model< +const model = Model.withLogicalClock(1234) as any as Model< n.obj<{ counter: n.val>; text: n.str; diff --git a/src/json-crdt/__demos__/type-safety.ts b/src/json-crdt/__demos__/type-safety.ts index 31981dac2d..a75346d9d4 100644 --- a/src/json-crdt/__demos__/type-safety.ts +++ b/src/json-crdt/__demos__/type-safety.ts @@ -10,7 +10,7 @@ import {Model, n} from '..'; console.clear(); -const model = Model.withLogicalClock(1234) as Model< +const model = Model.withLogicalClock(1234) as any as Model< n.obj<{ num: n.con; text: n.str; diff --git a/src/json-crdt/codec/indexed/binary/Encoder.ts b/src/json-crdt/codec/indexed/binary/Encoder.ts index e9cf90a6eb..01b158f1dd 100644 --- a/src/json-crdt/codec/indexed/binary/Encoder.ts +++ b/src/json-crdt/codec/indexed/binary/Encoder.ts @@ -15,7 +15,7 @@ export class Encoder { this.enc = new CborEncoder(writer || new CrdtWriter()); } - public encode(doc: Model, clockTable: ClockTable = ClockTable.from(doc.clock)): IndexedFields { + public encode(doc: Model, clockTable: ClockTable = ClockTable.from(doc.clock)): IndexedFields { this.clockTable = clockTable; const writer = this.enc.writer; writer.reset(); diff --git a/src/json-crdt/codec/structural/compact/Encoder.ts b/src/json-crdt/codec/structural/compact/Encoder.ts index cd0219b230..7ce90b1daa 100644 --- a/src/json-crdt/codec/structural/compact/Encoder.ts +++ b/src/json-crdt/codec/structural/compact/Encoder.ts @@ -11,7 +11,7 @@ export class Encoder { protected clock?: ClockEncoder; protected model!: Model; - public encode(model: Model): t.JsonCrdtCompactDocument { + public encode(model: Model): t.JsonCrdtCompactDocument { this.model = model; const isServerTime = model.clock.sid === SESSION.SERVER; const clock = model.clock; diff --git a/src/json-crdt/codec/structural/verbose/Encoder.ts b/src/json-crdt/codec/structural/verbose/Encoder.ts index ae3b911a5d..34231ad036 100644 --- a/src/json-crdt/codec/structural/verbose/Encoder.ts +++ b/src/json-crdt/codec/structural/verbose/Encoder.ts @@ -8,7 +8,7 @@ import type * as types from './types'; export class Encoder { protected model!: Model; - public encode(model: Model): types.JsonCrdtVerboseDocument { + public encode(model: Model): types.JsonCrdtVerboseDocument { this.model = model; const clock = model.clock; const isServerClock = clock.sid === SESSION.SERVER; diff --git a/src/json-crdt/model/Model.ts b/src/json-crdt/model/Model.ts index aeb134dcaa..034f013f91 100644 --- a/src/json-crdt/model/Model.ts +++ b/src/json-crdt/model/Model.ts @@ -37,7 +37,7 @@ export const enum ModelChangeType { * In instance of Model class represents the underlying data structure, * i.e. model, of the JSON CRDT document. */ -export class Model implements Printable { +export class Model implements Printable { /** * Create a CRDT model which uses logical clock. Logical clock assigns a * logical timestamp to every node and operation. Logical timestamp consists @@ -85,7 +85,7 @@ export class Model implements Printabl * so that the JSON document does not necessarily need to be an object. The * JSON document can be any JSON value. */ - public root: RootNode = new RootNode(this, ORIGIN); + public root: RootNode = new RootNode(this, ORIGIN); /** * Clock that keeps track of logical timestamps of the current editing session @@ -115,13 +115,13 @@ export class Model implements Printabl } /** @ignore */ - private _api?: ModelApi; + private _api?: ModelApi; /** * API for applying local changes to the current document. */ - public get api(): ModelApi { - if (!this._api) this._api = new ModelApi(this); + public get api(): ModelApi { + if (!this._api) this._api = new ModelApi(this); return this._api; } @@ -302,11 +302,11 @@ export class Model implements Printabl * @param sessionId Session ID to use for the new model. * @returns A copy of this model with a new session ID. */ - public fork(sessionId: number = randomSessionId()): Model { - const copy = Model.fromBinary(this.toBinary()); + public fork(sessionId: number = randomSessionId()): Model { + const copy = Model.fromBinary(this.toBinary()) as unknown as Model; if (copy.clock.sid !== sessionId && copy.clock instanceof ClockVector) copy.clock = copy.clock.fork(sessionId); copy.ext = this.ext; - return copy as Model; + return copy; } /** @@ -314,17 +314,17 @@ export class Model implements Printabl * * @returns A copy of this model with the same session ID. */ - public clone(): Model { + public clone(): Model { return this.fork(this.clock.sid); } /** * Resets the model to equivalent state of another model. */ - public reset(to: Model): void { + public reset(to: Model): void { this.index = new AvlMap(compare); const blob = to.toBinary(); - decoder.decode(blob, this); + decoder.decode(blob, this); this.clock = to.clock.clone(); this.ext = to.ext.clone(); const api = this._api; @@ -337,7 +337,7 @@ export class Model implements Printabl * * @returns JSON/CBOR of the model. */ - public view(): Readonly> { + public view(): Readonly> { return this.root.view(); } @@ -386,7 +386,6 @@ export class Model implements Printabl ); }, nl, - // (tab) => `View ${toTree(this.view(), tab)}`, (tab) => `view${printTree(tab, [(tab) => String(JSON.stringify(this.view(), null, 2)).replace(/\n/g, '\n' + tab)])}`, nl, diff --git a/src/json-crdt/model/__tests__/Model.events.spec.ts b/src/json-crdt/model/__tests__/Model.events.spec.ts index f7bcf4e6d6..939e8bcb13 100644 --- a/src/json-crdt/model/__tests__/Model.events.spec.ts +++ b/src/json-crdt/model/__tests__/Model.events.spec.ts @@ -1,3 +1,4 @@ +import {PatchBuilder} from '../../../json-crdt-patch'; import {Model, ModelChangeType} from '../Model'; describe('DOM Level 0, .onchange event system', () => { @@ -8,13 +9,17 @@ describe('DOM Level 0, .onchange event system', () => { cnt++; }; expect(cnt).toBe(0); - model.api.root({foo: 'bar'}); + const builder = new PatchBuilder(model.clock.clone()); + const objId = builder.json({foo: 123}); + builder.root(objId); + model.applyPatch(builder.flush()); expect(cnt).toBe(1); - model.api.obj([]).set({hello: 123}); + builder.insObj(objId, [['hello', builder.const(456)]]); + model.applyPatch(builder.flush()); expect(cnt).toBe(2); expect(model.view()).toStrictEqual({ - foo: 'bar', - hello: 123, + foo: 123, + hello: 456, }); }); @@ -25,9 +30,12 @@ describe('DOM Level 0, .onchange event system', () => { cnt++; }; expect(cnt).toBe(0); - model.api.root({foo: 123}); + const builder = new PatchBuilder(model.clock.clone()); + builder.root(builder.json({foo: 123})); + model.applyPatch(builder.flush()); expect(cnt).toBe(1); - model.api.obj([]).set({foo: 123}); + builder.root(builder.json({foo: 123})); + model.applyPatch(builder.flush()); expect(cnt).toBe(2); }); @@ -38,9 +46,13 @@ describe('DOM Level 0, .onchange event system', () => { cnt++; }; expect(cnt).toBe(0); - model.api.root({foo: 123}); + const builder = new PatchBuilder(model.clock.clone()); + const objId = builder.json({foo: 123}); + builder.root(objId); + model.applyPatch(builder.flush()); expect(cnt).toBe(1); - model.api.obj([]).set({foo: undefined}); + builder.insObj(objId, [['foo', builder.const(undefined)]]); + model.applyPatch(builder.flush()); expect(cnt).toBe(2); expect(model.view()).toStrictEqual({}); }); @@ -52,9 +64,13 @@ describe('DOM Level 0, .onchange event system', () => { cnt++; }; expect(cnt).toBe(0); - model.api.root({foo: 123}); + const builder = new PatchBuilder(model.clock.clone()); + const objId = builder.json({foo: 123}); + builder.root(objId); + model.applyPatch(builder.flush()); expect(cnt).toBe(1); - model.api.obj([]).set({bar: undefined}); + builder.insObj(objId, [['bar', builder.const(undefined)]]); + model.applyPatch(builder.flush()); expect(cnt).toBe(2); expect(model.view()).toStrictEqual({foo: 123}); }); @@ -66,30 +82,20 @@ describe('DOM Level 0, .onchange event system', () => { cnt++; }; expect(cnt).toBe(0); - model.api.root({foo: 123}); + const builder = new PatchBuilder(model.clock.clone()); + const objId = builder.json({foo: 123}); + builder.root(objId); + model.applyPatch(builder.flush()); expect(cnt).toBe(1); - model.api.root(123); + builder.root(builder.json(123)); + model.applyPatch(builder.flush()); expect(cnt).toBe(2); - model.api.root('asdf'); + builder.root(builder.json('asdf')); + model.applyPatch(builder.flush()); expect(cnt).toBe(3); }); describe('event types', () => { - it('should trigger the onchange event with a LOCAL event type', () => { - const model = Model.withLogicalClock(); - let cnt = 0; - model.onchange = (type) => { - expect(type).toBe(ModelChangeType.LOCAL); - cnt++; - }; - expect(cnt).toBe(0); - model.api.root({foo: 123}); - expect(cnt).toBe(1); - model.api.obj([]).set({foo: 55}); - expect(cnt).toBe(2); - expect(model.view()).toStrictEqual({foo: 55}); - }); - it('should trigger the onchange event with a REMOTE event type', () => { const model = Model.withLogicalClock(); let cnt = 0; @@ -97,7 +103,7 @@ describe('DOM Level 0, .onchange event system', () => { expect(type).toBe(ModelChangeType.REMOTE); cnt++; }; - const builder = model.api.builder; + const builder = new PatchBuilder(model.clock.clone()); builder.root(builder.json({foo: 123})); const patch = builder.flush(); expect(cnt).toBe(0); diff --git a/src/json-crdt/model/__tests__/Model.types.spec.ts b/src/json-crdt/model/__tests__/Model.types.spec.ts index 5d9ce0170a..7bda69de02 100644 --- a/src/json-crdt/model/__tests__/Model.types.spec.ts +++ b/src/json-crdt/model/__tests__/Model.types.spec.ts @@ -2,7 +2,7 @@ import {Model} from '../Model'; import type {ConNode, ObjNode, StrNode} from '../../nodes'; test('can add TypeScript types to Model view', () => { - const model = Model.withLogicalClock() as Model< + const model = Model.withLogicalClock() as any as Model< ObjNode<{ foo: StrNode; bar: ConNode; diff --git a/src/json-crdt/model/api/ModelApi.ts b/src/json-crdt/model/api/ModelApi.ts index a5180efbb1..dba1b8baed 100644 --- a/src/json-crdt/model/api/ModelApi.ts +++ b/src/json-crdt/model/api/ModelApi.ts @@ -1,10 +1,12 @@ +import {FanOut} from 'thingies/es2020/fanout'; import {VecNode, ConNode, ObjNode, ArrNode, BinNode, StrNode, ValNode} from '../../nodes'; import {ApiPath, ArrApi, BinApi, ConApi, NodeApi, ObjApi, StrApi, VecApi, ValApi} from './nodes'; import {Emitter} from '../../../util/events/Emitter'; import {Patch} from '../../../json-crdt-patch/Patch'; import {PatchBuilder} from '../../../json-crdt-patch/PatchBuilder'; import {ModelChangeType, type Model} from '../Model'; -import type {JsonNode} from '../../nodes'; +import {SyncStore} from '../../../util/events/sync-store'; +import type {JsonNode, JsonNodeView} from '../../nodes'; export interface ModelApiEvents { /** @@ -26,7 +28,7 @@ export interface ModelApiEvents { * * @category Local API */ -export class ModelApi { +export class ModelApi implements SyncStore> { /** * Patch builder for the local changes. */ @@ -42,15 +44,19 @@ export class ModelApi { /** * @param model Model instance on which the API operates. */ - constructor(public readonly model: Model) { + constructor(public readonly model: Model) { this.builder = new PatchBuilder(this.model.clock); + this.model.onchange = this.queueChange; } + public readonly changes = new FanOut(); + /** @ignore */ private queuedChanges: undefined | Set = undefined; - /** @ignore */ + /** @ignore @deprecated */ private readonly queueChange = (changeType: ModelChangeType): void => { + this.changes.emit(changeType); let changesQueued = this.queuedChanges; if (changesQueued) { changesQueued.add(changeType); @@ -67,19 +73,16 @@ export class ModelApi { }); }; - /** @ignore */ - private et: undefined | Emitter = undefined; + /** @ignore @deprecated */ + private et: Emitter = new Emitter(); /** * Event target for listening to {@link Model} changes. + * + * @deprecated */ public get events(): Emitter { - let et = this.et; - if (!et) { - this.et = et = new Emitter(); - this.model.onchange = this.queueChange; - } - return et; + return this.et; } /** @@ -277,4 +280,14 @@ export class ModelApi { this.events.emit(event); return patch; } + + // ---------------------------------------------------------------- SyncStore + + public readonly subscribe = (callback: () => void) => { + const listener = () => callback(); + this.events.on('change', listener); + return () => this.events.off('change', listener); + }; + + public readonly getSnapshot = () => this.view() as any; } diff --git a/src/json-crdt/model/api/__tests__/ModelApi.events.spec.ts b/src/json-crdt/model/api/__tests__/ModelApi.events.spec.ts index d6ec2fba9d..066867e27d 100644 --- a/src/json-crdt/model/api/__tests__/ModelApi.events.spec.ts +++ b/src/json-crdt/model/api/__tests__/ModelApi.events.spec.ts @@ -196,3 +196,42 @@ describe('DOM Level 2 events, .et.addEventListener()', () => { expect(set!.has(ModelChangeType.RESET)).toBe(true); }); }); + +describe('fanout', () => { + describe('changes', () => { + test('emits events on document change', async () => { + const doc = Model.withLogicalClock(); + const api = doc.api; + let cnt = 0; + api.root({a: {}}); + expect(cnt).toBe(0); + api.changes.listen(() => { + cnt++; + }); + api.obj([]).set({gg: true}); + await Promise.resolve(); + expect(cnt).toBe(1); + api.obj(['a']).set({1: 1, 2: 2}); + await Promise.resolve(); + expect(cnt).toBe(2); + }); + + test('can have multiple subscribers', async () => { + const doc = Model.withLogicalClock(); + const api = doc.api; + let cnt = 0; + api.root({a: {}}); + expect(cnt).toBe(0); + api.changes.listen(() => { + cnt++; + }); + api.changes.listen(() => { + cnt++; + }); + expect(cnt).toBe(0); + api.obj([]).set({gg: true}); + await Promise.resolve(); + expect(cnt).toBe(2); + }); + }); +}); diff --git a/src/json-crdt/model/api/__tests__/ModelApi.proxy.spec.ts b/src/json-crdt/model/api/__tests__/ModelApi.proxy.spec.ts index 8e85f0870f..053647d147 100644 --- a/src/json-crdt/model/api/__tests__/ModelApi.proxy.spec.ts +++ b/src/json-crdt/model/api/__tests__/ModelApi.proxy.spec.ts @@ -4,7 +4,7 @@ import {ConNode, RootNode, VecNode, ObjNode, StrNode} from '../../../nodes'; import {vec} from '../../../../json-crdt-patch'; test('proxy API supports object types', () => { - const model = Model.withLogicalClock() as Model< + const model = Model.withLogicalClock() as any as Model< ObjNode<{ foo: StrNode; bar: ConNode; @@ -15,7 +15,7 @@ test('proxy API supports object types', () => { bar: 1234, }); const root = model.api.r.proxy(); - const rootApi: ValApi = root.toApi(); + const rootApi = root.toApi(); expect(rootApi).toBeInstanceOf(ValApi); expect(rootApi.node).toBeInstanceOf(RootNode); expect(rootApi.view()).toStrictEqual({ @@ -50,7 +50,7 @@ describe('supports all node types', () => { }>; vec: VecNode<[StrNode]>; }>; - const model = Model.withLogicalClock() as Model; + const model = Model.withLogicalClock() as any as Model; const data = { obj: { str: 'asdf', diff --git a/src/json-crdt/model/api/__tests__/StringApi.spec.ts b/src/json-crdt/model/api/__tests__/StringApi.spec.ts index 3aa2dfb351..fab97a677c 100644 --- a/src/json-crdt/model/api/__tests__/StringApi.spec.ts +++ b/src/json-crdt/model/api/__tests__/StringApi.spec.ts @@ -63,4 +63,53 @@ describe('events', () => { await Promise.resolve(); expect(cnt).toEqual(1); }); + + describe('.changes', () => { + test('can listen to events', async () => { + const doc = Model.withLogicalClock(); + const api = doc.api; + api.root(''); + const str = api.str([]); + let cnt = 0; + const onView = () => cnt++; + str.ins(0, 'aaa'); + expect(cnt).toEqual(0); + const unsubscribe = str.events.changes.listen(onView); + str.ins(0, 'bbb'); + await Promise.resolve(); + expect(cnt).toEqual(1); + str.ins(0, 'ccc'); + await Promise.resolve(); + expect(cnt).toEqual(2); + unsubscribe(); + str.del(1, 7); + expect(cnt).toEqual(2); + }); + }); + + describe('SyncStore', () => { + test('can listen to events', async () => { + const doc = Model.withLogicalClock(); + const api = doc.api; + api.root(''); + const str = api.str([]); + let cnt = 0; + const onView = () => cnt++; + str.ins(0, 'aaa'); + expect(cnt).toEqual(0); + expect(str.events.getSnapshot()).toEqual('aaa'); + const unsubscribe = str.events.subscribe(onView); + str.ins(0, 'bbb'); + await Promise.resolve(); + expect(str.events.getSnapshot()).toEqual('bbbaaa'); + expect(cnt).toEqual(1); + str.ins(0, 'ccc'); + await Promise.resolve(); + expect(str.events.getSnapshot()).toEqual('cccbbbaaa'); + expect(cnt).toEqual(2); + unsubscribe(); + str.del(1, 7); + expect(cnt).toEqual(2); + }); + }); }); diff --git a/src/json-crdt/model/api/events/NodeEvents.ts b/src/json-crdt/model/api/events/NodeEvents.ts index fa9b48d6a4..190161c4e1 100644 --- a/src/json-crdt/model/api/events/NodeEvents.ts +++ b/src/json-crdt/model/api/events/NodeEvents.ts @@ -1,5 +1,8 @@ +import {FanOut, FanOutListener, FanOutUnsubscribe} from 'thingies/es2020/fanout'; import {Emitter} from '../../../../util/events/Emitter'; import type {NodeApi} from '../nodes'; +import type {JsonNode, JsonNodeView} from '../../../nodes'; +import type {SyncStore, SyncStoreUnsubscribe} from '../../../../util/events/sync-store'; export interface NodeEventMap { /** @@ -17,9 +20,46 @@ export interface NodeEventMap { view: CustomEvent; } -export class NodeEvents extends Emitter { - constructor(private readonly api: NodeApi) { +class ChangesFanOut extends FanOut> { + private _v: JsonNodeView | undefined = undefined; + private _u: FanOutUnsubscribe | undefined = undefined; + + constructor(private readonly api: NodeApi) { + super(); + } + + public listen(listener: FanOutListener>) { + if (!this.listeners.size) { + const api = this.api; + this._v = api.view(); + this._u = api.api.changes.listen(() => { + const view = api.view(); + if (view !== this._v) { + this._v = view; + this.emit(view); + } + }); + } + const unsubscribe = super.listen(listener); + return () => { + unsubscribe(); + if (!this.listeners.size) { + this._u?.(); + // this._unsub = this._view = undefined; + } + }; + } +} + +export class NodeEvents + extends Emitter + implements SyncStore> +{ + public readonly changes: ChangesFanOut; + + constructor(private readonly api: NodeApi) { super(); + this.changes = new ChangesFanOut(api); } private viewSubs: Set<(ev: NodeEventMap['view']) => any> = new Set(); @@ -60,4 +100,12 @@ export class NodeEvents extends Emitter { if (shouldUnsubscribeFromModelChanges) this.api.api.events.off('change', this.onModelChange); super.off(type, listener, options); } + + // ---------------------------------------------------------------- SyncStore + + public readonly subscribe = (callback: () => void): SyncStoreUnsubscribe => { + return this.changes.listen(() => callback()); + }; + + public readonly getSnapshot = () => this.api.view(); } diff --git a/src/json-crdt/model/api/nodes.ts b/src/json-crdt/model/api/nodes.ts index b24b091373..fb4d39abec 100644 --- a/src/json-crdt/model/api/nodes.ts +++ b/src/json-crdt/model/api/nodes.ts @@ -22,7 +22,7 @@ export class NodeApi implements Printable { constructor(public readonly node: N, public readonly api: ModelApi) {} /** @ignore */ - private ev: undefined | NodeEvents = undefined; + private ev: undefined | NodeEvents = undefined; /** * Event target for listening to node changes. You can subscribe to `"view"` @@ -34,9 +34,9 @@ export class NodeApi implements Printable { * }); * ``` */ - public get events(): NodeEvents { + public get events(): NodeEvents { const et = this.ev; - return et || (this.ev = new NodeEvents(this)); + return et || (this.ev = new NodeEvents(this)); } /** diff --git a/src/util/events/sync-store.ts b/src/util/events/sync-store.ts new file mode 100644 index 0000000000..e339587f02 --- /dev/null +++ b/src/util/events/sync-store.ts @@ -0,0 +1,7 @@ +export interface SyncStore { + subscribe: SyncStoreSubscribe; + getSnapshot: () => T; +} + +export type SyncStoreSubscribe = (callback: () => void) => SyncStoreUnsubscribe; +export type SyncStoreUnsubscribe = () => void; diff --git a/yarn.lock b/yarn.lock index 4dd0dd9c0f..cad24e5ea5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2243,7 +2243,6 @@ eastasianwidth@^0.2.0: "editing-traces@https://github.com/streamich/editing-traces#6494020428530a6e382378b98d1d7e31334e2d7b": version "0.0.0" - uid "6494020428530a6e382378b98d1d7e31334e2d7b" resolved "https://github.com/streamich/editing-traces#6494020428530a6e382378b98d1d7e31334e2d7b" ee-first@1.1.1: @@ -3667,7 +3666,6 @@ jsesc@^2.5.1: "json-crdt-traces@https://github.com/streamich/json-crdt-traces#02718a7a5d09e0dc6c31ea7d45a9ce3cbb0bf085": version "0.0.1" - uid "02718a7a5d09e0dc6c31ea7d45a9ce3cbb0bf085" resolved "https://github.com/streamich/json-crdt-traces#02718a7a5d09e0dc6c31ea7d45a9ce3cbb0bf085" json-logic-js@^2.0.1: @@ -5244,10 +5242,10 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -thingies@^1.11.1: - version "1.12.0" - resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.12.0.tgz#a815c224482d607aa70f563d3cbb351a338e4710" - integrity sha512-AiGqfYC1jLmJagbzQGuoZRM48JPsr9yB734a7K6wzr34NMhjUPrWSQrkF7ZBybf3yCerCL2Gcr02kMv4NmaZfA== +thingies@^1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.14.1.tgz#2456b15db2a4113b434874381b5691dbc4ce2f0f" + integrity sha512-ktiTHLvVsS5Cifx8zbvg7x4clgf3AS+52gMSGlW+fvk8xYzog8f9qHvivjpQF56NHCKKpj4O1wI6tH92V4a0rQ== thunky@^1.0.2: version "1.1.0" @@ -5442,7 +5440,6 @@ typescript@^5.2.2: uWebSockets.js@uNetworking/uWebSockets.js#v20.23.0: version "20.23.0" - uid "49f8f1eb54435e4c3e3e436571e93d1f06aaabbb" resolved "https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/49f8f1eb54435e4c3e3e436571e93d1f06aaabbb" uc.micro@^1.0.1, uc.micro@^1.0.5: