gjendje 1.3.0 → 1.3.2

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.
@@ -4,7 +4,14 @@
4
4
  var globalConfig = {};
5
5
  var PERSISTENT_SCOPES = /* @__PURE__ */ new Set(["local", "session", "bucket"]);
6
6
  function configure(config) {
7
- globalConfig = { ...globalConfig, ...config };
7
+ for (const key of Object.keys(config)) {
8
+ const value = config[key];
9
+ if (value === void 0) {
10
+ delete globalConfig[key];
11
+ } else {
12
+ globalConfig[key] = value;
13
+ }
14
+ }
8
15
  if (globalConfig.registry === false && globalConfig.scope && PERSISTENT_SCOPES.has(globalConfig.scope)) {
9
16
  log(
10
17
  "warn",
@@ -12,6 +19,9 @@ function configure(config) {
12
19
  );
13
20
  }
14
21
  }
22
+ function resetConfig() {
23
+ globalConfig = {};
24
+ }
15
25
  function getConfig() {
16
26
  return globalConfig;
17
27
  }
@@ -102,16 +112,27 @@ function unregisterByKey(rKey) {
102
112
  function getRegistry() {
103
113
  return registry;
104
114
  }
115
+ function destroyAll() {
116
+ const instances = [...registry.values()];
117
+ for (const instance of instances) {
118
+ if (!instance.isDestroyed) {
119
+ instance.destroy();
120
+ }
121
+ }
122
+ registry.clear();
123
+ }
105
124
 
106
125
  exports.PERSISTENT_SCOPES = PERSISTENT_SCOPES;
107
126
  exports.configure = configure;
108
127
  exports.createListeners = createListeners;
128
+ exports.destroyAll = destroyAll;
109
129
  exports.getConfig = getConfig;
110
130
  exports.getRegistered = getRegistered;
111
131
  exports.getRegistry = getRegistry;
112
132
  exports.log = log;
113
133
  exports.registerNew = registerNew;
114
134
  exports.reportError = reportError;
135
+ exports.resetConfig = resetConfig;
115
136
  exports.safeCall = safeCall;
116
137
  exports.safeCallChange = safeCallChange;
117
138
  exports.safeCallConfig = safeCallConfig;
@@ -1,4 +1,4 @@
1
- import { safeCall, getConfig, log, scopedKey, getRegistered, registerNew, PERSISTENT_SCOPES, reportError, safeCallConfig, safeCallChange, unregisterByKey, createListeners } from './chunk-FAISWCIZ.js';
1
+ import { safeCall, getConfig, log, scopedKey, getRegistered, registerNew, PERSISTENT_SCOPES, reportError, safeCallConfig, safeCallChange, unregisterByKey, createListeners } from './chunk-YPT6TO4H.js';
2
2
 
3
3
  // src/batch.ts
4
4
  var depth = 0;
@@ -24,8 +24,27 @@ function notify(fn) {
24
24
  }
25
25
  fn();
26
26
  }
27
+ var MAX_FLUSH_ITERATIONS = 100;
27
28
  function flush() {
29
+ let iterations = 0;
28
30
  while (queue.length > 0) {
31
+ if (++iterations > MAX_FLUSH_ITERATIONS) {
32
+ console.error(
33
+ "[gjendje] Batch flush exceeded maximum iterations \u2014 possible infinite loop. Delivering remaining notifications once before stopping."
34
+ );
35
+ const remaining = queue;
36
+ queue = [];
37
+ for (let i = 0; i < remaining.length; i++) {
38
+ const fn = remaining[i];
39
+ try {
40
+ if (fn) fn();
41
+ } catch (err) {
42
+ console.error("[gjendje] Notification threw during best-effort delivery:", err);
43
+ }
44
+ }
45
+ queue = [];
46
+ break;
47
+ }
29
48
  generation++;
30
49
  const current = queue;
31
50
  queue = [];
@@ -64,6 +83,28 @@ function shallowEqual(a, b) {
64
83
  return true;
65
84
  }
66
85
  if (Array.isArray(b)) return false;
86
+ if (a instanceof Set) {
87
+ if (!(b instanceof Set)) return false;
88
+ if (a.size !== b.size) return false;
89
+ for (const item of a) {
90
+ if (!b.has(item)) return false;
91
+ }
92
+ return true;
93
+ }
94
+ if (a instanceof Map) {
95
+ if (!(b instanceof Map)) return false;
96
+ if (a.size !== b.size) return false;
97
+ for (const [key, val] of a) {
98
+ if (!b.has(key) || !Object.is(val, b.get(key))) return false;
99
+ }
100
+ return true;
101
+ }
102
+ if (a instanceof Date) {
103
+ return b instanceof Date && a.getTime() === b.getTime();
104
+ }
105
+ if (a instanceof RegExp) {
106
+ return b instanceof RegExp && a.toString() === b.toString();
107
+ }
67
108
  const objA = a;
68
109
  const objB = b;
69
110
  const keysA = Object.keys(objA);
@@ -210,9 +251,9 @@ function notifyWatchers(watchers, prev, next) {
210
251
  // src/persist.ts
211
252
  function isVersionedValue(value) {
212
253
  if (!isRecord(value)) return false;
213
- return "v" in value && "data" in value && Number.isSafeInteger(value.v);
254
+ return Object.keys(value).length === 2 && "v" in value && "data" in value && Number.isSafeInteger(value.v);
214
255
  }
215
- function readAndMigrate(raw, options, key, scope) {
256
+ function readAndMigrate(raw, options, key, scope, onFallback) {
216
257
  const currentVersion = options.version ?? 1;
217
258
  const defaultValue = options.default;
218
259
  try {
@@ -244,6 +285,7 @@ function readAndMigrate(raw, options, key, scope) {
244
285
  const validationErr = new ValidationError(key, scope, data);
245
286
  safeCallConfig(config.onError, { key, scope, error: validationErr });
246
287
  }
288
+ onFallback?.();
247
289
  return defaultValue;
248
290
  }
249
291
  return data;
@@ -253,6 +295,7 @@ function readAndMigrate(raw, options, key, scope) {
253
295
  const readErr = new StorageReadError(key, scope, err);
254
296
  safeCallConfig(getConfig().onError, { key, scope, error: readErr });
255
297
  }
298
+ onFallback?.();
256
299
  return defaultValue;
257
300
  }
258
301
  }
@@ -290,12 +333,12 @@ function runMigrations(data, fromVersion, toVersion, migrations, key, scope) {
290
333
  try {
291
334
  current = migrateFn(current);
292
335
  } catch (err) {
293
- log("warn", `Migration from v${v} failed \u2014 returning partially migrated value.`);
336
+ log("warn", `Migration from v${v} failed \u2014 discarding partially migrated data.`);
337
+ const migrationErr = new MigrationError(key ?? "", scope ?? "memory", v, toVersion, err);
294
338
  if (key && scope) {
295
- const migrationErr = new MigrationError(key, scope, v, toVersion, err);
296
339
  safeCallConfig(getConfig().onError, { key, scope, error: migrationErr });
297
340
  }
298
- return current;
341
+ throw migrationErr;
299
342
  }
300
343
  }
301
344
  }
@@ -309,11 +352,40 @@ function createStorageAdapter(storage, key, options) {
309
352
  let cachedRaw;
310
353
  let cachedValue;
311
354
  let cacheValid = false;
355
+ const backupKey = `${key}:__gjendje_backup`;
356
+ function backupRawData(raw) {
357
+ try {
358
+ if (storage.getItem(backupKey) === null) {
359
+ storage.setItem(backupKey, raw);
360
+ log(
361
+ "warn",
362
+ `Original data for key "${key}" backed up to "${backupKey}" after migration/validation failure.`
363
+ );
364
+ }
365
+ } catch (backupErr) {
366
+ const scope = options.scope ?? "local";
367
+ log(
368
+ "error",
369
+ `Failed to backup data for key "${key}" to "${backupKey}" \u2014 original data may be lost.`
370
+ );
371
+ reportError(key, scope, backupErr);
372
+ }
373
+ }
312
374
  function parse(raw) {
313
375
  if (serialize) {
314
- return serialize.parse(raw);
376
+ const value = serialize.parse(raw);
377
+ if (options.validate && !options.validate(value)) {
378
+ const scope = options.scope ?? "local";
379
+ const config = getConfig();
380
+ safeCallConfig(config.onValidationFail, { key, scope, value });
381
+ const validationErr = new ValidationError(key, scope, value);
382
+ safeCallConfig(config.onError, { key, scope, error: validationErr });
383
+ backupRawData(raw);
384
+ return defaultValue;
385
+ }
386
+ return value;
315
387
  }
316
- return readAndMigrate(raw, options, key, options.scope);
388
+ return readAndMigrate(raw, options, key, options.scope, () => backupRawData(raw));
317
389
  }
318
390
  function read() {
319
391
  if (cacheValid) return cachedValue;
@@ -367,6 +439,7 @@ function createStorageAdapter(storage, key, options) {
367
439
  safeCallConfig(getConfig().onQuotaExceeded, { key, scope, error: writeErr });
368
440
  }
369
441
  reportError(key, scope, writeErr);
442
+ throw writeErr;
370
443
  }
371
444
  }
372
445
  let lastNotifiedValue = defaultValue;
@@ -452,6 +525,7 @@ function createBucketAdapter(key, bucketOptions, options) {
452
525
  let delegateUnsub;
453
526
  const ready = (async () => {
454
527
  if (!isBucketSupported()) return;
528
+ let hadUserWrite = false;
455
529
  try {
456
530
  const openOptions = {
457
531
  persisted: bucketOptions.persisted ?? false,
@@ -485,7 +559,7 @@ function createBucketAdapter(key, bucketOptions, options) {
485
559
  const storage = await bucket.localStorage();
486
560
  if (isDestroyed) return;
487
561
  const currentValue = delegate.get();
488
- const hadUserWrite = !shallowEqual(currentValue, defaultValue);
562
+ hadUserWrite = !shallowEqual(currentValue, defaultValue);
489
563
  delegate.destroy?.();
490
564
  delegate = createStorageAdapter(storage, key, options);
491
565
  if (isDestroyed) {
@@ -507,10 +581,12 @@ function createBucketAdapter(key, bucketOptions, options) {
507
581
  reportError(key, "bucket", err);
508
582
  }
509
583
  if (isDestroyed) return;
510
- const storedValue = delegate.get();
511
- if (!shallowEqual(storedValue, defaultValue)) {
512
- lastNotifiedValue = storedValue;
513
- notify(notifyListeners);
584
+ if (!hadUserWrite) {
585
+ const storedValue = delegate.get();
586
+ if (!shallowEqual(storedValue, defaultValue)) {
587
+ lastNotifiedValue = storedValue;
588
+ notify(notifyListeners);
589
+ }
514
590
  }
515
591
  delegateUnsub = delegate.subscribe((value) => {
516
592
  lastNotifiedValue = value;
@@ -646,7 +722,7 @@ function withSync(adapter, key, scope) {
646
722
  }
647
723
 
648
724
  // src/adapters/url.ts
649
- function createUrlAdapter(key, defaultValue, serializer, persist) {
725
+ function createUrlAdapter(key, defaultValue, serializer, persist, urlReplace) {
650
726
  if (typeof window === "undefined") {
651
727
  throw new Error("[gjendje] URL scope is not available in this environment.");
652
728
  }
@@ -686,11 +762,19 @@ function createUrlAdapter(key, defaultValue, serializer, persist) {
686
762
  }
687
763
  const search = params.toString();
688
764
  const newUrl = search ? `${window.location.pathname}?${search}${window.location.hash}` : `${window.location.pathname}${window.location.hash}`;
689
- window.history.pushState(null, "", newUrl);
765
+ if (urlReplace) {
766
+ window.history.replaceState(null, "", newUrl);
767
+ } else {
768
+ window.history.pushState(null, "", newUrl);
769
+ }
690
770
  cachedSearch = search ? `?${search}` : "";
691
771
  cachedValue = persist ? mergeKeys(toStore, defaultValue, persist) : value;
692
- } catch {
772
+ } catch (e) {
693
773
  cachedSearch = void 0;
774
+ const writeErr = new StorageWriteError(key, "url", e);
775
+ log("error", writeErr.message);
776
+ reportError(key, "url", writeErr);
777
+ throw writeErr;
694
778
  }
695
779
  }
696
780
  let lastNotifiedValue = defaultValue;
@@ -775,7 +859,8 @@ function resolveAdapter(storageKey, scope, options) {
775
859
  storageKey,
776
860
  options.default,
777
861
  options.serialize ?? { stringify: JSON.stringify, parse: JSON.parse },
778
- options.persist
862
+ options.persist,
863
+ options.urlReplace
779
864
  );
780
865
  case "server":
781
866
  if (!_serverAdapterFactory) {
@@ -818,6 +903,7 @@ var StateImpl = class {
818
903
  this._s = preallocatedState ?? {
819
904
  lastValue: adapter.get(),
820
905
  isDestroyed: false,
906
+ hasUserWrite: false,
821
907
  interceptors: void 0,
822
908
  changeHandlers: void 0,
823
909
  settled: RESOLVED,
@@ -878,8 +964,14 @@ var StateImpl = class {
878
964
  let next = typeof valueOrUpdater === "function" ? valueOrUpdater(prev) : valueOrUpdater;
879
965
  next = this._applyInterceptors(next, prev);
880
966
  if (this._options.isEqual?.(next, prev)) return;
967
+ try {
968
+ this._adapter.set(next);
969
+ } catch (err) {
970
+ if (err instanceof StorageWriteError) return;
971
+ throw err;
972
+ }
881
973
  s.lastValue = next;
882
- this._adapter.set(next);
974
+ s.hasUserWrite = true;
883
975
  s.settled = this._adapter.ready;
884
976
  this._notifyChange(next, prev);
885
977
  }
@@ -892,8 +984,14 @@ var StateImpl = class {
892
984
  const prev = this._adapter.get();
893
985
  const next = this._applyInterceptors(this._defaultValue, prev);
894
986
  if (this._options.isEqual?.(next, prev)) return;
987
+ try {
988
+ this._adapter.set(next);
989
+ } catch (err) {
990
+ if (err instanceof StorageWriteError) return;
991
+ throw err;
992
+ }
895
993
  s.lastValue = next;
896
- this._adapter.set(next);
994
+ s.hasUserWrite = true;
897
995
  s.settled = this._adapter.ready;
898
996
  safeCallConfig(this._config.onReset, { key: this.key, scope: this.scope, previousValue: prev });
899
997
  this._notifyChange(next, prev);
@@ -1298,7 +1396,7 @@ function createBase(key, options) {
1298
1396
  const instance = new StateImpl(key, scope, rKey, adapter, options, config);
1299
1397
  if (isSsrMode && !isServer()) {
1300
1398
  instance._s.hydrated = afterHydration(() => {
1301
- if (instance.isDestroyed) return;
1399
+ if (instance.isDestroyed || instance._s.hasUserWrite) return;
1302
1400
  const currentValue = instance.get();
1303
1401
  if (!shallowEqual(currentValue, options.default)) return;
1304
1402
  let realAdapter;