@warp-drive/experiments 0.2.7-alpha.9 → 0.2.7-beta.1

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +2 -1
  3. package/declarations/data-worker.d.ts +35 -2
  4. package/declarations/data-worker.d.ts.map +1 -0
  5. package/declarations/document-storage-CwwTQ49-.d.ts +91 -0
  6. package/declarations/document-storage-CwwTQ49-.d.ts.map +1 -0
  7. package/declarations/document-storage.d.ts +2 -1
  8. package/declarations/image-fetch.d.ts +60 -1
  9. package/declarations/image-fetch.d.ts.map +1 -0
  10. package/declarations/image-worker.d.ts +40 -1
  11. package/declarations/image-worker.d.ts.map +1 -0
  12. package/declarations/storage.d.ts +359 -0
  13. package/declarations/storage.d.ts.map +1 -0
  14. package/declarations/types-CRs-Qww6.d.ts +17 -0
  15. package/declarations/types-CRs-Qww6.d.ts.map +1 -0
  16. package/declarations/worker-fetch.d.ts +23 -1
  17. package/declarations/worker-fetch.d.ts.map +1 -0
  18. package/dist/data-worker.js +235 -339
  19. package/dist/data-worker.js.map +1 -0
  20. package/dist/document-storage-LM925Stz.js +270 -0
  21. package/dist/document-storage-LM925Stz.js.map +1 -0
  22. package/dist/document-storage.js +3 -1
  23. package/dist/image-fetch.js +108 -79
  24. package/dist/image-fetch.js.map +1 -0
  25. package/dist/image-worker.js +95 -95
  26. package/dist/image-worker.js.map +1 -0
  27. package/dist/storage.js +914 -0
  28. package/dist/storage.js.map +1 -0
  29. package/dist/unpkg/dev/data-worker.js +231 -338
  30. package/dist/unpkg/dev/data-worker.js.map +1 -0
  31. package/dist/unpkg/dev/document-storage-DkiEg9rY.js +268 -0
  32. package/dist/unpkg/dev/document-storage-DkiEg9rY.js.map +1 -0
  33. package/dist/unpkg/dev/document-storage.js +2 -348
  34. package/dist/unpkg/dev/image-fetch.js +104 -71
  35. package/dist/unpkg/dev/image-fetch.js.map +1 -0
  36. package/dist/unpkg/dev/image-worker.js +93 -94
  37. package/dist/unpkg/dev/image-worker.js.map +1 -0
  38. package/dist/unpkg/dev/storage.js +913 -0
  39. package/dist/unpkg/dev/storage.js.map +1 -0
  40. package/dist/unpkg/dev/worker-fetch.js +112 -145
  41. package/dist/unpkg/dev/worker-fetch.js.map +1 -0
  42. package/dist/unpkg/dev-deprecated/data-worker.js +231 -338
  43. package/dist/unpkg/dev-deprecated/data-worker.js.map +1 -0
  44. package/dist/unpkg/dev-deprecated/document-storage-DkiEg9rY.js +268 -0
  45. package/dist/unpkg/dev-deprecated/document-storage-DkiEg9rY.js.map +1 -0
  46. package/dist/unpkg/dev-deprecated/document-storage.js +2 -348
  47. package/dist/unpkg/dev-deprecated/image-fetch.js +104 -71
  48. package/dist/unpkg/dev-deprecated/image-fetch.js.map +1 -0
  49. package/dist/unpkg/dev-deprecated/image-worker.js +93 -94
  50. package/dist/unpkg/dev-deprecated/image-worker.js.map +1 -0
  51. package/dist/unpkg/dev-deprecated/storage.js +913 -0
  52. package/dist/unpkg/dev-deprecated/storage.js.map +1 -0
  53. package/dist/unpkg/dev-deprecated/worker-fetch.js +112 -145
  54. package/dist/unpkg/dev-deprecated/worker-fetch.js.map +1 -0
  55. package/dist/unpkg/prod/data-worker.js +224 -324
  56. package/dist/unpkg/prod/data-worker.js.map +1 -0
  57. package/dist/unpkg/prod/document-storage-D0k_6GM9.js +262 -0
  58. package/dist/unpkg/prod/document-storage-D0k_6GM9.js.map +1 -0
  59. package/dist/unpkg/prod/document-storage.js +2 -338
  60. package/dist/unpkg/prod/image-fetch.js +101 -71
  61. package/dist/unpkg/prod/image-fetch.js.map +1 -0
  62. package/dist/unpkg/prod/image-worker.js +93 -94
  63. package/dist/unpkg/prod/image-worker.js.map +1 -0
  64. package/dist/unpkg/prod/storage.js +844 -0
  65. package/dist/unpkg/prod/storage.js.map +1 -0
  66. package/dist/unpkg/prod/worker-fetch.js +109 -145
  67. package/dist/unpkg/prod/worker-fetch.js.map +1 -0
  68. package/dist/unpkg/prod-deprecated/data-worker.js +224 -324
  69. package/dist/unpkg/prod-deprecated/data-worker.js.map +1 -0
  70. package/dist/unpkg/prod-deprecated/document-storage-D0k_6GM9.js +262 -0
  71. package/dist/unpkg/prod-deprecated/document-storage-D0k_6GM9.js.map +1 -0
  72. package/dist/unpkg/prod-deprecated/document-storage.js +2 -338
  73. package/dist/unpkg/prod-deprecated/image-fetch.js +101 -71
  74. package/dist/unpkg/prod-deprecated/image-fetch.js.map +1 -0
  75. package/dist/unpkg/prod-deprecated/image-worker.js +93 -94
  76. package/dist/unpkg/prod-deprecated/image-worker.js.map +1 -0
  77. package/dist/unpkg/prod-deprecated/storage.js +844 -0
  78. package/dist/unpkg/prod-deprecated/storage.js.map +1 -0
  79. package/dist/unpkg/prod-deprecated/worker-fetch.js +109 -145
  80. package/dist/unpkg/prod-deprecated/worker-fetch.js.map +1 -0
  81. package/dist/worker-fetch.js +116 -153
  82. package/dist/worker-fetch.js.map +1 -0
  83. package/package.json +9 -9
  84. package/declarations/data-worker/cache-handler.d.ts +0 -7
  85. package/declarations/data-worker/fetch.d.ts +0 -21
  86. package/declarations/data-worker/types.d.ts +0 -31
  87. package/declarations/data-worker/utils.d.ts +0 -12
  88. package/declarations/data-worker/worker.d.ts +0 -23
  89. package/declarations/document-storage/index.d.ts +0 -90
  90. package/declarations/image-worker/fetch.d.ts +0 -18
  91. package/declarations/image-worker/types.d.ts +0 -21
  92. package/declarations/image-worker/worker.d.ts +0 -14
  93. package/dist/index-CGCX7hY2.js +0 -349
