diff --git a/package.json b/package.json index dc25f80..0ff40c4 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,10 @@ "@babel/preset-typescript": "^7.21.5", "@parcel/config-default": "^2.9.3", "@parcel/core": "^2.9.3", - "@parcel/packager-ts": "2.9.2", + "@parcel/packager-ts": "^2.9.3", "@parcel/transformer-js": "^2.9.3", "@parcel/transformer-react-refresh-wrap": "^2.9.3", - "@parcel/transformer-typescript-types": "2.9.2", + "@parcel/transformer-typescript-types": "^2.9.3", "@preconstruct/cli": "^2.7.0", "@types/jest": "^29.4.0", "@types/node": "^18.14.6", diff --git a/packages/suspense/CHANGELOG.md b/packages/suspense/CHANGELOG.md index 5ab6471..4109f35 100644 --- a/packages/suspense/CHANGELOG.md +++ b/packages/suspense/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.0.44 +* `createCache` and `createIntervalCache` methods `subscribeToStatus` renamed to `subscribe` and parameters changed to also include value or error +* `createCache` subscribers notified after value explicitly cached via `cache` +* `useCacheMutation` sync mutation notifies subscribers after mutation + ## 0.0.43 * Fix potential update loop in `useImperativeCacheValue` from nested object properties diff --git a/packages/suspense/src/cache/createCache.test.ts b/packages/suspense/src/cache/createCache.test.ts index 029ab23..752f4fe 100644 --- a/packages/suspense/src/cache/createCache.test.ts +++ b/packages/suspense/src/cache/createCache.test.ts @@ -366,7 +366,7 @@ describe("createCache", () => { }); }); - describe("subscribeToStatus", () => { + describe("subscribe", () => { let callbackA: jest.Mock; let callbackB: jest.Mock; @@ -376,10 +376,10 @@ describe("createCache", () => { }); it("should subscribe to keys that have not been loaded", async () => { - cache.subscribeToStatus(callbackA, "sync"); + cache.subscribe(callbackA, "sync"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); await Promise.resolve(); @@ -387,61 +387,73 @@ describe("createCache", () => { }); it("should notify of the transition from undefined to resolved for synchronous caches", async () => { - cache.subscribeToStatus(callbackA, "sync"); + cache.subscribe(callbackA, "sync"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); cache.readAsync("sync"); expect(callbackA).toHaveBeenCalledTimes(3); - expect(callbackA).toHaveBeenCalledWith(STATUS_PENDING); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_PENDING }); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: "sync", + }); }); it("should notify of the transition from undefined to pending to resolved for async caches", async () => { - cache.subscribeToStatus(callbackA, "async"); + cache.subscribe(callbackA, "async"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); const thenable = cache.readAsync("async"); expect(callbackA).toHaveBeenCalledTimes(2); - expect(callbackA).toHaveBeenCalledWith(STATUS_PENDING); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_PENDING }); await thenable; expect(callbackA).toHaveBeenCalledTimes(3); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: "async", + }); }); it("should only notify each subscriber once", async () => { - cache.subscribeToStatus(callbackA, "sync"); - cache.subscribeToStatus(callbackB, "sync"); + cache.subscribe(callbackA, "sync"); + cache.subscribe(callbackB, "sync"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); expect(callbackB).toHaveBeenCalledTimes(1); - expect(callbackB).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackB).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); cache.readAsync("sync"); expect(callbackA).toHaveBeenCalledTimes(3); - expect(callbackA).toHaveBeenCalledWith(STATUS_PENDING); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_PENDING }); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: "sync", + }); expect(callbackB).toHaveBeenCalledTimes(3); - expect(callbackB).toHaveBeenCalledWith(STATUS_PENDING); - expect(callbackB).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackB).toHaveBeenCalledWith({ status: STATUS_PENDING }); + expect(callbackB).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: "sync", + }); }); it("should not notify after a subscriber unsubscribes", async () => { - const unsubscribe = cache.subscribeToStatus(callbackA, "sync"); + const unsubscribe = cache.subscribe(callbackA, "sync"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); unsubscribe(); @@ -451,8 +463,8 @@ describe("createCache", () => { }); it("should track subscribers separately, per key", async () => { - cache.subscribeToStatus(callbackA, "sync-1"); - cache.subscribeToStatus(callbackB, "sync-2"); + cache.subscribe(callbackA, "sync-1"); + cache.subscribe(callbackB, "sync-2"); callbackA.mockClear(); callbackB.mockClear(); @@ -464,8 +476,8 @@ describe("createCache", () => { }); it("should track unsubscriptions separately, per key", async () => { - const unsubscribeA = cache.subscribeToStatus(callbackA, "sync-1"); - cache.subscribeToStatus(callbackB, "sync-2"); + const unsubscribeA = cache.subscribe(callbackA, "sync-1"); + cache.subscribe(callbackB, "sync-2"); callbackA.mockClear(); callbackB.mockClear(); @@ -487,11 +499,17 @@ describe("createCache", () => { await Promise.resolve(); - cache.subscribeToStatus(callbackA, "async"); - cache.subscribeToStatus(callbackB, "error"); + cache.subscribe(callbackA, "async"); + cache.subscribe(callbackB, "error"); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); - expect(callbackB).toHaveBeenCalledWith(STATUS_REJECTED); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: "async", + }); + expect(callbackB).toHaveBeenCalledWith({ + error: "error", + status: STATUS_REJECTED, + }); }); it("should notify subscribers after a value is evicted", async () => { @@ -500,20 +518,25 @@ describe("createCache", () => { await Promise.resolve(); - cache.subscribeToStatus(callbackA, "sync-1"); - cache.subscribeToStatus(callbackB, "sync-2"); + cache.subscribe(callbackA, "sync-1"); + cache.subscribe(callbackB, "sync-2"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: "sync-1", + }); expect(callbackB).toHaveBeenCalledTimes(1); - expect(callbackB).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackB).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: "sync-2", + }); cache.evict("sync-1"); expect(callbackA).toHaveBeenCalledTimes(2); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); expect(callbackB).toHaveBeenCalledTimes(1); - expect(callbackB).toHaveBeenCalledWith(STATUS_RESOLVED); }); it("should notify subscribers after all values are evicted", async () => { @@ -522,20 +545,48 @@ describe("createCache", () => { await Promise.resolve(); - cache.subscribeToStatus(callbackA, "sync-1"); - cache.subscribeToStatus(callbackB, "sync-2"); + cache.subscribe(callbackA, "sync-1"); + cache.subscribe(callbackB, "sync-2"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: "sync-1", + }); expect(callbackB).toHaveBeenCalledTimes(1); - expect(callbackB).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackB).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: "sync-2", + }); cache.evictAll(); expect(callbackA).toHaveBeenCalledTimes(2); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); expect(callbackB).toHaveBeenCalledTimes(2); - expect(callbackB).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackB).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); + }); + + it("should notify subscribers when an object is externally cached", () => { + const cache = createCache<[string], Object>({ + debugLabel: "cache", + load: async () => {}, + }); + + cache.subscribe(callbackA, "externally-managed"); + + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); + + const value = { id: 123 }; + + cache.cache(value, "externally-managed"); + + expect(callbackA).toHaveBeenCalledTimes(2); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value, + }); + expect(callbackA.mock.lastCall[0].value).toEqual(value); }); }); @@ -765,7 +816,7 @@ describe("createCache", () => { getKey: getCacheKey, load, }); - console.log(consoleMock.mock.calls); + expect(consoleMock).toHaveBeenCalled(); expect(consoleMock.mock.calls[0]).toEqual( expect.arrayContaining([ diff --git a/packages/suspense/src/cache/createCache.ts b/packages/suspense/src/cache/createCache.ts index 48c640e..4496967 100644 --- a/packages/suspense/src/cache/createCache.ts +++ b/packages/suspense/src/cache/createCache.ts @@ -1,6 +1,11 @@ import { isDevelopment } from "#is-development"; import { unstable_getCacheForType as getCacheForTypeMutable } from "react"; -import { STATUS_NOT_FOUND, STATUS_PENDING } from "../constants"; +import { + STATUS_NOT_FOUND, + STATUS_PENDING, + STATUS_REJECTED, + STATUS_RESOLVED, +} from "../constants"; import { Cache, CacheLoadOptions, @@ -8,7 +13,11 @@ import { PendingRecord, Record, Status, - StatusCallback, + StatusAborted, + StatusNotFound, + StatusPending, + SubscriptionCallback, + SubscriptionData, UnsubscribeCallback, } from "../types"; import { @@ -38,7 +47,7 @@ export type InternalCache, Value> = Cache< __getOrCreateRecord: (...params: Params) => Record; __isImmutable: () => boolean; __mutationAbortControllerMap: Map; - __notifySubscribers: (params: Params) => void; + __notifySubscribers: (params: Params, data?: SubscriptionData) => void; __recordMap: CacheMap>; }; @@ -118,7 +127,7 @@ export function createCache, Value>( const mutationAbortControllerMap = new Map(); // Stores a set of callbacks (by key) for status subscribers. - const subscriberMap = new Map>(); + const subscriberMap = new Map>>(); // Immutable caches should read from backing cache directly. // Only mutable caches should use React-managed cache @@ -195,6 +204,8 @@ export function createCache, Value>( recordMap.set(cacheKey, record); pendingMutationRecordMap.set(cacheKey, record); + + notifySubscribers(params); } function createPendingMutationRecordMap(): CacheMap> { @@ -241,7 +252,9 @@ export function createCache, Value>( subscriberMap.forEach((set) => { set.forEach((callback) => { - callback(STATUS_NOT_FOUND); + callback({ + status: STATUS_NOT_FOUND, + }); }); }); subscriberMap.clear(); @@ -338,7 +351,9 @@ export function createCache, Value>( const set = subscriberMap.get(key); if (set) { set.forEach((callback) => { - callback(STATUS_NOT_FOUND); + callback({ + status: STATUS_NOT_FOUND, + }); }); } } @@ -381,13 +396,48 @@ export function createCache, Value>( } } - function notifySubscribers(params: Params): void { + function getSubscriptionData(params: Params): SubscriptionData { + const status = getStatus(...params); + const record = getRecord(...params); + + if (status === STATUS_PENDING) { + // Special case pending, async mutation + return { status }; + } + + if (record) { + if (isResolvedRecord(record)) { + return { + status: STATUS_RESOLVED, + value: record.data.value, + }; + } else if (isRejectedRecord(record)) { + return { + error: record.data.error, + status: STATUS_REJECTED, + }; + } + } + + return { + status: status as StatusNotFound | StatusPending | StatusAborted, + }; + } + + function notifySubscribers( + params: Params, + data?: SubscriptionData + ): void { const cacheKey = getKey(params); + const set = subscriberMap.get(cacheKey); if (set) { - const status = getStatus(...params); + if (data === undefined) { + data = getSubscriptionData(params); + } + set.forEach((callback) => { - callback(status); + callback(data!); }); } } @@ -427,11 +477,11 @@ export function createCache, Value>( } } - function subscribeToStatus( - callback: StatusCallback, + function subscribe( + callback: SubscriptionCallback, ...params: Params ): UnsubscribeCallback { - debugLog("subscribeToStatus()", params); + debugLog("subscribe()", params); const cacheKey = getKey(params); let set = subscriberMap.get(cacheKey); @@ -443,9 +493,9 @@ export function createCache, Value>( } try { - const status = getStatus(...params); + const data = getSubscriptionData(params); - callback(status); + callback(data); } finally { return () => { set!.delete(callback); @@ -479,7 +529,7 @@ export function createCache, Value>( readAsync, read, prefetch, - subscribeToStatus, + subscribe: subscribe, }; return value; diff --git a/packages/suspense/src/cache/createExternallyManagedCache.test.tsx b/packages/suspense/src/cache/createExternallyManagedCache.test.tsx index 53df939..3e08c70 100644 --- a/packages/suspense/src/cache/createExternallyManagedCache.test.tsx +++ b/packages/suspense/src/cache/createExternallyManagedCache.test.tsx @@ -35,7 +35,7 @@ describe("createExternallyManagedCache", () => { expect(() => cache.getValue("test")).toThrow("expected error"); }); - describe("subscribeToStatus", () => { + describe("subscribe", () => { let callback: jest.Mock; beforeEach(() => { @@ -43,33 +43,39 @@ describe("createExternallyManagedCache", () => { }); it("should update when resolved", async () => { - cache.subscribeToStatus(callback, "test"); + cache.subscribe(callback, "test"); expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callback).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); cache.cacheValue("value", "test"); await Promise.resolve(); expect(callback).toHaveBeenCalledTimes(3); - expect(callback).toHaveBeenCalledWith(STATUS_PENDING); - expect(callback).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callback).toHaveBeenCalledWith({ status: STATUS_PENDING }); + expect(callback).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: "value", + }); }); it("should update when rejected", async () => { - cache.subscribeToStatus(callback, "test"); + cache.subscribe(callback, "test"); expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callback).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); - cache.cacheError("expected error", "test"); + const error = new Error("expected error"); + + cache.cacheError(error, "test"); await Promise.resolve(); expect(callback).toHaveBeenCalledTimes(3); - expect(callback).toHaveBeenCalledWith(STATUS_PENDING); - expect(callback).toHaveBeenCalledWith(STATUS_REJECTED); + expect(callback).toHaveBeenCalledWith({ status: STATUS_PENDING }); + expect(callback).toHaveBeenCalledWith({ status: STATUS_REJECTED, error }); + expect(callback.mock.lastCall[0].error).toBe(error); }); }); diff --git a/packages/suspense/src/cache/createIntervalCache/createIntervalCache.test.ts b/packages/suspense/src/cache/createIntervalCache/createIntervalCache.test.ts index 5086e3d..eef8dc3 100644 --- a/packages/suspense/src/cache/createIntervalCache/createIntervalCache.test.ts +++ b/packages/suspense/src/cache/createIntervalCache/createIntervalCache.test.ts @@ -801,7 +801,7 @@ describe("createIntervalCache", () => { }); }); - describe("subscribeToStatus", () => { + describe("subscribe", () => { let callbackA: jest.Mock; let callbackB: jest.Mock; @@ -811,10 +811,10 @@ describe("createIntervalCache", () => { }); it("should subscribe to keys that have not been loaded", async () => { - cache.subscribeToStatus(callbackA, 1, 5, "text"); + cache.subscribe(callbackA, 1, 5, "text"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); await Promise.resolve(); @@ -822,54 +822,63 @@ describe("createIntervalCache", () => { }); it("should notify of the transition from undefined to pending to resolved", async () => { - cache.subscribeToStatus(callbackA, 1, 5, "text"); + cache.subscribe(callbackA, 1, 5, "text"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); const promise = cache.readAsync(1, 5, "text"); expect(callbackA).toHaveBeenCalledTimes(2); - expect(callbackA).toHaveBeenCalledWith(STATUS_PENDING); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_PENDING }); await promise; expect(callbackA).toHaveBeenCalledTimes(3); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: [1, 2, 3, 4, 5], + }); }); it("should only notify each subscriber once", async () => { - cache.subscribeToStatus(callbackA, 1, 5, "text"); - cache.subscribeToStatus(callbackB, 1, 5, "text"); + cache.subscribe(callbackA, 1, 5, "text"); + cache.subscribe(callbackB, 1, 5, "text"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); expect(callbackB).toHaveBeenCalledTimes(1); - expect(callbackB).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackB).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); const promise = cache.readAsync(1, 5, "text"); expect(callbackA).toHaveBeenCalledTimes(2); - expect(callbackA).toHaveBeenCalledWith(STATUS_PENDING); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_PENDING }); expect(callbackB).toHaveBeenCalledTimes(2); - expect(callbackB).toHaveBeenCalledWith(STATUS_PENDING); + expect(callbackB).toHaveBeenCalledWith({ status: STATUS_PENDING }); await promise; expect(callbackA).toHaveBeenCalledTimes(3); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: [1, 2, 3, 4, 5], + }); expect(callbackB).toHaveBeenCalledTimes(3); - expect(callbackB).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackB).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: [1, 2, 3, 4, 5], + }); }); it("should not notify after a subscriber unsubscribes", async () => { - const unsubscribe = cache.subscribeToStatus(callbackA, 1, 5, "test"); + const unsubscribe = cache.subscribe(callbackA, 1, 5, "test"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); unsubscribe(); @@ -879,8 +888,8 @@ describe("createIntervalCache", () => { }); it("should track subscribers separately, per key", async () => { - cache.subscribeToStatus(callbackA, 1, 5, "test-1"); - cache.subscribeToStatus(callbackB, 1, 5, "test-2"); + cache.subscribe(callbackA, 1, 5, "test-1"); + cache.subscribe(callbackB, 1, 5, "test-2"); callbackA.mockClear(); callbackB.mockClear(); @@ -892,8 +901,8 @@ describe("createIntervalCache", () => { }); it("should track unsubscriptions separately, per key", async () => { - const unsubscribeA = cache.subscribeToStatus(callbackA, 1, 5, "test-1"); - cache.subscribeToStatus(callbackB, 1, 5, "test-2"); + const unsubscribeA = cache.subscribe(callbackA, 1, 5, "test-1"); + cache.subscribe(callbackB, 1, 5, "test-2"); callbackA.mockClear(); callbackB.mockClear(); @@ -919,51 +928,68 @@ describe("createIntervalCache", () => { await willReject; } catch (error) {} - cache.subscribeToStatus(callbackA, 1, 5, "will-resolve"); - cache.subscribeToStatus(callbackB, 1, 5, "will-error"); + cache.subscribe(callbackA, 1, 5, "will-resolve"); + cache.subscribe(callbackB, 1, 5, "will-error"); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); - expect(callbackB).toHaveBeenCalledWith(STATUS_REJECTED); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: [1, 2, 3, 4, 5], + }); + expect(callbackB).toHaveBeenCalledWith({ + status: STATUS_REJECTED, + error: "expected", + }); }); it("should notify subscribers after a value is evicted", async () => { await cache.readAsync(1, 5, "test-1"); await cache.readAsync(1, 5, "test-2"); - cache.subscribeToStatus(callbackA, 1, 5, "test-1"); - cache.subscribeToStatus(callbackB, 1, 5, "test-2"); + cache.subscribe(callbackA, 1, 5, "test-1"); + cache.subscribe(callbackB, 1, 5, "test-2"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: [1, 2, 3, 4, 5], + }); expect(callbackB).toHaveBeenCalledTimes(1); - expect(callbackB).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackB).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: [1, 2, 3, 4, 5], + }); cache.evict("test-1"); expect(callbackA).toHaveBeenCalledTimes(2); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); expect(callbackB).toHaveBeenCalledTimes(1); - expect(callbackB).toHaveBeenCalledWith(STATUS_RESOLVED); }); it("should notify subscribers after all values are evicted", async () => { await cache.readAsync(1, 5, "test-1"); await cache.readAsync(1, 5, "test-2"); - cache.subscribeToStatus(callbackA, 1, 5, "test-1"); - cache.subscribeToStatus(callbackB, 1, 5, "test-2"); + cache.subscribe(callbackA, 1, 5, "test-1"); + cache.subscribe(callbackB, 1, 5, "test-2"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: [1, 2, 3, 4, 5], + }); expect(callbackB).toHaveBeenCalledTimes(1); - expect(callbackB).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackB).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: [1, 2, 3, 4, 5], + }); cache.evictAll(); expect(callbackA).toHaveBeenCalledTimes(2); - expect(callbackA).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); expect(callbackB).toHaveBeenCalledTimes(2); - expect(callbackB).toHaveBeenCalledWith(STATUS_NOT_FOUND); + expect(callbackB).toHaveBeenCalledWith({ status: STATUS_NOT_FOUND }); }); it("should notify of in-progress request when an interval is refined", async () => { @@ -980,17 +1006,20 @@ describe("createIntervalCache", () => { const willResolve = cache.readAsync(2, 4, "test"); - cache.subscribeToStatus(callbackA, 2, 4, "test"); + cache.subscribe(callbackA, 2, 4, "test"); expect(callbackA).toHaveBeenCalledTimes(1); - expect(callbackA).toHaveBeenCalledWith(STATUS_PENDING); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_PENDING }); expect(cache.getStatus(2, 4, "test")).toBe(STATUS_PENDING); await willResolve; expect(callbackA).toHaveBeenCalledTimes(2); - expect(callbackA).toHaveBeenCalledWith(STATUS_PENDING); - expect(callbackA).toHaveBeenCalledWith(STATUS_RESOLVED); + expect(callbackA).toHaveBeenCalledWith({ status: STATUS_PENDING }); + expect(callbackA).toHaveBeenCalledWith({ + status: STATUS_RESOLVED, + value: [2, 3, 4], + }); expect(cache.getStatus(2, 4, "test")).toBe(STATUS_RESOLVED); }); }); diff --git a/packages/suspense/src/cache/createIntervalCache/createIntervalCache.ts b/packages/suspense/src/cache/createIntervalCache/createIntervalCache.ts index 6ae019b..13f50c5 100644 --- a/packages/suspense/src/cache/createIntervalCache/createIntervalCache.ts +++ b/packages/suspense/src/cache/createIntervalCache/createIntervalCache.ts @@ -18,7 +18,11 @@ import { IntervalCacheLoadOptions, PendingRecord, Record, - StatusCallback, + StatusAborted, + StatusNotFound, + StatusPending, + SubscriptionCallback, + SubscriptionData, } from "../../types"; import { assertPendingRecord } from "../../utils/assertRecordStatus"; import { createDeferred } from "../../utils/createDeferred"; @@ -51,8 +55,8 @@ type Metadata = { sortedValues: Value[]; }; -type SubscriptionData = { - callbacks: Set; +type SubscribersSetMetadata = { + callbacks: Set>; end: Point; params: Params; start: Point; @@ -121,7 +125,7 @@ export function createIntervalCache< // That interval tree maps an interval to a set of callbacks. const subscriberMap: Map< string, - DataIntervalTree, Point> + DataIntervalTree, Point> > = new Map(); const debugLog = (message: string, params?: Params, ...args: any[]) => { @@ -165,7 +169,7 @@ export function createIntervalCache< record.data.abortController.abort(); - notifySubscribers(start, end, ...params); + notifySubscribers(start, end, params); } catch (error) { caught = error; } @@ -223,7 +227,9 @@ export function createIntervalCache< const tree = subscriberMap.get(cacheKey); if (tree) { for (let node of tree.inOrder()) { - node.data.callbacks.forEach((callback) => callback(STATUS_NOT_FOUND)); + node.data.callbacks.forEach((callback) => + callback({ status: STATUS_NOT_FOUND }) + ); } } @@ -239,7 +245,9 @@ export function createIntervalCache< subscriberMap.forEach((tree) => { for (let node of tree.inOrder()) { - node.data.callbacks.forEach((callback) => callback(STATUS_NOT_FOUND)); + node.data.callbacks.forEach((callback) => + callback({ status: STATUS_NOT_FOUND }) + ); } }); @@ -295,7 +303,7 @@ export function createIntervalCache< metadata.recordMap.set(cacheKey, record); - notifySubscribers(start, end, ...params); + notifySubscribers(start, end, params); processPendingRecord( metadata, @@ -425,19 +433,46 @@ export function createIntervalCache< return value instanceof PartialArray; } - function notifySubscribers( + function getSubscriptionData( start: Point, end: Point, - ...params: Params - ): void { + params: Params + ): SubscriptionData { + const metadata = getOrCreateIntervalMetadata(...params); + const cacheKey = createCacheKey(start, end); + + let record = metadata.recordMap.get(cacheKey); + if (record) { + if (isResolvedRecord(record)) { + return { + status: STATUS_RESOLVED, + value: record.data.value, + }; + } else if (isRejectedRecord(record)) { + return { + error: record.data.error, + status: STATUS_REJECTED, + }; + } + } + + const status = getStatus(start, end, ...params); + + return { + status: status as StatusNotFound | StatusPending | StatusAborted, + }; + } + + function notifySubscribers(start: Point, end: Point, params: Params): void { const cacheKey = getKey(...params); const tree = subscriberMap.get(cacheKey); if (tree) { const matches = tree.search(start, end); - matches.forEach((match: SubscriptionData) => { - const status = getStatus(match.start, match.end, ...match.params); + matches.forEach((match: SubscribersSetMetadata) => { + const data = getSubscriptionData(match.start, match.end, match.params); + match.callbacks.forEach((callback) => { - callback(status); + callback(data!); }); }); } @@ -579,7 +614,7 @@ export function createIntervalCache< deferred.reject(error); - notifySubscribers(start, end, ...params); + notifySubscribers(start, end, params); return; } @@ -659,7 +694,7 @@ export function createIntervalCache< deferred.resolve(value); - notifySubscribers(start, end, ...params); + notifySubscribers(start, end, params); } } catch (error) { let errorMessage = "Unknown Error"; @@ -683,7 +718,7 @@ export function createIntervalCache< deferred.reject(error); - notifySubscribers(start, end, ...params); + notifySubscribers(start, end, params); } } } @@ -717,13 +752,13 @@ export function createIntervalCache< } } - function subscribeToStatus( - callback: StatusCallback, + function subscribe( + callback: SubscriptionCallback, start: Point, end: Point, ...params: Params ) { - debugLog(`subscribeToStatus(${start}, ${end})`, params); + debugLog(`subscribe(${start}, ${end})`, params); const cacheKey = getKey(...params); @@ -750,9 +785,9 @@ export function createIntervalCache< } try { - const status = getStatus(start, end, ...params); + const data = getSubscriptionData(match.start, match.end, match.params); - callback(status); + callback(data); } finally { return () => { if (tree && match) { @@ -777,7 +812,7 @@ export function createIntervalCache< isPartialResult, readAsync, read, - subscribeToStatus, + subscribe, }; } diff --git a/packages/suspense/src/hooks/useCacheMutation.test.tsx b/packages/suspense/src/hooks/useCacheMutation.test.tsx index 69fbc45..5efe391 100644 --- a/packages/suspense/src/hooks/useCacheMutation.test.tsx +++ b/packages/suspense/src/hooks/useCacheMutation.test.tsx @@ -5,10 +5,21 @@ import { createRoot } from "react-dom/client"; import { act } from "react-dom/test-utils"; -import { Component, PropsWithChildren, ReactNode } from "react"; -import { useImperativeCacheValue } from ".."; +import { + Component, + PropsWithChildren, + ReactNode, + useLayoutEffect, +} from "react"; +import { STATUS_PENDING, STATUS_RESOLVED, useImperativeCacheValue } from ".."; import { createCache } from "../cache/createCache"; -import { Cache, CacheLoadOptions, Deferred, Status } from "../types"; +import { + Cache, + CacheLoadOptions, + Deferred, + Status, + SubscriptionData, +} from "../types"; import { createDeferred } from "../utils/createDeferred"; import { MutationApi, useCacheMutation } from "./useCacheMutation"; import { useCacheStatus } from "./useCacheStatus"; @@ -579,6 +590,99 @@ describe("useCacheMutation", () => { ); }); }); + + describe("cache subscriptions", () => { + let subscriptionMocks: { + [cacheKey: string]: jest.Mock[]>; + }; + + beforeEach(() => { + subscriptionMocks = {}; + + Component = ({ cacheKey }: Props) => { + mutationApi[cacheKey] = useCacheMutation(cache); + + useLayoutEffect(() => { + const mock = jest.fn(); + subscriptionMocks[cacheKey] = mock; + return cache.subscribe(mock, cacheKey); + }, [cacheKey]); + + return cache.read(cacheKey); + }; + }); + + it("should be notified after a sync mutation", async () => { + await mount(); + + const mockOne = subscriptionMocks.one!; + const mockTwo = subscriptionMocks.two!; + + expect(mockOne).toBeCalledTimes(1); + expect(mockOne).toBeCalledWith({ + status: STATUS_RESOLVED, + value: "one", + }); + expect(mockTwo).toBeCalledTimes(1); + expect(mockTwo).toBeCalledWith({ + status: STATUS_RESOLVED, + value: "two", + }); + + act(() => { + mutationApi.two!.mutateSync(["two"], "mutated-two"); + }); + + expect(mockOne).toBeCalledTimes(1); + expect(mockTwo).toBeCalledTimes(2); + expect(mockTwo).toBeCalledWith({ + status: STATUS_RESOLVED, + value: "mutated-two", + }); + }); + + it("should be notified after an async mutation", async () => { + await mount(); + + const mockOne = subscriptionMocks.one!; + const mockTwo = subscriptionMocks.two!; + + expect(mockOne).toBeCalledTimes(1); + expect(mockOne).toBeCalledWith({ + status: STATUS_RESOLVED, + value: "one", + }); + expect(mockTwo).toBeCalledTimes(1); + expect(mockTwo).toBeCalledWith({ + status: STATUS_RESOLVED, + value: "two", + }); + + let pendingDeferred: Deferred | null = null; + await act(async () => { + // Don't wait for the mutation API to resolve; we want to test the in-between state too + mutationApi.two!.mutateAsync(["two"], async () => { + pendingDeferred = createDeferred(); + return pendingDeferred.promise; + }); + }); + + expect(mockOne).toBeCalledTimes(1); + expect(mockTwo).toBeCalledTimes(2); + expect(mockTwo).toBeCalledWith({ status: STATUS_PENDING }); + + await act(async () => { + pendingDeferred!.resolve("mutated-two"); + }); + + expect(mockOne).toBeCalledTimes(1); + expect(mockTwo).toBeCalledTimes(3); + expect(mockTwo).toBeCalledWith({ + status: STATUS_RESOLVED, + value: "mutated-two", + }); + }); + }); }); type State = { errorMessage: string | null }; diff --git a/packages/suspense/src/hooks/useCacheMutation.ts b/packages/suspense/src/hooks/useCacheMutation.ts index 3ea52c3..ee49ca0 100644 --- a/packages/suspense/src/hooks/useCacheMutation.ts +++ b/packages/suspense/src/hooks/useCacheMutation.ts @@ -4,7 +4,7 @@ import { useTransition, } from "react"; import { InternalCache } from "../cache/createCache"; -import { STATUS_PENDING, STATUS_REJECTED } from "../constants"; +import { STATUS_PENDING, STATUS_REJECTED, STATUS_RESOLVED } from "../constants"; import { Cache, Record } from "../types"; import { createDeferred } from "../utils/createDeferred"; import { createResolvedRecord, updateRecordToResolved } from "../utils/Record"; @@ -67,6 +67,8 @@ export function useCacheMutation, Value>( startTransition(() => { refresh(createPendingMutationRecordMap, pendingMutationRecordMap); }); + + notifySubscribers(params); }, [refresh, startTransition] ); @@ -102,10 +104,11 @@ export function useCacheMutation, Value>( mutationAbortControllerMap.set(cacheKey, abortController); startTransition(() => { - notifySubscribers(params); refresh(createPendingMutationRecordMap, pendingMutationRecordMap); }); + notifySubscribers(params, { status: STATUS_PENDING }); + try { // Wait until the mutation finishes or is aborted const newValue = await Promise.race([ @@ -135,9 +138,13 @@ export function useCacheMutation, Value>( } startTransition(() => { - notifySubscribers(params); refresh(createPendingMutationRecordMap, pendingMutationRecordMap); }); + + notifySubscribers(params, { + status: STATUS_RESOLVED, + value: newValue as Value, + }); } catch (error) { (record as Record).data = { error, @@ -154,9 +161,13 @@ export function useCacheMutation, Value>( recordMap.set(cacheKey, record); startTransition(() => { - notifySubscribers(params); refresh(createPendingMutationRecordMap, pendingMutationRecordMap); }); + + notifySubscribers(params, { + error, + status: STATUS_REJECTED, + }); } finally { // Cleanup after mutation by deleting the abort controller // If this mutation has already been preempted by a newer mutation diff --git a/packages/suspense/src/hooks/useCacheStatus.ts b/packages/suspense/src/hooks/useCacheStatus.ts index ca4a21f..117fbca 100644 --- a/packages/suspense/src/hooks/useCacheStatus.ts +++ b/packages/suspense/src/hooks/useCacheStatus.ts @@ -7,7 +7,7 @@ export function useCacheStatus>( ...params: Params ): Status { return useSyncExternalStore( - (callback) => cache.subscribeToStatus(callback, ...params), + (callback) => cache.subscribe(callback, ...params), () => cache.getStatus(...params), () => cache.getStatus(...params) ); diff --git a/packages/suspense/src/hooks/useIntervalCacheStatus.ts b/packages/suspense/src/hooks/useIntervalCacheStatus.ts index 16e6ebd..f06c325 100644 --- a/packages/suspense/src/hooks/useIntervalCacheStatus.ts +++ b/packages/suspense/src/hooks/useIntervalCacheStatus.ts @@ -9,7 +9,7 @@ export function useIntervalCacheStatus>( ...params: Params ): Status { return useSyncExternalStore( - (callback) => cache.subscribeToStatus(callback, start, end, ...params), + (callback) => cache.subscribe(callback, start, end, ...params), () => cache.getStatus(start, end, ...params), () => cache.getStatus(start, end, ...params) ); diff --git a/packages/suspense/src/types.ts b/packages/suspense/src/types.ts index 5afc01d..9028d16 100644 --- a/packages/suspense/src/types.ts +++ b/packages/suspense/src/types.ts @@ -6,14 +6,14 @@ import { STATUS_RESOLVED, } from "./constants"; -export type StatusNotStarted = typeof STATUS_NOT_FOUND; +export type StatusNotFound = typeof STATUS_NOT_FOUND; export type StatusPending = typeof STATUS_PENDING; export type StatusAborted = typeof STATUS_ABORTED; export type StatusRejected = typeof STATUS_REJECTED; export type StatusResolved = typeof STATUS_RESOLVED; export type Status = - | StatusNotStarted + | StatusNotFound | StatusPending | StatusAborted | StatusRejected @@ -54,8 +54,35 @@ export type RecordData = | ResolvedRecordData | RejectedRecordData; +export interface SubscriptionDataNotFound { + readonly status: StatusNotFound; +} +export interface SubscriptionDataPending { + readonly status: StatusPending; +} +export interface SubscriptionDataAborted { + readonly status: StatusAborted; +} +export interface SubscriptionDataRejected { + readonly error: any; + readonly status: StatusRejected; +} +export interface SubscriptionDataResolved { + readonly status: StatusResolved; + readonly value: Type; +} + +export type SubscriptionData = + | SubscriptionDataNotFound + | SubscriptionDataPending + | SubscriptionDataAborted + | SubscriptionDataRejected + | SubscriptionDataResolved; + export type StreamingSubscribeCallback = () => void; -export type StatusCallback = (status: Status) => void; +export type SubscriptionCallback = ( + data: SubscriptionData +) => void; export type UnsubscribeCallback = () => void; // Convenience type used by Suspense caches. @@ -82,8 +109,8 @@ export interface Cache { prefetch(...params: Params): void; read(...params: Params): Value; readAsync(...params: Params): PromiseLike | Value; - subscribeToStatus( - callback: StatusCallback, + subscribe( + callback: SubscriptionCallback, ...params: Params ): UnsubscribeCallback; } @@ -114,8 +141,8 @@ export type IntervalCache = { ): PromiseLike | Value[]; isPartialResult: (value: Value[]) => boolean; read(start: Point, end: Point, ...params: Params): Value[]; - subscribeToStatus( - callback: StatusCallback, + subscribe( + callback: SubscriptionCallback, start: Point, end: Point, ...params: Params diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15b3369..450801d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,8 +14,8 @@ importers: specifier: ^2.9.3 version: 2.9.3 '@parcel/packager-ts': - specifier: 2.9.2 - version: 2.9.2(@parcel/core@2.9.3) + specifier: ^2.9.3 + version: 2.9.3(@parcel/core@2.9.3) '@parcel/transformer-js': specifier: ^2.9.3 version: 2.9.3(@parcel/core@2.9.3) @@ -23,8 +23,8 @@ importers: specifier: ^2.9.3 version: 2.9.3(@parcel/core@2.9.3) '@parcel/transformer-typescript-types': - specifier: 2.9.2 - version: 2.9.2(@parcel/core@2.9.3)(typescript@5.0.4) + specifier: ^2.9.3 + version: 2.9.3(@parcel/core@2.9.3)(typescript@5.0.4) '@preconstruct/cli': specifier: ^2.7.0 version: 2.7.0 @@ -1604,19 +1604,6 @@ packages: - '@parcel/core' dev: true - /@parcel/cache@2.9.2(@parcel/core@2.9.3): - resolution: {integrity: sha512-Bde9HmxaO+H5qPbcxBl/JzzZ/7ewoHFDWLOQ4zdfyh+q4IyLS257WAUGm4x6BeNjc1S7YjoelEbBKdgw8mQOig==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@parcel/core': ^2.9.2 - dependencies: - '@parcel/core': 2.9.3 - '@parcel/fs': 2.9.2(@parcel/core@2.9.3) - '@parcel/logger': 2.9.2 - '@parcel/utils': 2.9.2 - lmdb: 2.7.11 - dev: true - /@parcel/cache@2.9.3(@parcel/core@2.9.3): resolution: {integrity: sha512-Bj/H2uAJJSXtysG7E/x4EgTrE2hXmm7td/bc97K8M9N7+vQjxf7xb0ebgqe84ePVMkj4MVQSMEJkEucXVx4b0Q==} engines: {node: '>= 12.0.0'} @@ -1630,13 +1617,6 @@ packages: lmdb: 2.7.11 dev: true - /@parcel/codeframe@2.9.2: - resolution: {integrity: sha512-+T1POu9uU2tkPi3P25ojtU3CKoGYfirc2bE/1iNyvbuEtpkAzl9UQFXphGqFyuJSI429mP2pWL8SeKG0b5zaUw==} - engines: {node: '>= 12.0.0'} - dependencies: - chalk: 4.1.2 - dev: true - /@parcel/codeframe@2.9.3: resolution: {integrity: sha512-z7yTyD6h3dvduaFoHpNqur74/2yDWL++33rjQjIjCaXREBN6dKHoMGMizzo/i4vbiI1p9dDox2FIDEHCMQxqdA==} engines: {node: '>= 12.0.0'} @@ -1731,14 +1711,6 @@ packages: semver: 7.5.3 dev: true - /@parcel/diagnostic@2.9.2: - resolution: {integrity: sha512-cHvQ3GtC0dJixtt5Ne1SG0vogt6PE9Fu2KmrFMLcL57rowi3sl+W+Lh02sujd/V0ZQOSRV01WdXJXDsiI/na8g==} - engines: {node: '>= 12.0.0'} - dependencies: - '@mischnic/json-sourcemap': 0.1.0 - nullthrows: 1.1.1 - dev: true - /@parcel/diagnostic@2.9.3: resolution: {integrity: sha512-6jxBdyB3D7gP4iE66ghUGntWt2v64E6EbD4AetZk+hNJpgudOOPsKTovcMi/i7I4V0qD7WXSF4tvkZUoac0jwA==} engines: {node: '>= 12.0.0'} @@ -1747,40 +1719,16 @@ packages: nullthrows: 1.1.1 dev: true - /@parcel/events@2.9.2: - resolution: {integrity: sha512-aDKq9gl8vK/LTTsAs3k8wBsFYVQ8NOSa0aC0Thq+l5KRN04U/ljNiDVmxDkwJgAvT0Iv62kT9ooBl6aQPUWNyQ==} - engines: {node: '>= 12.0.0'} - dev: true - /@parcel/events@2.9.3: resolution: {integrity: sha512-K0Scx+Bx9f9p1vuShMzNwIgiaZUkxEnexaKYHYemJrM7pMAqxIuIqhnvwurRCsZOVLUJPDDNJ626cWTc5vIq+A==} engines: {node: '>= 12.0.0'} dev: true - /@parcel/fs-search@2.9.2: - resolution: {integrity: sha512-PP1aFLaH5rk8mF8AN62/R68Ne9Xq/VNhj3h1BxdESiHkhRIrM1ZcQ4t4WBaMjPaLXi1jSKLQ8fY50QBVIJKy4Q==} - engines: {node: '>= 12.0.0'} - dev: true - /@parcel/fs-search@2.9.3: resolution: {integrity: sha512-nsNz3bsOpwS+jphcd+XjZL3F3PDq9lik0O8HPm5f6LYkqKWT+u/kgQzA8OkAHCR3q96LGiHxUywHPEBc27vI4Q==} engines: {node: '>= 12.0.0'} dev: true - /@parcel/fs@2.9.2(@parcel/core@2.9.3): - resolution: {integrity: sha512-URKchUywNyoOIcOsmwcxr8gp+CBVjD502Fb6RhAdFhdZV2o3X2BLTGf03fQzSSJ0IDO3jKUTK0UUg/Mz8Vd3Rw==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@parcel/core': ^2.9.2 - dependencies: - '@parcel/core': 2.9.3 - '@parcel/fs-search': 2.9.2 - '@parcel/types': 2.9.2(@parcel/core@2.9.3) - '@parcel/utils': 2.9.2 - '@parcel/watcher': 2.1.0 - '@parcel/workers': 2.9.2(@parcel/core@2.9.3) - dev: true - /@parcel/fs@2.9.3(@parcel/core@2.9.3): resolution: {integrity: sha512-/PrRKgCRw22G7rNPSpgN3Q+i2nIkZWuvIOAdMG4KWXC4XLp8C9jarNaWd5QEQ75amjhQSl3oUzABzkdCtkKrgg==} engines: {node: '>= 12.0.0'} @@ -1802,13 +1750,6 @@ packages: nullthrows: 1.1.1 dev: true - /@parcel/hash@2.9.2: - resolution: {integrity: sha512-zXjg3BTxevsTe2Ylqsmm2Cw6gcIObaSz2dBjeRXO3LM8ziXJ4c7tOBKIXHPcnc2JmOyp3pmFB1sQaE+qXKh0DQ==} - engines: {node: '>= 12.0.0'} - dependencies: - xxhash-wasm: 0.4.2 - dev: true - /@parcel/hash@2.9.3: resolution: {integrity: sha512-qlH5B85XLzVAeijgKPjm1gQu35LoRYX/8igsjnN8vOlbc3O8BYAUIutU58fbHbtE8MJPbxQQUw7tkTjeoujcQQ==} engines: {node: '>= 12.0.0'} @@ -1816,14 +1757,6 @@ packages: xxhash-wasm: 0.4.2 dev: true - /@parcel/logger@2.9.2: - resolution: {integrity: sha512-rhb+CZZ4tKbrH585GTec32qxEpbjqrjaAbBRmyjGknsTleoiazcrLiutE7h+VRItKmv5QG+yPgrZ0PFx83fmhw==} - engines: {node: '>= 12.0.0'} - dependencies: - '@parcel/diagnostic': 2.9.2 - '@parcel/events': 2.9.2 - dev: true - /@parcel/logger@2.9.3: resolution: {integrity: sha512-5FNBszcV6ilGFcijEOvoNVG6IUJGsnMiaEnGQs7Fvc1dktTjEddnoQbIYhcSZL63wEmzBZOgkT5yDMajJ/41jw==} engines: {node: '>= 12.0.0'} @@ -1832,13 +1765,6 @@ packages: '@parcel/events': 2.9.3 dev: true - /@parcel/markdown-ansi@2.9.2: - resolution: {integrity: sha512-2iWqdaQhDEPL11V4TAssghJLZUXwB4RXzCgOEniWv7Hj/3ymXA4VzCyOncRoIqpm4MvxBV3tLPGM7qVqbCzN8Q==} - engines: {node: '>= 12.0.0'} - dependencies: - chalk: 4.1.2 - dev: true - /@parcel/markdown-ansi@2.9.3: resolution: {integrity: sha512-/Q4X8F2aN8UNjAJrQ5NfK2OmZf6shry9DqetUSEndQ0fHonk78WKt6LT0zSKEBEW/bB/bXk6mNMsCup6L8ibjQ==} engines: {node: '>= 12.0.0'} @@ -1857,20 +1783,6 @@ packages: - '@parcel/core' dev: true - /@parcel/node-resolver-core@3.0.2(@parcel/core@2.9.3): - resolution: {integrity: sha512-fDsELMiEZoMOfqVKQY+BpGA92egLy4rTCC0ra1J+rKpevOubh/qNL2px3+FZUlfsxFO59iaR4qBSjBUzfD3zlg==} - engines: {node: '>= 12.0.0'} - dependencies: - '@mischnic/json-sourcemap': 0.1.0 - '@parcel/diagnostic': 2.9.2 - '@parcel/fs': 2.9.2(@parcel/core@2.9.3) - '@parcel/utils': 2.9.2 - nullthrows: 1.1.1 - semver: 5.7.1 - transitivePeerDependencies: - - '@parcel/core' - dev: true - /@parcel/node-resolver-core@3.0.3(@parcel/core@2.9.3): resolution: {integrity: sha512-AjxNcZVHHJoNT/A99PKIdFtwvoze8PAiC3yz8E/dRggrDIOboUEodeQYV5Aq++aK76uz/iOP0tST2T8A5rhb1A==} engines: {node: '>= 12.0.0'} @@ -1960,23 +1872,6 @@ packages: - '@swc/helpers' dev: true - /@parcel/package-manager@2.9.2(@parcel/core@2.9.3): - resolution: {integrity: sha512-4/ytXWzm0456gbT93klZNM1CMSqG9SCbJWKk7m5pqy5f8hCYDSrd9Qza+tTynK73cNCHzl4ehS3wsHDhsT+q+Q==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@parcel/core': ^2.9.2 - dependencies: - '@parcel/core': 2.9.3 - '@parcel/diagnostic': 2.9.2 - '@parcel/fs': 2.9.2(@parcel/core@2.9.3) - '@parcel/logger': 2.9.2 - '@parcel/node-resolver-core': 3.0.2(@parcel/core@2.9.3) - '@parcel/types': 2.9.2(@parcel/core@2.9.3) - '@parcel/utils': 2.9.2 - '@parcel/workers': 2.9.2(@parcel/core@2.9.3) - semver: 5.7.1 - dev: true - /@parcel/package-manager@2.9.3(@parcel/core@2.9.3): resolution: {integrity: sha512-NH6omcNTEupDmW4Lm1e4NUYBjdqkURxgZ4CNESESInHJe6tblVhNB8Rpr1ar7zDar7cly9ILr8P6N3Ei7bTEjg==} engines: {node: '>= 12.0.0'} @@ -2056,20 +1951,11 @@ packages: - '@parcel/core' dev: true - /@parcel/packager-ts@2.9.2(@parcel/core@2.9.3): - resolution: {integrity: sha512-vqVjMTmHA35wEPPXqqkuNykdZh+6g49sT/GZbCJtjJsqmDtVd5AEExgVMpCZ5+ZqPL3VEyjR46uhjrP+1PKvAg==} - engines: {node: '>= 12.0.0', parcel: ^2.9.2} - dependencies: - '@parcel/plugin': 2.9.2(@parcel/core@2.9.3) - transitivePeerDependencies: - - '@parcel/core' - dev: true - - /@parcel/plugin@2.9.2(@parcel/core@2.9.3): - resolution: {integrity: sha512-5v4sdeD5Cft4Vg2D61HW9TK0oi50X2jrm0hVFbUbCG2/TPWs77BMN6Nq+dMV38wEaGbnPjRolxBtRp+ungF09w==} - engines: {node: '>= 12.0.0'} + /@parcel/packager-ts@2.9.3(@parcel/core@2.9.3): + resolution: {integrity: sha512-Vd9dm1FqaFDw/kWCh95zgGS08HvIpSLg5Aa+AIhFiM0G+kpRSItcBSNJVwC7JKmLk1rmQhmQKoCKX26+nvyAzA==} + engines: {node: '>= 12.0.0', parcel: ^2.9.3} dependencies: - '@parcel/types': 2.9.2(@parcel/core@2.9.3) + '@parcel/plugin': 2.9.3(@parcel/core@2.9.3) transitivePeerDependencies: - '@parcel/core' dev: true @@ -2083,15 +1969,6 @@ packages: - '@parcel/core' dev: true - /@parcel/profiler@2.9.2: - resolution: {integrity: sha512-C846buL+bmnP/F360rUp4I9dwkdUkVM+gFe/AK3JCjtA0TZQIysLqntIQ7g6JK8VUa3e9Q8GwmTfncPAFoiaNQ==} - engines: {node: '>= 12.0.0'} - dependencies: - '@parcel/diagnostic': 2.9.2 - '@parcel/events': 2.9.2 - chrome-trace-event: 1.0.3 - dev: true - /@parcel/profiler@2.9.3: resolution: {integrity: sha512-pyHc9lw8VZDfgZoeZWZU9J0CVEv1Zw9O5+e0DJPDPHuXJYr72ZAOhbljtU3owWKAeW+++Q2AZWkbUGEOjI/e6g==} engines: {node: '>= 12.0.0'} @@ -2355,25 +2232,25 @@ packages: - '@parcel/core' dev: true - /@parcel/transformer-typescript-types@2.9.2(@parcel/core@2.9.3)(typescript@5.0.4): - resolution: {integrity: sha512-DCWx42Lg2XqwXf90TFCD+uV44GYat69NkTPvilrcr3gws/4y3l978cYu0Q0FShOLWOfteTWWuL+sD0fDIBZDKQ==} - engines: {node: '>= 12.0.0', parcel: ^2.9.2} + /@parcel/transformer-typescript-types@2.9.3(@parcel/core@2.9.3)(typescript@5.0.4): + resolution: {integrity: sha512-W+Ze3aUTdZuBQokXlkEQ/1hUApUm6VRyYzPqEs9jcqCqU8mv18i5ZGAz4bMuIJOBprp7M2wt10SJJx/SC1pl1A==} + engines: {node: '>= 12.0.0', parcel: ^2.9.3} peerDependencies: typescript: '>=3.0.0' dependencies: - '@parcel/diagnostic': 2.9.2 - '@parcel/plugin': 2.9.2(@parcel/core@2.9.3) + '@parcel/diagnostic': 2.9.3 + '@parcel/plugin': 2.9.3(@parcel/core@2.9.3) '@parcel/source-map': 2.1.1 - '@parcel/ts-utils': 2.9.2(typescript@5.0.4) - '@parcel/utils': 2.9.2 + '@parcel/ts-utils': 2.9.3(typescript@5.0.4) + '@parcel/utils': 2.9.3 nullthrows: 1.1.1 typescript: 5.0.4 transitivePeerDependencies: - '@parcel/core' dev: true - /@parcel/ts-utils@2.9.2(typescript@5.0.4): - resolution: {integrity: sha512-Julkwe/iJ/CWrL+/s0L/LUq7FWyB0bRvd7G1qnL1cGjCuspD6qSp6Ko4xLmSYakco8FTlIU8VRbRTBs9UHOIEQ==} + /@parcel/ts-utils@2.9.3(typescript@5.0.4): + resolution: {integrity: sha512-MiQoXFV8I4IWZT/q5yolKN/gnEY5gZfGB2X7W9WHJbRgyjlT/A5cPERXzVBj6mc3/VM1GdZJz76w637GUcQhow==} engines: {node: '>= 12.0.0'} peerDependencies: typescript: '>=3.0.0' @@ -2382,20 +2259,6 @@ packages: typescript: 5.0.4 dev: true - /@parcel/types@2.9.2(@parcel/core@2.9.3): - resolution: {integrity: sha512-i8WOfWuvBQ88Q0frgJOmIPOZDUZ6BaGtyq+seo0B1Y0Bt04/KF4qPFo9E1umpL8ZgtA1kMtyZd1gsSmXLP5COw==} - dependencies: - '@parcel/cache': 2.9.2(@parcel/core@2.9.3) - '@parcel/diagnostic': 2.9.2 - '@parcel/fs': 2.9.2(@parcel/core@2.9.3) - '@parcel/package-manager': 2.9.2(@parcel/core@2.9.3) - '@parcel/source-map': 2.1.1 - '@parcel/workers': 2.9.2(@parcel/core@2.9.3) - utility-types: 3.10.0 - transitivePeerDependencies: - - '@parcel/core' - dev: true - /@parcel/types@2.9.3(@parcel/core@2.9.3): resolution: {integrity: sha512-NSNY8sYtRhvF1SqhnIGgGvJocyWt1K8Tnw5cVepm0g38ywtX6mwkBvMkmeehXkII4mSUn+frD9wGsydTunezvA==} dependencies: @@ -2410,20 +2273,6 @@ packages: - '@parcel/core' dev: true - /@parcel/utils@2.9.2: - resolution: {integrity: sha512-Gvl23c54ZYmBmXqpk7Kbw1S6+taWncgdqTo+XaokOzh3jjih1bmMVSMS+CwtBrYhPZ32x84JNeOxsxz01zsrAA==} - engines: {node: '>= 12.0.0'} - dependencies: - '@parcel/codeframe': 2.9.2 - '@parcel/diagnostic': 2.9.2 - '@parcel/hash': 2.9.2 - '@parcel/logger': 2.9.2 - '@parcel/markdown-ansi': 2.9.2 - '@parcel/source-map': 2.1.1 - chalk: 4.1.2 - nullthrows: 1.1.1 - dev: true - /@parcel/utils@2.9.3: resolution: {integrity: sha512-cesanjtj/oLehW8Waq9JFPmAImhoiHX03ihc3JTWkrvJYSbD7wYKCDgPAM3JiRAqvh1LZ6P699uITrYWNoRLUg==} engines: {node: '>= 12.0.0'} @@ -2449,21 +2298,6 @@ packages: node-gyp-build: 4.6.0 dev: true - /@parcel/workers@2.9.2(@parcel/core@2.9.3): - resolution: {integrity: sha512-38jd6jWMPNx41gWSJVtdRiTfE+6TvLfM35mkGg3ykpESk8QwwumaV2CLgvdfnFjxeUlRtOGi8m+bWiWqKJetww==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@parcel/core': ^2.9.2 - dependencies: - '@parcel/core': 2.9.3 - '@parcel/diagnostic': 2.9.2 - '@parcel/logger': 2.9.2 - '@parcel/profiler': 2.9.2 - '@parcel/types': 2.9.2(@parcel/core@2.9.3) - '@parcel/utils': 2.9.2 - nullthrows: 1.1.1 - dev: true - /@parcel/workers@2.9.3(@parcel/core@2.9.3): resolution: {integrity: sha512-zRrDuZJzTevrrwElYosFztgldhqW6G9q5zOeQXfVQFkkEJCNfg36ixeiofKRU8uu2x+j+T6216mhMNB6HiuY+w==} engines: {node: '>= 12.0.0'}