gjendje 0.4.2 → 0.4.4

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.
@@ -0,0 +1,1046 @@
1
+ // src/batch.ts
2
+ var depth = 0;
3
+ var queue = /* @__PURE__ */ new Set();
4
+ var flushBuf = new Array(16);
5
+ function batch(fn) {
6
+ depth++;
7
+ try {
8
+ fn();
9
+ } finally {
10
+ depth--;
11
+ if (depth === 0) flush();
12
+ }
13
+ }
14
+ function notify(fn) {
15
+ if (depth > 0) {
16
+ queue.add(fn);
17
+ return;
18
+ }
19
+ fn();
20
+ }
21
+ function flush() {
22
+ while (queue.size > 0) {
23
+ const size = queue.size;
24
+ if (size > flushBuf.length) {
25
+ flushBuf = new Array(size);
26
+ }
27
+ let i = 0;
28
+ for (const notification of queue) {
29
+ flushBuf[i++] = notification;
30
+ }
31
+ queue.clear();
32
+ depth++;
33
+ try {
34
+ for (let j = 0; j < size; j++) {
35
+ ;
36
+ flushBuf[j]();
37
+ }
38
+ } finally {
39
+ depth--;
40
+ for (let j = 0; j < size; j++) {
41
+ flushBuf[j] = void 0;
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ // src/config.ts
48
+ var globalConfig = {};
49
+ function configure(config) {
50
+ globalConfig = { ...globalConfig, ...config };
51
+ }
52
+ function getConfig() {
53
+ return globalConfig;
54
+ }
55
+ var LOG_PRIORITY = { debug: 0, warn: 1, error: 2 };
56
+ function log(level, message) {
57
+ const configLevel = globalConfig.logLevel ?? "warn";
58
+ if (configLevel === "silent") return;
59
+ if (LOG_PRIORITY[level] >= LOG_PRIORITY[configLevel]) {
60
+ console[level](`[gjendje] ${message}`);
61
+ }
62
+ }
63
+ function reportError(key, scope, error) {
64
+ globalConfig.onError?.({ key, scope, error });
65
+ }
66
+
67
+ // src/utils.ts
68
+ function shallowEqual(a, b) {
69
+ if (Object.is(a, b)) return true;
70
+ if (typeof a !== typeof b) return false;
71
+ if (a === null || b === null) return false;
72
+ if (typeof a !== "object" || typeof b !== "object") return false;
73
+ if (Array.isArray(a)) {
74
+ if (!Array.isArray(b)) return false;
75
+ if (a.length !== b.length) return false;
76
+ for (let i = 0; i < a.length; i++) {
77
+ if (!Object.is(a[i], b[i])) return false;
78
+ }
79
+ return true;
80
+ }
81
+ if (Array.isArray(b)) return false;
82
+ const objA = a;
83
+ const objB = b;
84
+ const keysA = Object.keys(objA);
85
+ const keysB = Object.keys(objB);
86
+ if (keysA.length !== keysB.length) return false;
87
+ for (let i = 0; i < keysA.length; i++) {
88
+ const key = keysA[i];
89
+ if (!Object.hasOwn(objB, key) || !Object.is(objA[key], objB[key])) return false;
90
+ }
91
+ return true;
92
+ }
93
+
94
+ // src/listeners.ts
95
+ function createListeners() {
96
+ const set = /* @__PURE__ */ new Set();
97
+ return {
98
+ notify(value) {
99
+ for (const listener of set) {
100
+ try {
101
+ listener(value);
102
+ } catch (err) {
103
+ console.error("[gjendje] Listener threw:", err);
104
+ }
105
+ }
106
+ },
107
+ subscribe(listener) {
108
+ set.add(listener);
109
+ return () => {
110
+ set.delete(listener);
111
+ };
112
+ },
113
+ clear() {
114
+ set.clear();
115
+ }
116
+ };
117
+ }
118
+
119
+ // src/registry.ts
120
+ function scopedKey(key, scope) {
121
+ return `${scope}:${key}`;
122
+ }
123
+ var registry = /* @__PURE__ */ new Map();
124
+ function getRegistered(rKey) {
125
+ return registry.get(rKey);
126
+ }
127
+ function registerByKey(rKey, key, scope, instance, config) {
128
+ const existing = registry.get(rKey);
129
+ if (existing !== void 0) {
130
+ if (existing.isDestroyed) {
131
+ registry.set(rKey, instance);
132
+ } else if (config.warnOnDuplicate) {
133
+ log("warn", `Duplicate state("${key}") with scope "${scope}". Returning cached instance.`);
134
+ }
135
+ return;
136
+ }
137
+ if (config.maxKeys !== void 0 && registry.size >= config.maxKeys) {
138
+ throw new Error(
139
+ `[gjendje] maxKeys limit (${config.maxKeys}) reached. Cannot register state("${key}") with scope "${scope}".`
140
+ );
141
+ }
142
+ registry.set(rKey, instance);
143
+ config.onRegister?.({ key, scope });
144
+ }
145
+ function unregisterByKey(rKey) {
146
+ registry.delete(rKey);
147
+ }
148
+ function getRegistry() {
149
+ return registry;
150
+ }
151
+
152
+ // src/persist.ts
153
+ function isVersionedValue(value) {
154
+ const hasShape = value !== null && typeof value === "object" && "v" in value && "data" in value;
155
+ return hasShape && Number.isSafeInteger(value.v);
156
+ }
157
+ function readAndMigrate(raw, options, key, scope) {
158
+ const currentVersion = options.version ?? 1;
159
+ const defaultValue = options.default;
160
+ try {
161
+ const parsed = JSON.parse(raw);
162
+ let storedVersion = 1;
163
+ let data;
164
+ if (isVersionedValue(parsed)) {
165
+ storedVersion = parsed.v;
166
+ data = parsed.data;
167
+ } else {
168
+ storedVersion = 1;
169
+ data = parsed;
170
+ }
171
+ if (storedVersion < currentVersion && options.migrate) {
172
+ data = runMigrations(data, storedVersion, currentVersion, options.migrate);
173
+ if (key && scope) {
174
+ getConfig().onMigrate?.({
175
+ key,
176
+ scope,
177
+ fromVersion: storedVersion,
178
+ toVersion: currentVersion,
179
+ data
180
+ });
181
+ }
182
+ }
183
+ if (options.validate && !options.validate(data)) {
184
+ return defaultValue;
185
+ }
186
+ return data;
187
+ } catch {
188
+ log("debug", `Failed to read/migrate stored value \u2014 falling back to default.`);
189
+ return defaultValue;
190
+ }
191
+ }
192
+ function wrapForStorage(value, version) {
193
+ if (!version || version === 1) {
194
+ return JSON.stringify(value);
195
+ }
196
+ const envelope = { v: version, data: value };
197
+ return JSON.stringify(envelope);
198
+ }
199
+ function pickKeys(value, keys) {
200
+ if (!keys || typeof value !== "object" || value === null) return value;
201
+ const partial = {};
202
+ for (const k of keys) {
203
+ if (Object.hasOwn(value, k)) {
204
+ partial[k] = value[k];
205
+ }
206
+ }
207
+ return partial;
208
+ }
209
+ function mergeKeys(stored, defaultValue, keys) {
210
+ if (!keys || typeof stored !== "object" || stored === null) return stored;
211
+ return { ...defaultValue, ...stored };
212
+ }
213
+ var MAX_MIGRATION_STEPS = 1e3;
214
+ function runMigrations(data, fromVersion, toVersion, migrations) {
215
+ if (fromVersion < 0 || toVersion - fromVersion > MAX_MIGRATION_STEPS) {
216
+ log("warn", `Migration range v${fromVersion}\u2192v${toVersion} is out of bounds \u2014 skipping.`);
217
+ return data;
218
+ }
219
+ let current = data;
220
+ for (let v = fromVersion; v < toVersion; v++) {
221
+ const migrateFn = migrations[v];
222
+ if (migrateFn) {
223
+ try {
224
+ current = migrateFn(current);
225
+ } catch {
226
+ log("warn", `Migration from v${v} failed \u2014 returning partially migrated value.`);
227
+ return current;
228
+ }
229
+ }
230
+ }
231
+ return current;
232
+ }
233
+
234
+ // src/adapters/storage.ts
235
+ function createStorageAdapter(storage, key, options) {
236
+ const { default: defaultValue, version, serialize, persist } = options;
237
+ const listeners = createListeners();
238
+ let cachedRaw;
239
+ let cachedValue;
240
+ function parse(raw) {
241
+ if (serialize) {
242
+ return serialize.parse(raw);
243
+ }
244
+ return readAndMigrate(raw, options, key, options.scope);
245
+ }
246
+ function read() {
247
+ try {
248
+ const raw = storage.getItem(key);
249
+ if (raw === null) {
250
+ cachedRaw = null;
251
+ cachedValue = void 0;
252
+ return defaultValue;
253
+ }
254
+ if (raw === cachedRaw) return cachedValue;
255
+ let value;
256
+ try {
257
+ value = parse(raw);
258
+ } catch {
259
+ cachedRaw = void 0;
260
+ cachedValue = void 0;
261
+ return defaultValue;
262
+ }
263
+ value = mergeKeys(value, defaultValue, persist);
264
+ cachedRaw = raw;
265
+ cachedValue = value;
266
+ return value;
267
+ } catch {
268
+ return defaultValue;
269
+ }
270
+ }
271
+ function write(value) {
272
+ try {
273
+ const toStore = pickKeys(value, persist);
274
+ const raw = serialize ? serialize.stringify(toStore) : wrapForStorage(toStore, version);
275
+ storage.setItem(key, raw);
276
+ cachedRaw = void 0;
277
+ cachedValue = void 0;
278
+ } catch (e) {
279
+ cachedRaw = void 0;
280
+ cachedValue = void 0;
281
+ log(
282
+ "error",
283
+ `Failed to write key "${key}" to storage: ${e instanceof Error ? e.message : String(e)}`
284
+ );
285
+ const isQuotaError = e instanceof DOMException && (e.name === "QuotaExceededError" || e.code === 22);
286
+ if (isQuotaError && options.scope) {
287
+ getConfig().onQuotaExceeded?.({ key, scope: options.scope, error: e });
288
+ }
289
+ }
290
+ }
291
+ let lastNotifiedValue = defaultValue;
292
+ const notifyListeners = () => listeners.notify(lastNotifiedValue);
293
+ function onStorageEvent(event) {
294
+ if (event.storageArea !== storage || event.key !== key) return;
295
+ cachedRaw = void 0;
296
+ cachedValue = void 0;
297
+ lastNotifiedValue = read();
298
+ notify(notifyListeners);
299
+ }
300
+ if (typeof window !== "undefined") {
301
+ window.addEventListener("storage", onStorageEvent);
302
+ }
303
+ return {
304
+ ready: Promise.resolve(),
305
+ get() {
306
+ return read();
307
+ },
308
+ set(value) {
309
+ write(value);
310
+ lastNotifiedValue = value;
311
+ notify(notifyListeners);
312
+ },
313
+ subscribe: listeners.subscribe,
314
+ destroy() {
315
+ cachedRaw = void 0;
316
+ cachedValue = void 0;
317
+ listeners.clear();
318
+ if (typeof window !== "undefined") {
319
+ window.removeEventListener("storage", onStorageEvent);
320
+ }
321
+ }
322
+ };
323
+ }
324
+
325
+ // src/adapters/bucket.ts
326
+ function isBucketSupported() {
327
+ return typeof navigator !== "undefined" && "storageBuckets" in navigator && navigator.storageBuckets != null;
328
+ }
329
+ function parseExpiry(expires) {
330
+ if (typeof expires === "number") return expires;
331
+ const units = {
332
+ ms: 1,
333
+ s: 1e3,
334
+ m: 6e4,
335
+ h: 36e5,
336
+ d: 864e5,
337
+ w: 6048e5
338
+ };
339
+ const match = expires.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/);
340
+ if (!match || !match[1] || !match[2]) return void 0;
341
+ const value = parseFloat(match[1]);
342
+ const unit = units[match[2]];
343
+ if (!unit) return void 0;
344
+ return Date.now() + value * unit;
345
+ }
346
+ function parseQuota(quota) {
347
+ if (typeof quota === "number") return quota;
348
+ const units = {
349
+ b: 1,
350
+ kb: 1024,
351
+ mb: 1048576,
352
+ gb: 1073741824
353
+ };
354
+ const match = quota.toLowerCase().match(/^(\d+(?:\.\d+)?)(b|kb|mb|gb)$/);
355
+ if (!match || !match[1] || !match[2]) return void 0;
356
+ const value = parseFloat(match[1]);
357
+ const unit = units[match[2]];
358
+ if (!unit) return void 0;
359
+ return Math.floor(value * unit);
360
+ }
361
+ function createBucketAdapter(key, bucketOptions, options) {
362
+ const { default: defaultValue } = options;
363
+ const fallbackScope = bucketOptions.fallback ?? "local";
364
+ const listeners = createListeners();
365
+ let lastNotifiedValue = defaultValue;
366
+ const notifyListeners = () => listeners.notify(lastNotifiedValue);
367
+ const fallbackStorage = fallbackScope === "tab" ? sessionStorage : localStorage;
368
+ let delegate = createStorageAdapter(fallbackStorage, key, options);
369
+ let isDestroyed = false;
370
+ const ready = (async () => {
371
+ if (!isBucketSupported()) return;
372
+ try {
373
+ const openOptions = {
374
+ persisted: bucketOptions.persisted ?? false,
375
+ durability: "strict"
376
+ };
377
+ if (bucketOptions.expires != null) {
378
+ const parsed = parseExpiry(bucketOptions.expires);
379
+ if (parsed != null) {
380
+ openOptions.expires = parsed;
381
+ } else {
382
+ log(
383
+ "warn",
384
+ `Invalid bucket expires format: "${bucketOptions.expires}". Expected a number or a string like "7d", "24h", "30m".`
385
+ );
386
+ }
387
+ }
388
+ if (bucketOptions.quota != null) {
389
+ const parsed = parseQuota(bucketOptions.quota);
390
+ if (parsed != null) {
391
+ openOptions.quota = parsed;
392
+ } else {
393
+ log(
394
+ "warn",
395
+ `Invalid bucket quota format: "${bucketOptions.quota}". Expected a number or a string like "10mb", "50kb", "1gb".`
396
+ );
397
+ }
398
+ }
399
+ const bucketManager = navigator.storageBuckets;
400
+ if (!bucketManager) return;
401
+ const bucket = await bucketManager.open(bucketOptions.name, openOptions);
402
+ const storage = await bucket.localStorage();
403
+ if (isDestroyed) return;
404
+ const currentValue = delegate.get();
405
+ const hadUserWrite = !shallowEqual(currentValue, defaultValue);
406
+ delegate.destroy?.();
407
+ delegate = createStorageAdapter(storage, key, options);
408
+ if (hadUserWrite) {
409
+ delegate.set(currentValue);
410
+ }
411
+ } catch {
412
+ }
413
+ if (isDestroyed) return;
414
+ const storedValue = delegate.get();
415
+ if (!shallowEqual(storedValue, defaultValue)) {
416
+ lastNotifiedValue = storedValue;
417
+ notify(notifyListeners);
418
+ }
419
+ delegate.subscribe((value) => {
420
+ lastNotifiedValue = value;
421
+ notify(notifyListeners);
422
+ });
423
+ })();
424
+ return {
425
+ ready,
426
+ get() {
427
+ return delegate.get();
428
+ },
429
+ set(value) {
430
+ delegate.set(value);
431
+ lastNotifiedValue = value;
432
+ notify(notifyListeners);
433
+ },
434
+ subscribe: listeners.subscribe,
435
+ destroy() {
436
+ isDestroyed = true;
437
+ listeners.clear();
438
+ delegate.destroy?.();
439
+ }
440
+ };
441
+ }
442
+
443
+ // src/adapters/render.ts
444
+ var RESOLVED = Promise.resolve();
445
+ function createRenderAdapter(defaultValue) {
446
+ let current = defaultValue;
447
+ const listeners = /* @__PURE__ */ new Set();
448
+ const notifyListeners = () => {
449
+ for (const listener of listeners) {
450
+ try {
451
+ listener(current);
452
+ } catch (err) {
453
+ console.error("[gjendje] Listener threw:", err);
454
+ }
455
+ }
456
+ };
457
+ return {
458
+ ready: RESOLVED,
459
+ get() {
460
+ return current;
461
+ },
462
+ set(value) {
463
+ current = value;
464
+ notify(notifyListeners);
465
+ },
466
+ subscribe(listener) {
467
+ listeners.add(listener);
468
+ return () => {
469
+ listeners.delete(listener);
470
+ };
471
+ },
472
+ destroy() {
473
+ listeners.clear();
474
+ }
475
+ };
476
+ }
477
+
478
+ // src/adapters/sync.ts
479
+ function withSync(adapter, key, scope) {
480
+ const channelName = `state:${key}`;
481
+ const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(channelName) : null;
482
+ const listeners = createListeners();
483
+ adapter.subscribe((value) => {
484
+ listeners.notify(value);
485
+ });
486
+ if (channel) {
487
+ channel.onmessage = (event) => {
488
+ const msg = event.data;
489
+ if (msg == null || typeof msg !== "object" || !("value" in msg)) return;
490
+ if (Object.keys(msg).length !== 1) return;
491
+ const value = msg.value;
492
+ try {
493
+ adapter.set(value);
494
+ if (scope) {
495
+ getConfig().onSync?.({ key, scope, value, source: "remote" });
496
+ }
497
+ } catch (err) {
498
+ log(
499
+ "error",
500
+ `Sync failed for key "${key}": ${err instanceof Error ? err.message : String(err)}`
501
+ );
502
+ if (scope) {
503
+ reportError(key, scope, err);
504
+ }
505
+ }
506
+ };
507
+ }
508
+ return {
509
+ get ready() {
510
+ return adapter.ready;
511
+ },
512
+ get() {
513
+ return adapter.get();
514
+ },
515
+ set(value) {
516
+ adapter.set(value);
517
+ channel?.postMessage({ value });
518
+ },
519
+ subscribe(listener) {
520
+ return listeners.subscribe(listener);
521
+ },
522
+ destroy() {
523
+ listeners.clear();
524
+ channel?.close();
525
+ adapter.destroy?.();
526
+ }
527
+ };
528
+ }
529
+
530
+ // src/adapters/url.ts
531
+ function createUrlAdapter(key, defaultValue, serializer, persist) {
532
+ if (typeof window === "undefined") {
533
+ throw new Error("[state] URL scope is not available in this environment.");
534
+ }
535
+ const listeners = createListeners();
536
+ const defaultSerialized = serializer.stringify(defaultValue);
537
+ function read() {
538
+ try {
539
+ const params = new URLSearchParams(window.location.search);
540
+ const raw = params.get(key);
541
+ if (raw === null) return defaultValue;
542
+ return mergeKeys(serializer.parse(raw), defaultValue, persist);
543
+ } catch {
544
+ return defaultValue;
545
+ }
546
+ }
547
+ function write(value) {
548
+ try {
549
+ const params = new URLSearchParams(window.location.search);
550
+ const toStore = pickKeys(value, persist);
551
+ const stringified = serializer.stringify(toStore);
552
+ const isDefault = stringified === defaultSerialized;
553
+ if (isDefault) {
554
+ params.delete(key);
555
+ } else {
556
+ params.set(key, stringified);
557
+ }
558
+ const search = params.toString();
559
+ const newUrl = search ? `${window.location.pathname}?${search}${window.location.hash}` : `${window.location.pathname}${window.location.hash}`;
560
+ window.history.pushState(null, "", newUrl);
561
+ } catch {
562
+ }
563
+ }
564
+ let lastNotifiedValue = defaultValue;
565
+ const notifyListeners = () => listeners.notify(lastNotifiedValue);
566
+ function onPopState() {
567
+ lastNotifiedValue = read();
568
+ notify(notifyListeners);
569
+ }
570
+ window.addEventListener("popstate", onPopState);
571
+ return {
572
+ ready: Promise.resolve(),
573
+ get() {
574
+ return read();
575
+ },
576
+ set(value) {
577
+ write(value);
578
+ lastNotifiedValue = value;
579
+ notify(notifyListeners);
580
+ },
581
+ subscribe: listeners.subscribe,
582
+ destroy() {
583
+ listeners.clear();
584
+ window.removeEventListener("popstate", onPopState);
585
+ }
586
+ };
587
+ }
588
+
589
+ // src/ssr.ts
590
+ function isServer() {
591
+ return typeof window === "undefined" || typeof document === "undefined";
592
+ }
593
+ var BROWSER_SCOPES = /* @__PURE__ */ new Set(["tab", "local", "url", "bucket"]);
594
+ function afterHydration(fn) {
595
+ if (isServer()) return Promise.resolve();
596
+ return new Promise((resolve) => {
597
+ Promise.resolve().then(() => {
598
+ if (typeof requestAnimationFrame !== "undefined") {
599
+ requestAnimationFrame(() => {
600
+ fn();
601
+ resolve();
602
+ });
603
+ } else {
604
+ setTimeout(() => {
605
+ fn();
606
+ resolve();
607
+ }, 0);
608
+ }
609
+ });
610
+ });
611
+ }
612
+
613
+ // src/core.ts
614
+ var _serverAdapterFactory;
615
+ function registerServerAdapter(factory) {
616
+ _serverAdapterFactory = factory;
617
+ }
618
+ var PERSISTENT_SCOPES = /* @__PURE__ */ new Set(["local", "tab", "bucket"]);
619
+ var SYNCABLE_SCOPES = /* @__PURE__ */ new Set(["local", "bucket"]);
620
+ var RESOLVED2 = Promise.resolve();
621
+ var RENDER_SHIM = {
622
+ ready: RESOLVED2,
623
+ get: () => void 0,
624
+ set: () => {
625
+ },
626
+ subscribe: () => () => {
627
+ }
628
+ };
629
+ function resolveStorageKey(key, options, configPrefix) {
630
+ if (options.prefix === false) return key;
631
+ const prefix = options.prefix ?? configPrefix;
632
+ return prefix ? `${prefix}:${key}` : key;
633
+ }
634
+ function resolveAdapter(storageKey, scope, options) {
635
+ switch (scope) {
636
+ case "render":
637
+ return createRenderAdapter(options.default);
638
+ case "tab":
639
+ if (typeof sessionStorage === "undefined") {
640
+ throw new Error(
641
+ '[state] sessionStorage is not available. Use ssr: true or scope: "render" for server environments.'
642
+ );
643
+ }
644
+ return createStorageAdapter(sessionStorage, storageKey, options);
645
+ case "local":
646
+ if (typeof localStorage === "undefined") {
647
+ throw new Error(
648
+ '[state] localStorage is not available. Use ssr: true or scope: "server" for server environments.'
649
+ );
650
+ }
651
+ return createStorageAdapter(localStorage, storageKey, options);
652
+ case "url":
653
+ return createUrlAdapter(
654
+ storageKey,
655
+ options.default,
656
+ options.serialize ?? {
657
+ stringify: (v) => JSON.stringify(v),
658
+ parse: (s) => JSON.parse(s)
659
+ },
660
+ options.persist
661
+ );
662
+ case "server":
663
+ if (!_serverAdapterFactory) {
664
+ throw new Error(
665
+ '[state] scope: "server" requires the server adapter. Import { withServerSession } from "gjendje" or "gjendje/server" to enable it.'
666
+ );
667
+ }
668
+ return _serverAdapterFactory(storageKey, options.default);
669
+ case "bucket": {
670
+ if (!options.bucket) {
671
+ throw new Error(
672
+ '[state] scope: "bucket" requires a bucket option. Example: { scope: "bucket", bucket: { name: "my-bucket" } }'
673
+ );
674
+ }
675
+ return createBucketAdapter(storageKey, options.bucket, options);
676
+ }
677
+ default: {
678
+ const _exhaustive = scope;
679
+ throw new Error(`[state] Unknown scope: ${_exhaustive}`);
680
+ }
681
+ }
682
+ }
683
+ var StateImpl = class {
684
+ key;
685
+ scope;
686
+ _adapter;
687
+ _defaultValue;
688
+ _options;
689
+ _rKey;
690
+ _config;
691
+ _s;
692
+ constructor(key, scope, rKey, adapter, options, config) {
693
+ this.key = key;
694
+ this.scope = scope;
695
+ this._rKey = rKey;
696
+ this._adapter = adapter;
697
+ this._defaultValue = options.default;
698
+ this._options = options;
699
+ this._config = config;
700
+ this._s = {
701
+ lastValue: adapter.get(),
702
+ isDestroyed: false,
703
+ interceptors: void 0,
704
+ hooks: void 0,
705
+ settled: RESOLVED2,
706
+ resolveDestroyed: void 0,
707
+ destroyed: void 0,
708
+ hydrated: void 0,
709
+ watchers: void 0,
710
+ watchUnsub: void 0,
711
+ watchPrev: void 0
712
+ };
713
+ }
714
+ get() {
715
+ return this._s.isDestroyed ? this._s.lastValue : this._adapter.get();
716
+ }
717
+ peek() {
718
+ return this._s.isDestroyed ? this._s.lastValue : this._adapter.get();
719
+ }
720
+ set(valueOrUpdater) {
721
+ const s = this._s;
722
+ if (s.isDestroyed) return;
723
+ const prev = this._adapter.get();
724
+ let next = typeof valueOrUpdater === "function" ? valueOrUpdater(prev) : valueOrUpdater;
725
+ if (s.interceptors !== void 0 && s.interceptors.size > 0) {
726
+ for (const interceptor of s.interceptors) {
727
+ next = interceptor(next, prev);
728
+ }
729
+ }
730
+ if (this._options.isEqual?.(next, prev)) return;
731
+ s.lastValue = next;
732
+ this._adapter.set(next);
733
+ s.settled = this._adapter.ready;
734
+ if (s.hooks !== void 0 && s.hooks.size > 0) {
735
+ for (const hook of s.hooks) {
736
+ hook(next, prev);
737
+ }
738
+ }
739
+ }
740
+ subscribe(listener) {
741
+ return this._adapter.subscribe(listener);
742
+ }
743
+ reset() {
744
+ const s = this._s;
745
+ if (s.isDestroyed) return;
746
+ const prev = this._adapter.get();
747
+ let next = this._defaultValue;
748
+ if (s.interceptors !== void 0 && s.interceptors.size > 0) {
749
+ for (const interceptor of s.interceptors) {
750
+ next = interceptor(next, prev);
751
+ }
752
+ }
753
+ if (this._options.isEqual?.(next, prev)) return;
754
+ s.lastValue = next;
755
+ this._adapter.set(next);
756
+ s.settled = this._adapter.ready;
757
+ if (s.hooks !== void 0 && s.hooks.size > 0) {
758
+ for (const hook of s.hooks) {
759
+ hook(next, prev);
760
+ }
761
+ }
762
+ }
763
+ get ready() {
764
+ return this._adapter.ready;
765
+ }
766
+ get settled() {
767
+ return this._s.settled;
768
+ }
769
+ get hydrated() {
770
+ return this._s.hydrated ?? RESOLVED2;
771
+ }
772
+ get destroyed() {
773
+ const s = this._s;
774
+ if (!s.destroyed) {
775
+ s.destroyed = new Promise((resolve) => {
776
+ s.resolveDestroyed = resolve;
777
+ });
778
+ }
779
+ return s.destroyed;
780
+ }
781
+ get isDestroyed() {
782
+ return this._s.isDestroyed;
783
+ }
784
+ intercept(fn) {
785
+ const s = this._s;
786
+ if (!s.interceptors) s.interceptors = /* @__PURE__ */ new Set();
787
+ s.interceptors.add(fn);
788
+ return () => {
789
+ s.interceptors?.delete(fn);
790
+ };
791
+ }
792
+ use(fn) {
793
+ const s = this._s;
794
+ if (!s.hooks) s.hooks = /* @__PURE__ */ new Set();
795
+ s.hooks.add(fn);
796
+ return () => {
797
+ s.hooks?.delete(fn);
798
+ };
799
+ }
800
+ watch(watchKey, listener) {
801
+ const s = this._s;
802
+ if (!s.watchers) s.watchers = /* @__PURE__ */ new Map();
803
+ this._ensureWatchSubscription();
804
+ let listeners = s.watchers.get(watchKey);
805
+ if (!listeners) {
806
+ listeners = /* @__PURE__ */ new Set();
807
+ s.watchers.set(watchKey, listeners);
808
+ }
809
+ listeners.add(listener);
810
+ const watcherMap = s.watchers;
811
+ return () => {
812
+ listeners.delete(listener);
813
+ if (listeners.size === 0) {
814
+ watcherMap.delete(watchKey);
815
+ }
816
+ };
817
+ }
818
+ destroy() {
819
+ const s = this._s;
820
+ if (s.isDestroyed) return;
821
+ s.lastValue = this._adapter.get();
822
+ s.isDestroyed = true;
823
+ s.interceptors?.clear();
824
+ s.hooks?.clear();
825
+ s.watchers?.clear();
826
+ s.watchUnsub?.();
827
+ this._adapter.destroy?.();
828
+ unregisterByKey(this._rKey);
829
+ this._config.onDestroy?.({ key: this.key, scope: this.scope });
830
+ if (s.resolveDestroyed) {
831
+ s.resolveDestroyed();
832
+ } else {
833
+ s.destroyed = RESOLVED2;
834
+ }
835
+ }
836
+ _ensureWatchSubscription() {
837
+ const s = this._s;
838
+ if (s.watchUnsub) return;
839
+ s.watchPrev = this._adapter.get();
840
+ s.watchUnsub = this._adapter.subscribe((next) => {
841
+ if (!s.watchers || s.watchers.size === 0) {
842
+ s.watchPrev = next;
843
+ return;
844
+ }
845
+ for (const [watchKey, listeners] of s.watchers) {
846
+ const prevVal = s.watchPrev !== null && typeof s.watchPrev === "object" ? s.watchPrev[watchKey] : void 0;
847
+ const nextVal = next !== null && typeof next === "object" ? next[watchKey] : void 0;
848
+ if (!Object.is(prevVal, nextVal)) {
849
+ for (const listener of listeners) {
850
+ listener(nextVal);
851
+ }
852
+ }
853
+ }
854
+ s.watchPrev = next;
855
+ });
856
+ }
857
+ };
858
+ var RenderStateImpl = class extends StateImpl {
859
+ // Direct reference — avoids a getter cast on every get()/set() call
860
+ _r;
861
+ _hasIsEqual;
862
+ constructor(key, rKey, options, config) {
863
+ super(key, "render", rKey, RENDER_SHIM, options, config);
864
+ const rs = this._s;
865
+ rs.current = options.default;
866
+ rs.renderListeners = void 0;
867
+ rs.notifyFn = void 0;
868
+ this._r = rs;
869
+ this._hasIsEqual = options.isEqual !== void 0;
870
+ }
871
+ get() {
872
+ return this._r.current;
873
+ }
874
+ peek() {
875
+ return this._r.current;
876
+ }
877
+ set(valueOrUpdater) {
878
+ const s = this._r;
879
+ if (s.isDestroyed) return;
880
+ const prev = s.current;
881
+ let next = typeof valueOrUpdater === "function" ? valueOrUpdater(prev) : valueOrUpdater;
882
+ if (s.interceptors !== void 0 && s.interceptors.size > 0) {
883
+ for (const interceptor of s.interceptors) {
884
+ next = interceptor(next, prev);
885
+ }
886
+ }
887
+ if (this._hasIsEqual && this._options.isEqual?.(next, prev)) return;
888
+ s.current = next;
889
+ if (s.notifyFn !== void 0) {
890
+ notify(s.notifyFn);
891
+ }
892
+ if (s.hooks !== void 0 && s.hooks.size > 0) {
893
+ for (const hook of s.hooks) {
894
+ hook(next, prev);
895
+ }
896
+ }
897
+ }
898
+ subscribe(listener) {
899
+ const s = this._r;
900
+ if (!s.renderListeners) {
901
+ const listeners = /* @__PURE__ */ new Set();
902
+ s.renderListeners = listeners;
903
+ s.notifyFn = () => {
904
+ for (const l of listeners) {
905
+ try {
906
+ l(s.current);
907
+ } catch (err) {
908
+ console.error("[gjendje] Listener threw:", err);
909
+ }
910
+ }
911
+ };
912
+ }
913
+ const set = s.renderListeners;
914
+ set.add(listener);
915
+ return () => {
916
+ set.delete(listener);
917
+ };
918
+ }
919
+ reset() {
920
+ const s = this._r;
921
+ if (s.isDestroyed) return;
922
+ const prev = s.current;
923
+ let next = this._defaultValue;
924
+ if (s.interceptors !== void 0 && s.interceptors.size > 0) {
925
+ for (const interceptor of s.interceptors) {
926
+ next = interceptor(next, prev);
927
+ }
928
+ }
929
+ if (this._hasIsEqual && this._options.isEqual?.(next, prev)) return;
930
+ s.current = next;
931
+ if (s.notifyFn !== void 0) {
932
+ notify(s.notifyFn);
933
+ }
934
+ if (s.hooks !== void 0 && s.hooks.size > 0) {
935
+ for (const hook of s.hooks) {
936
+ hook(next, prev);
937
+ }
938
+ }
939
+ }
940
+ get ready() {
941
+ return RESOLVED2;
942
+ }
943
+ _ensureWatchSubscription() {
944
+ const s = this._r;
945
+ if (s.watchUnsub) return;
946
+ s.watchPrev = s.current;
947
+ s.watchUnsub = this.subscribe((next) => {
948
+ if (!s.watchers || s.watchers.size === 0) {
949
+ s.watchPrev = next;
950
+ return;
951
+ }
952
+ for (const [watchKey, listeners] of s.watchers) {
953
+ const prevVal = s.watchPrev !== null && typeof s.watchPrev === "object" ? s.watchPrev[watchKey] : void 0;
954
+ const nextVal = next !== null && typeof next === "object" ? next[watchKey] : void 0;
955
+ if (!Object.is(prevVal, nextVal)) {
956
+ for (const listener of listeners) {
957
+ listener(nextVal);
958
+ }
959
+ }
960
+ }
961
+ s.watchPrev = next;
962
+ });
963
+ }
964
+ destroy() {
965
+ const s = this._r;
966
+ if (s.isDestroyed) return;
967
+ s.lastValue = s.current;
968
+ s.isDestroyed = true;
969
+ s.interceptors?.clear();
970
+ s.hooks?.clear();
971
+ s.watchers?.clear();
972
+ s.watchUnsub?.();
973
+ s.renderListeners?.clear();
974
+ unregisterByKey(this._rKey);
975
+ this._config.onDestroy?.({ key: this.key, scope: this.scope });
976
+ if (s.resolveDestroyed) {
977
+ s.resolveDestroyed();
978
+ } else {
979
+ s.destroyed = RESOLVED2;
980
+ }
981
+ }
982
+ };
983
+ function createRenderState(key, rKey, options, config) {
984
+ return new RenderStateImpl(key, rKey, options, config);
985
+ }
986
+ function createBase(key, options) {
987
+ if (!key) {
988
+ throw new Error("[state] key must be a non-empty string.");
989
+ }
990
+ const config = getConfig();
991
+ if (config.keyPattern && !config.keyPattern.test(key)) {
992
+ throw new Error(
993
+ `[gjendje] Key "${key}" does not match the configured keyPattern ${config.keyPattern}.`
994
+ );
995
+ }
996
+ const scope = options.scope ?? config.scope ?? "render";
997
+ const rKey = scopedKey(key, scope);
998
+ const existing = getRegistered(rKey);
999
+ if (existing && !existing.isDestroyed) return existing;
1000
+ if (config.requireValidation && PERSISTENT_SCOPES.has(scope) && !options.validate) {
1001
+ throw new Error(
1002
+ `[gjendje] A validate function is required for persisted scope "${scope}" on state("${key}"). Set requireValidation: false in configure() to disable.`
1003
+ );
1004
+ }
1005
+ const isSsrMode = (options.ssr ?? config.ssr) && BROWSER_SCOPES.has(scope);
1006
+ const useRenderFallback = isSsrMode && isServer();
1007
+ const effectiveSync = options.sync ?? (config.sync && SYNCABLE_SCOPES.has(scope));
1008
+ if (effectiveSync && !SYNCABLE_SCOPES.has(scope)) {
1009
+ log(
1010
+ "warn",
1011
+ `sync: true is ignored for scope "${scope}". Only "local" and "bucket" scopes support cross-tab sync.`
1012
+ );
1013
+ }
1014
+ let instance;
1015
+ if (scope === "render" && !isSsrMode) {
1016
+ instance = new RenderStateImpl(key, rKey, options, config);
1017
+ } else {
1018
+ const storageKey = resolveStorageKey(key, options, config.prefix);
1019
+ const baseAdapter = useRenderFallback ? createRenderAdapter(options.default) : resolveAdapter(storageKey, scope, options);
1020
+ const shouldSync = effectiveSync && SYNCABLE_SCOPES.has(scope) && !useRenderFallback;
1021
+ const adapter = shouldSync ? withSync(baseAdapter, storageKey, scope) : baseAdapter;
1022
+ instance = new StateImpl(key, scope, rKey, adapter, options, config);
1023
+ if (isSsrMode && !isServer()) {
1024
+ instance._s.hydrated = afterHydration(() => {
1025
+ try {
1026
+ const realAdapter = resolveAdapter(storageKey, scope, options);
1027
+ const storedValue = realAdapter.get();
1028
+ const serverValue = options.default;
1029
+ const clientValue = storedValue;
1030
+ if (!shallowEqual(storedValue, options.default)) {
1031
+ instance.set(storedValue);
1032
+ }
1033
+ config.onHydrate?.({ key, scope, serverValue, clientValue });
1034
+ realAdapter.destroy?.();
1035
+ } catch (err) {
1036
+ log("debug", `Hydration adapter unavailable for state("${key}") \u2014 using render fallback.`);
1037
+ reportError(key, scope, err);
1038
+ }
1039
+ });
1040
+ }
1041
+ }
1042
+ registerByKey(rKey, key, scope, instance, config);
1043
+ return instance;
1044
+ }
1045
+
1046
+ export { batch, configure, createBase, createListeners, createRenderState, getConfig, getRegistered, getRegistry, log, notify, registerByKey, registerServerAdapter, scopedKey, shallowEqual };