@@ -0,0 +1,844 @@
1
+ import { defineSignal, entangleSignal, getOrCreateInternalSignal, makeInitializer, memoized, notifyInternalSignal, withSignalStore } from "@warp-drive/core/signals/-leaked";
2
+
3
+ //#region src/storage/-private/reactive-map.ts
4
+ /**
5
+ * A reactive wrapper around the browser's Map API that provides
6
+ * granular per-key reactivity via WarpDrive's signal system.
7
+ */
8
+ var SignalMap = class {
9
+ _map = {};
10
+ _signals = withSignalStore(this._map);
11
+ subscribe(key) {
12
+ const existing = this._signals.has(key);
13
+ entangleSignal(this._signals, this._map, key, void 0);
14
+ const size = this._size;
15
+ this._size = existing ? size : size + 1;
16
+ return existing;
17
+ }
18
+ notify(key) {
19
+ const signal = getOrCreateInternalSignal(this._signals, this._map, key, 0);
20
+ notifyInternalSignal(signal);
21
+ }
22
+ clear() {
23
+ for (const value of this._signals.values()) notifyInternalSignal(value);
24
+ this._size = 0;
25
+ }
26
+ get size() {
27
+ return this._size;
28
+ }
29
+ };
30
+ defineSignal(SignalMap.prototype, "_size", 0);
31
+
32
+ //#endregion
33
+ //#region src/storage/cache.ts
34
+ const DEFAULT_CACHE_ID = "reactive-cache";
35
+ const CACHES = /* @__PURE__ */ new Map();
36
+ const StorageEvents = new BroadcastChannel("reactive-cache-events");
37
+ const CACHE_URL = `${location.origin}/api/reactive-cache.json`;
38
+ /**
39
+ * Emits a {@link StorageEvent} on the local context to
40
+ * match the broadcast event.
41
+ */
42
+ function emitStorageEvent(event) {
43
+ window.dispatchEvent(new CustomEvent("storage", { detail: event }));
44
+ }
45
+ /**
46
+ * Updates the cache and re-emits the event on the local context as a StorageEvent
47
+ * This is necessary to trigger the reactive updates in the current tab.
48
+ */
49
+ function handleCacheEvent(event) {
50
+ const { data } = event;
51
+ const cache = CACHES.get(data.storageArea);
52
+ if (!cache) return;
53
+ if (data.key === null) {
54
+ cache._data = /* @__PURE__ */ new Map();
55
+ emitStorageEvent({
56
+ storageArea: cache,
57
+ key: null,
58
+ oldValue: null,
59
+ newValue: null
60
+ });
61
+ return;
62
+ }
63
+ if (data.newValue === null) cache._data.delete(data.key);
64
+ else cache._data.set(data.key, data.newValue);
65
+ emitStorageEvent({
66
+ storageArea: cache,
67
+ key: data.key,
68
+ oldValue: data.oldValue,
69
+ newValue: data.newValue
70
+ });
71
+ }
72
+ StorageEvents.addEventListener("message", handleCacheEvent);
73
+ /**
74
+ * A reactive interface for json stored in the browser [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) API.
75
+ *
76
+ * This is a good option for larger data sets than can be efficiently stored in localStorage
77
+ * but should not be used as a permanent DB or storage solution.
78
+ */
79
+ var CacheStorage = class CacheStorage {
80
+ #cache;
81
+ #cacheId;
82
+ #inititalization;
83
+ _data = /* @__PURE__ */ new Map();
84
+ _nextUpdate = null;
85
+ _bufferedEvents = [];
86
+ async #initialize() {
87
+ const cache = await caches.open(this.#cacheId);
88
+ this.#cache = cache;
89
+ this._data = await deserializeFromCache(cache);
90
+ return this;
91
+ }
92
+ constructor(cacheId) {
93
+ this.#cacheId = cacheId;
94
+ this.#cache = null;
95
+ this.#inititalization = this.#initialize();
96
+ }
97
+ get length() {
98
+ return this._data.size;
99
+ }
100
+ #update(key, oldValue, newValue) {
101
+ const cache = this.#cache;
102
+ if (!cache) throw new Error("Cache not initialized");
103
+ this._bufferedEvents.push({
104
+ storageArea: this.#cacheId,
105
+ key,
106
+ oldValue,
107
+ newValue
108
+ });
109
+ if (!this._nextUpdate) this._nextUpdate = window.setTimeout(() => {
110
+ const events = this._bufferedEvents;
111
+ this._bufferedEvents = [];
112
+ this._nextUpdate = null;
113
+ serializeToCache(cache, this._data);
114
+ for (const event of events) StorageEvents.postMessage(event);
115
+ }, 0);
116
+ }
117
+ clear() {
118
+ this._data = /* @__PURE__ */ new Map();
119
+ this.#update(null, null, null);
120
+ }
121
+ getItem(key) {
122
+ return this._data.get(key) ?? null;
123
+ }
124
+ key(index) {
125
+ let i = 0;
126
+ for (const value of this._data.keys()) {
127
+ if (i === index) return value;
128
+ i++;
129
+ }
130
+ return null;
131
+ }
132
+ removeItem(key) {
133
+ if (this._data.has(key)) {
134
+ const oldValue = this._data.get(key) ?? null;
135
+ this._data.delete(key);
136
+ this.#update(key, oldValue, null);
137
+ }
138
+ }
139
+ setItem(key, value) {
140
+ const oldValue = this._data.get(key) ?? null;
141
+ if (oldValue === value) return;
142
+ this._data.set(key, value);
143
+ this.#update(key, oldValue, value);
144
+ }
145
+ /**
146
+ * Get the singleton CacheStorage instance.
147
+ *
148
+ */
149
+ static get(cacheId = DEFAULT_CACHE_ID) {
150
+ let cache = CACHES.get(cacheId);
151
+ if (!cache) {
152
+ cache = new CacheStorage(cacheId);
153
+ CACHES.set(cacheId, cache);
154
+ }
155
+ return cache.#inititalization;
156
+ }
157
+ static expectCache(cacheId = DEFAULT_CACHE_ID) {
158
+ const cache = CACHES.get(cacheId);
159
+ if (!cache) throw new Error(`Cache with id ${cacheId} not found. Make sure to call CacheStorage.get(${cacheId}) first.`);
160
+ return cache;
161
+ }
162
+ /**
163
+ * Returns the IDs of all {@link CacheStorage} instances that have been
164
+ * opened in this context via {@link CacheStorage.get}.
165
+ */
166
+ static getAllCacheIds() {
167
+ return Array.from(CACHES.keys());
168
+ }
169
+ };
170
+ function serializeToCache(cache, data) {
171
+ const entries = Array.from(data.entries());
172
+ const blob = JSON.stringify({
173
+ version: 1,
174
+ data: entries
175
+ });
176
+ const response = new Response(blob);
177
+ return cache.put(CACHE_URL, response);
178
+ }
179
+ function deserializeFromCache(cache) {
180
+ return cache.match(CACHE_URL).then((response) => {
181
+ if (!response) return /* @__PURE__ */ new Map();
182
+ return response.json().then((json) => {
183
+ if (json.version !== 1) throw new Error(`Unsupported cache version: ${json.version}`);
184
+ return new Map(json.data);
185
+ });
186
+ });
187
+ }
188
+
189
+ //#endregion
190
+ //#region src/storage/storage.ts
191
+ let localStorageInstance = null;
192
+ let sessionStorageInstance = null;
193
+ let localStorageOptions = {};
194
+ let sessionStorageOptions = {};
195
+ /**
196
+ * Retrieves the singleton instance of the LocalStorage service.
197
+ *
198
+ * If the instance does not already exist, it is created.
199
+ */
200
+ function getLocalStorage() {
201
+ if (!localStorageInstance) localStorageInstance = new ReactiveStorage(localStorage, localStorageOptions);
202
+ return localStorageInstance;
203
+ }
204
+ function getSessionStorage() {
205
+ if (!sessionStorageInstance) sessionStorageInstance = new ReactiveStorage(sessionStorage, sessionStorageOptions);
206
+ return sessionStorageInstance;
207
+ }
208
+ const CacheStorages = /* @__PURE__ */ new Map();
209
+ function getCacheStorage(namespace = null) {
210
+ const key = namespace ?? "reactive-cache";
211
+ let cache = CacheStorages.get(key);
212
+ if (!cache) {
213
+ const storage = CacheStorage.expectCache(key);
214
+ cache = new ReactiveStorage(storage);
215
+ CacheStorages.set(key, cache);
216
+ }
217
+ return cache;
218
+ }
219
+ /**
220
+ * Configure options for the localStorage singleton.
221
+ * Must be called before getLocalStorage() is first invoked.
222
+ */
223
+ function configureLocalStorage(options) {
224
+ localStorageOptions = options;
225
+ }
226
+ /**
227
+ * Configure options for the sessionStorage singleton.
228
+ * Must be called before getSessionStorage() is first invoked.
229
+ */
230
+ function configureSessionStorage(options) {
231
+ sessionStorageOptions = options;
232
+ }
233
+ /**
234
+ * Check if an error is a quota exceeded error
235
+ */
236
+ function isQuotaExceededError(error) {
237
+ if (error instanceof DOMException) {
238
+ if (error.name === "QuotaExceededError") return true;
239
+ if (error.name === "NS_ERROR_DOM_QUOTA_REACHED") return true;
240
+ }
241
+ return false;
242
+ }
243
+ /**
244
+ * Check if an error indicates storage is unavailable (private browsing, etc.)
245
+ */
246
+ function isStorageUnavailableError(error) {
247
+ if (error instanceof DOMException) {
248
+ if (error.name === "SecurityError") return true;
249
+ if (error.name === "InvalidStateError") return true;
250
+ }
251
+ return false;
252
+ }
253
+ /**
254
+ * A reactive wrapper around the Web Storage API (localStorage/sessionStorage)
255
+ * that provides signal-based access to storage items and length.
256
+ *
257
+ * Will automatically update when storage events occur in other tabs/windows.
258
+ */
259
+ var ReactiveStorage = class {
260
+ _storage;
261
+ _options;
262
+ _memoryOnly = false;
263
+ _values = {};
264
+ _effects = /* @__PURE__ */ new Map();
265
+ setEffect(key, fn) {
266
+ this._effects.set(key, fn);
267
+ }
268
+ constructor(storage, options = {}) {
269
+ this._storage = storage;
270
+ this._options = options;
271
+ if (!(storage instanceof CacheStorage)) try {
272
+ const testKey = "__storage_test__";
273
+ storage.setItem(testKey, testKey);
274
+ storage.removeItem(testKey);
275
+ this._length = storage.length;
276
+ } catch (error) {
277
+ if (options.fallbackToMemory && isStorageUnavailableError(error)) {
278
+ this._memoryOnly = true;
279
+ this._length = 0;
280
+ } else throw error;
281
+ }
282
+ if (!this._memoryOnly) window.addEventListener("storage", (event) => {
283
+ const data = "detail" in event ? event.detail : event;
284
+ if (data.storageArea === storage) {
285
+ this._values[data.key] = data.newValue;
286
+ this._signals.notify(data.key);
287
+ this._length = this._storage.length;
288
+ const effect = this._effects.get(data.key);
289
+ if (effect) effect(data);
290
+ }
291
+ });
292
+ }
293
+ /**
294
+ * Reactive access to the number of keys in Storage
295
+ */
296
+ get length() {
297
+ return this._length;
298
+ }
299
+ /**
300
+ * Non-reactive way to peek the current value of a key in Storage
301
+ */
302
+ peekItem(key) {
303
+ const keyStr = String(key);
304
+ if (this._memoryOnly) return this._values[keyStr] ?? null;
305
+ const value = this._values[keyStr];
306
+ if (value !== void 0) return value;
307
+ return this._storage.getItem(keyStr);
308
+ }
309
+ /**
310
+ * Reactive access to Storage contents
311
+ */
312
+ getItem(key) {
313
+ const keyStr = String(key);
314
+ if (this._memoryOnly) {
315
+ this._signals.subscribe(keyStr);
316
+ return this._values[keyStr] ?? null;
317
+ }
318
+ const value = this._values[keyStr];
319
+ if (value !== void 0) {
320
+ this._signals.subscribe(keyStr);
321
+ return value;
322
+ }
323
+ const item = this._storage.getItem(keyStr);
324
+ this._values[keyStr] = item;
325
+ this._signals.subscribe(keyStr);
326
+ return item;
327
+ }
328
+ /**
329
+ * Set a value in Storage, triggering reactivity
330
+ */
331
+ setItem(key, value) {
332
+ const keyStr = String(key);
333
+ const valueStr = String(value);
334
+ if (this._values[keyStr] === valueStr) return;
335
+ if (this._memoryOnly) {
336
+ const currentValue = this._values[keyStr];
337
+ if (currentValue === null || currentValue === void 0) this._length += 1;
338
+ this._values[keyStr] = valueStr;
339
+ this._signals.notify(keyStr);
340
+ return;
341
+ }
342
+ try {
343
+ this._storage.setItem(keyStr, valueStr);
344
+ this._values[keyStr] = valueStr;
345
+ this._signals.notify(keyStr);
346
+ this._length = this._storage.length;
347
+ } catch (error) {
348
+ if (isQuotaExceededError(error)) {
349
+ if (this._options.updateOnQuotaExceeded) {
350
+ this._values[keyStr] = valueStr;
351
+ this._signals.notify(keyStr);
352
+ }
353
+ if (this._options.onQuotaExceeded) {
354
+ const result = this._options.onQuotaExceeded(keyStr, valueStr);
355
+ if (result instanceof Promise ? false : result) {
356
+ this._storage.setItem(keyStr, valueStr);
357
+ this._length = this._storage.length;
358
+ }
359
+ } else throw error;
360
+ } else throw error;
361
+ }
362
+ }
363
+ /**
364
+ * Remove a value from Storage, triggering reactivity
365
+ */
366
+ removeItem(key) {
367
+ const keyStr = String(key);
368
+ if (this._memoryOnly) {
369
+ const value = this._values[keyStr];
370
+ if (value !== null && value !== void 0) this._length -= 1;
371
+ this._values[keyStr] = null;
372
+ return;
373
+ }
374
+ this._storage.removeItem(keyStr);
375
+ this._values[keyStr] = null;
376
+ this._signals.notify(keyStr);
377
+ this._length = this._storage.length;
378
+ }
379
+ /**
380
+ * Clears all keys from Storage, triggering reactivity
381
+ */
382
+ clear() {
383
+ this._values = {};
384
+ this._signals.clear();
385
+ this._length = 0;
386
+ if (this._memoryOnly) return;
387
+ this._storage.clear();
388
+ }
389
+ /**
390
+ * Reactive access to the key at the given index
391
+ */
392
+ key(index) {
393
+ this._length;
394
+ if (this._memoryOnly) return Object.keys(this._values).filter((k) => this._values[k] !== null)[index] ?? null;
395
+ return this._storage.key(index);
396
+ }
397
+ };
398
+ defineSignal(ReactiveStorage.prototype, "_length", 0);
399
+ defineSignal(ReactiveStorage.prototype, "_signals", makeInitializer(() => new SignalMap()));
400
+
401
+ //#endregion
402
+ //#region src/storage/-private/storage-infra.ts
403
+ /**
404
+ * Configuration options for fields that are also query parameters
405
+ */
406
+ /**
407
+ * Metadata attached to persisted resource classes
408
+ */
409
+ /**
410
+ * A function which generates a unique primary-key
411
+ * string for a given LocalResource or SessionResource
412
+ * instance.
413
+ *
414
+ * Use functions when you want to create more than
415
+ * one instance of a resource type, each with its own
416
+ * persisted data.
417
+ */
418
+ /**
419
+ * Symbol used to store persistence metadata on classes
420
+ */
421
+ const PERSISTED_RESOURCE_META = Symbol("StorageResourceMeta");
422
+ /**
423
+ * Setup persisted resource metadata on target
424
+ * if not already present
425
+ *
426
+ * @private
427
+ */
428
+ function initMeta(target) {
429
+ let meta = target[PERSISTED_RESOURCE_META];
430
+ if (!meta) {
431
+ meta = {
432
+ id: "",
433
+ namespace: null,
434
+ pkFn: null,
435
+ type: "",
436
+ fields: /* @__PURE__ */ new Map(),
437
+ initializers: /* @__PURE__ */ new Map(),
438
+ paramConfigs: null,
439
+ paramCompanion: null,
440
+ instances: null,
441
+ typeOverrides: null
442
+ };
443
+ target[PERSISTED_RESOURCE_META] = meta;
444
+ }
445
+ return meta;
446
+ }
447
+ function useMeta(meta, instance) {
448
+ if (meta.id === "" && meta.pkFn) {
449
+ meta.instances = meta.instances ?? /* @__PURE__ */ new WeakMap();
450
+ let instanceMeta = meta.instances.get(instance);
451
+ if (!instanceMeta) {
452
+ instanceMeta = { ...meta };
453
+ const pk = meta.pkFn(instance);
454
+ instanceMeta.id = pk;
455
+ meta.instances.set(instance, instanceMeta);
456
+ }
457
+ return instanceMeta;
458
+ }
459
+ return meta;
460
+ }
461
+ /**
462
+ * Load storage field
463
+ */
464
+ function getField(instance, meta, key, overrideType) {
465
+ const stored = getStorage(overrideType || meta.type, meta.namespace).getItem(keyFor(meta, key));
466
+ if (stored) return JSON.parse(stored);
467
+ else {
468
+ const initializer = meta.initializers.get(key);
469
+ if (initializer) {
470
+ const value = initializer.call(instance);
471
+ if (value !== void 0) return value;
472
+ }
473
+ }
474
+ return null;
475
+ }
476
+ function keyFor(meta, key) {
477
+ return `persisted:${meta.id}:${key}`;
478
+ }
479
+ function getStorage(type, namespace) {
480
+ if (type === "local-resource") return getLocalStorage();
481
+ else if (type === "session-resource") return getSessionStorage();
482
+ else return getCacheStorage(namespace);
483
+ }
484
+ /**
485
+ * Update storage field
486
+ */
487
+ function setField(meta, key, value, overrideType) {
488
+ getStorage(overrideType || meta.type, meta.namespace).setItem(keyFor(meta, key), JSON.stringify(value));
489
+ }
490
+ function getResourceMeta(target) {
491
+ return target[PERSISTED_RESOURCE_META];
492
+ }
493
+ function _createStorageResource(id, type, namespace) {
494
+ return function(target) {
495
+ const meta = getResourceMeta(target.prototype);
496
+ meta.id = typeof id === "string" ? id : "";
497
+ meta.pkFn = typeof id === "function" ? id : null;
498
+ meta.type = type;
499
+ meta.namespace = namespace;
500
+ if (meta.pkFn !== null) {
501
+ const originalConstructor = target.prototype.constructor;
502
+ target.prototype.constructor = function DynamicStorageInitializer(...args) {
503
+ const instance = new originalConstructor(...args);
504
+ installEffectsForDynamicInstance(meta, instance);
505
+ return instance;
506
+ };
507
+ }
508
+ if (!("toJSON" in target.prototype)) target.prototype.toJSON = function() {
509
+ const instanceMeta = useMeta(meta, this);
510
+ const isLocalResource = meta.type === "local-resource" || meta.type === "cache-resource";
511
+ const fields = isLocalResource ? new Set(meta.fields.keys()) : /* @__PURE__ */ new Set();
512
+ for (const [key, overrideType] of meta.typeOverrides?.entries() ?? []) if (overrideType === "local-storage") fields.add(key);
513
+ else if (overrideType === "session-storage" && isLocalResource) fields.delete(key);
514
+ let idKey = "$key";
515
+ while (fields.has(idKey)) idKey = `_${idKey}`;
516
+ const data = { [idKey]: instanceMeta.id };
517
+ for (const key of fields) {
518
+ const value = getField(this, instanceMeta, key, meta.typeOverrides?.get(key) ?? null);
519
+ if (value !== null) data[key] = value;
520
+ }
521
+ return data;
522
+ };
523
+ };
524
+ }
525
+ async function installEffectsForDynamicInstance(meta, instance) {
526
+ await Promise.resolve();
527
+ for (const [key, value] of meta.fields.entries()) if (typeof value === "function") installEffect(useMeta(meta, instance), key);
528
+ }
529
+ /**
530
+ * Returns the descriptor but is cast
531
+ * to void to satisfy Typescript's incorrect
532
+ * typing for legacy decorators.
533
+ */
534
+ function setupField(target, key, orgDesc, type) {
535
+ const meta = initMeta(target);
536
+ meta.fields.set(key, null);
537
+ const overrideType = type === "local" ? "local-storage" : type === "session" ? "session-storage" : type === "cache" ? "cache-storage" : null;
538
+ if (overrideType) {
539
+ meta.typeOverrides = meta.typeOverrides || /* @__PURE__ */ new Map();
540
+ meta.typeOverrides.set(key, overrideType);
541
+ }
542
+ const initializer = orgDesc?.initializer || null;
543
+ meta.initializers.set(key, initializer);
544
+ return memoized(target, key, {
545
+ configurable: true,
546
+ enumerable: true,
547
+ get() {
548
+ return getField(this, useMeta(meta, this), key, overrideType);
549
+ },
550
+ set(value) {
551
+ setField(useMeta(meta, this), key, value, overrideType);
552
+ }
553
+ });
554
+ }
555
+ async function installEffect(meta, key) {
556
+ await Promise.resolve();
557
+ const effect = meta.fields.get(key);
558
+ getStorage(meta.type, meta.namespace).setEffect(keyFor(meta, key), (event) => {
559
+ const oldValue = event.oldValue ? JSON.parse(event.oldValue) : null;
560
+ const newValue = event.newValue ? JSON.parse(event.newValue) : null;
561
+ effect({
562
+ key,
563
+ from: oldValue,
564
+ to: newValue
565
+ });
566
+ });
567
+ }
568
+ /**
569
+ * Get or create the param companion object for a StorageResource instance.
570
+ *
571
+ * The companion object contains URL-serialized versions of all @param decorated fields.
572
+ * Each param field gets a corresponding property on the companion that:
573
+ * - Reads: Serializes the storage value to a URL string (returns null if not active)
574
+ * - Writes: Deserializes the URL string and updates the storage value
575
+ *
576
+ * The companion object is reactive using trackedObject, ensuring that changes
577
+ * to the underlying storage fields trigger updates in the query param system.
578
+ *
579
+ * This companion object is what QPRoute will bind to for URL query params.
580
+ *
581
+ * @private
582
+ * @param instance - The StorageResource instance
583
+ * @param groupControlMap - Optional map of fieldName -> controlFieldName for grouped params
584
+ * @returns The companion object with serialized param properties
585
+ */
586
+ function getParamCompanion(instance, groupControlMap) {
587
+ const meta = getResourceMeta(instance);
588
+ const instanceMeta = useMeta(meta, instance);
589
+ if (instanceMeta.paramCompanion) return instanceMeta.paramCompanion;
590
+ const companion = {};
591
+ instanceMeta.paramCompanion = companion;
592
+ const paramConfigs = instanceMeta.paramConfigs;
593
+ if (!paramConfigs || paramConfigs.size === 0) return companion;
594
+ for (const [fieldName, config] of paramConfigs.entries()) {
595
+ const controlFieldName = groupControlMap?.[fieldName];
596
+ createParamField(companion, fieldName, config, controlFieldName, instance, meta);
597
+ }
598
+ return companion;
599
+ }
600
+ function isActiveGroupParam(companion, controlFieldName) {
601
+ return companion[controlFieldName] !== null;
602
+ }
603
+ /**
604
+ * The default value of a param is determined by:
605
+ * - the getDefault() function if provided, and its return is not undefined
606
+ * - the initializer value provided to the field, if not undefined
607
+ * - null otherwise
608
+ *
609
+ * The value is the deserialized form (i.e., the local storage value type)
610
+ */
611
+ function getParamFieldDefaultValue(meta, config, fieldName, instance) {
612
+ const defaultValue = config.getDefault?.(instance);
613
+ if (defaultValue !== void 0) return defaultValue;
614
+ const initializer = meta.initializers.get(fieldName);
615
+ if (initializer) {
616
+ const initValue = initializer.call(instance);
617
+ if (initValue !== void 0) return initValue;
618
+ }
619
+ return null;
620
+ }
621
+ function createParamField(companion, fieldName, config, controlFieldName, instance, meta) {
622
+ const desc = {
623
+ configurable: true,
624
+ enumerable: true,
625
+ get() {
626
+ if (!config.initialized) {
627
+ config.initialized = true;
628
+ return null;
629
+ }
630
+ if (controlFieldName && !isActiveGroupParam(companion, controlFieldName)) return null;
631
+ const defaultValue = getParamFieldDefaultValue(meta, config, fieldName, instance);
632
+ const rawValue = instance[fieldName];
633
+ if (rawValue === defaultValue) return null;
634
+ const serialized = config.serialize(rawValue, instance);
635
+ if (!serialized) return null;
636
+ return serialized;
637
+ },
638
+ set(urlValue) {
639
+ /**
640
+ * set will never come from anything except URL deserialization.
641
+ * which is managed by Ember.
642
+ *
643
+ * Our job is to not unnecessarily update the storage resource
644
+ * so we need to run the compare function to see if the
645
+ * incoming URL value is different from the existing local value.
646
+ */
647
+ if (!urlValue) return;
648
+ const rawValue = instance[fieldName];
649
+ if (config.serialize(rawValue, instance) === urlValue) return;
650
+ instance[fieldName] = config.deserialize(urlValue, instance);
651
+ }
652
+ };
653
+ memoized(companion, fieldName, desc);
654
+ Object.defineProperty(companion, fieldName, desc);
655
+ }
656
+
657
+ //#endregion
658
+ //#region src/storage/storage-resource.ts
659
+ /**
660
+ * Decorator which transforms a class into a StorageResource
661
+ * persisted in localStorage.
662
+ *
663
+ * LocalResources must either be singletons or expect all instances
664
+ * to share state unless a primary key function is provided.
665
+ *
666
+ * When a primary key function is provided, each instance
667
+ * will have its own persisted data based on the key generated
668
+ * by the function.
669
+ *
670
+ * The function will be called once per instance during
671
+ * initialization to determine the unique ID for that instance.
672
+ */
673
+ function LocalResource(id) {
674
+ return _createStorageResource(id, "local-resource", null);
675
+ }
676
+ /**
677
+ * Decorator which transforms a class into a StorageResource
678
+ * persisted in sessionStorage.
679
+ *
680
+ * SessionResources must either be singletons or expect all instances
681
+ * to share state unless a primary key function is provided.
682
+ *
683
+ * When a primary key function is provided, each instance
684
+ * will have its own persisted data based on the key generated
685
+ * by the function.
686
+ *
687
+ * The function will be called once per instance during
688
+ * initialization to determine the unique ID for that instance.
689
+ */
690
+ function SessionResource(id) {
691
+ return _createStorageResource(id, "session-resource", null);
692
+ }
693
+ /**
694
+ * Decorator which transforms a class into a StorageResource
695
+ * persisted via the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
696
+ * api and shared across all tabs/windows under the same origin.
697
+ *
698
+ * CacheResources must either be singletons or expect all instances
699
+ * to share state unless a primary key function is provided.
700
+ *
701
+ * When a primary key function is provided, each instance
702
+ * will have its own persisted data based on the key generated
703
+ * by the function.
704
+ *
705
+ * The function will be called once per instance during
706
+ * initialization to determine the unique ID for that instance.
707
+ *
708
+ * All object cached in the same `namespace` share the namespace's storage context,
709
+ * so partitioning can be achieved by using different namespaces for different groups
710
+ * of data.
711
+ */
712
+ function CacheResource(id, namespace = null) {
713
+ return _createStorageResource(id, "cache-resource", namespace);
714
+ }
715
+ /**
716
+ * Decorator which marks a property as a field on
717
+ * a LocalResource or SessionResource
718
+ *
719
+ * The field's value will be initialized from the persisted resource data
720
+ * if available, falling back to the property's default value otherwise.
721
+ *
722
+ * Fields can be of any type that is serializable to and restorable from JSON,
723
+ * but complex types (like objects or arrays) should be handled with care to avoid
724
+ * unintended mutations or reactivity issues.
725
+ *
726
+ * By default, fields are persisted in the storage type defined by the resource decorator
727
+ * (@LocalResource or @SessionResource). However, you can override this behavior
728
+ * by passing 'local' or 'session' as an argument to the decorator.
729
+ *
730
+ * ---
731
+ *
732
+ * **Example:**
733
+ *
734
+ * ```ts
735
+ * @LocalResource('user-settings')
736
+ * class UserSettings {
737
+ * @field
738
+ * theme: 'light' | 'dark' = 'light';
739
+ *
740
+ * @field('session')
741
+ * sessionToken: string | null = null;
742
+ * }
743
+ * ```
744
+ *
745
+ */
746
+ function field(...args) {
747
+ if (args.length === 0) return setupField;
748
+ if (args.length === 1) {
749
+ const type = args[0];
750
+ return (target, key, desc) => setupField(target, key, desc, type);
751
+ }
752
+ if (args.length === 2 || args.length === 3) {
753
+ const [target, key, descriptor] = args;
754
+ return setupField(target, key, descriptor);
755
+ }
756
+ }
757
+ function input(type) {
758
+ return function(target, key, desc) {
759
+ const originalSet = desc.set;
760
+ desc.set = function(value) {
761
+ switch (type) {
762
+ case "number":
763
+ value = Number(value);
764
+ break;
765
+ case "boolean": value = value === "false" || value === "" || value === "0" || value === "undefined" || value === "null" || value === false || value === 0 || value === null || value === void 0 ? 0 : 1;
766
+ }
767
+ originalSet.call(this, value);
768
+ };
769
+ return desc;
770
+ };
771
+ }
772
+ /**
773
+ * Effects are fields that run a side-effecting function.
774
+ *
775
+ * Effects are intended to enable synchronizing get states between
776
+ * tabs or windows that result in needing to synchronize other
777
+ * non-reactive state.
778
+ *
779
+ * For example, when a user selects a light/dark mode theme preference
780
+ * that differs from the system preference, effects can be used to trigger
781
+ * DOM updates on the documentElement necessary to ensure its state is
782
+ * consistent with the persisted resource state and reactive application state.
783
+ *
784
+ * To do this without an effect would require either setting up your own
785
+ * storage event listeners or consuming the reactive state of the property
786
+ * in another effect-like API such as an Ember modifier or React useEffect,
787
+ *
788
+ * Effects *only* run when the stored value changes due to storage events
789
+ * emitted from other tabs or windows. They do not run when the property
790
+ * is updated in the same context.
791
+ *
792
+ * ---
793
+ *
794
+ * **Example:**
795
+ *
796
+ * ```ts
797
+ * @LocalResource('user-preferences')
798
+ * class UserPreferences {
799
+ * @effect(syncThemeToDOM)
800
+ * explicitThemePreference: 'light' | 'dark' | null = null;
801
+ *
802
+ * @matchMedia('(prefers-color-scheme: dark)')
803
+ * systemPrefersDarkMode: boolean = false;
804
+ * }
805
+ *
806
+ * function syncThemeToDOM(update: ValueTransition<'light' | 'dark' | null>): void {
807
+ * const newTheme = update.to;
808
+ * document.documentElement.style.colorScheme = newTheme ?? 'light dark';
809
+ *
810
+ * if (newTheme === 'dark') {
811
+ * document.documentElement.classList.add('dark-theme');
812
+ * document.documentElement.classList.remove('light-theme');
813
+ * } else if (newTheme === 'light') {
814
+ * document.documentElement.classList.add('light-theme');
815
+ * document.documentElement.classList.remove('dark-theme');
816
+ * } else {
817
+ * document.documentElement.classList.remove('light-theme');
818
+ * document.documentElement.classList.remove('dark-theme');
819
+ * }
820
+ * }
821
+ * ```
822
+ */
823
+ function effect(fn, type) {
824
+ const overrideType = type === "local" ? "local-storage" : type === "session" ? "session-storage" : null;
825
+ return function effectField(target, key, _descriptor) {
826
+ const meta = initMeta(target);
827
+ meta.fields.set(key, fn);
828
+ if (meta.pkFn === null) installEffect(meta, key);
829
+ return {
830
+ configurable: true,
831
+ enumerable: true,
832
+ get() {
833
+ return getField(this, useMeta(meta, this), key, overrideType);
834
+ },
835
+ set(value) {
836
+ setField(useMeta(meta, this), key, value, overrideType);
837
+ }
838
+ };
839
+ };
840
+ }
841
+
842
+ //#endregion
843
+ export { CacheResource, CacheStorage, DEFAULT_CACHE_ID, LocalResource, SessionResource, getParamCompanion as _getParamCompanion, initMeta as _initMeta, configureLocalStorage, configureSessionStorage, effect, field, getCacheStorage, getLocalStorage, getSessionStorage, input };
844
+ //# sourceMappingURL=storage.js.map