gjendje 0.7.1 → 0.8.0

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.
package/README.md CHANGED
@@ -50,7 +50,7 @@ form.patch({ name: 'Alice' }) // only updates name
50
50
  form.patch({ name: 'Bob', age: 30 }) // updates multiple properties at once
51
51
  ```
52
52
 
53
- [See all scopes and examples](https://github.com/charliebeckstrand/gjendje/blob/main/docs/scopes.md)
53
+ [All scopes and examples](https://github.com/charliebeckstrand/gjendje/blob/main/docs/scopes.md)
54
54
 
55
55
  ## Configure
56
56
 
@@ -71,6 +71,19 @@ const theme = state({ theme: 'light' })
71
71
  theme.scope // 'local' — derived from configure
72
72
  ```
73
73
 
74
+ You can also configure global events:
75
+
76
+ ```ts
77
+ configure({
78
+ onError: ({ key, scope, error }) => {
79
+ console.error(`[${key}] (${scope}) error:`, error)
80
+ },
81
+ onChange: ({ key, scope, value, previousValue }) => {
82
+ console.log(`[${key}] (${scope}) changed:`, previousValue, '→', value)
83
+ },
84
+ })
85
+ ```
86
+
74
87
  [Full configure reference](https://github.com/charliebeckstrand/gjendje/blob/main/docs/configure.md)
75
88
 
76
89
  ## Scopes
@@ -88,7 +101,7 @@ theme.scope // 'local' — derived from configure
88
101
 
89
102
  ## API
90
103
 
91
- Every scope — `state.local`, `state.session`, `state.url`, `state.bucket`, and `state.server` — shares the same core API: `get`, `set`, `reset`, `subscribe`, `watch`, `intercept`, and more.
104
+ Every scope — `state.local`, `state.session`, `state.url`, `state.bucket`, and `state.server` — shares the same core API: `get`, `set`, `patch`, `reset`, `subscribe`, `watch`, `intercept`, and more.
92
105
 
93
106
  [Full API reference](https://github.com/charliebeckstrand/gjendje/blob/main/docs/api.md) · [Persistence reference](https://github.com/charliebeckstrand/gjendje/blob/main/docs/persistence.md)
94
107
 
@@ -183,6 +183,9 @@ function readAndMigrate(raw, options, key, scope) {
183
183
  }
184
184
  }
185
185
  if (options.validate && !options.validate(data)) {
186
+ if (key && scope) {
187
+ getConfig().onValidationFail?.({ key, scope, value: data });
188
+ }
186
189
  return defaultValue;
187
190
  }
188
191
  return data;
@@ -407,6 +410,10 @@ function createBucketAdapter(key, bucketOptions, options) {
407
410
  const hadUserWrite = !shallowEqual(currentValue, defaultValue);
408
411
  delegate.destroy?.();
409
412
  delegate = createStorageAdapter(storage, key, options);
413
+ const bucketValue = delegate.get();
414
+ if (hadUserWrite && shallowEqual(bucketValue, defaultValue)) {
415
+ getConfig().onExpire?.({ key, scope: "bucket", expiredAt: Date.now() });
416
+ }
410
417
  if (hadUserWrite) {
411
418
  delegate.set(currentValue);
412
419
  }
@@ -727,9 +734,18 @@ var StateImpl = class {
727
734
  const prev = this._adapter.get();
728
735
  let next = typeof valueOrUpdater === "function" ? valueOrUpdater(prev) : valueOrUpdater;
729
736
  if (s.interceptors !== void 0 && s.interceptors.size > 0) {
737
+ const original = next;
730
738
  for (const interceptor of s.interceptors) {
731
739
  next = interceptor(next, prev);
732
740
  }
741
+ if (!Object.is(original, next)) {
742
+ this._config.onIntercept?.({
743
+ key: this.key,
744
+ scope: this.scope,
745
+ original,
746
+ intercepted: next
747
+ });
748
+ }
733
749
  }
734
750
  if (this._options.isEqual?.(next, prev)) return;
735
751
  s.lastValue = next;
@@ -740,6 +756,7 @@ var StateImpl = class {
740
756
  hook(next, prev);
741
757
  }
742
758
  }
759
+ this._config.onChange?.({ key: this.key, scope: this.scope, value: next, previousValue: prev });
743
760
  }
744
761
  subscribe(listener) {
745
762
  return this._adapter.subscribe(listener);
@@ -750,9 +767,18 @@ var StateImpl = class {
750
767
  const prev = this._adapter.get();
751
768
  let next = this._defaultValue;
752
769
  if (s.interceptors !== void 0 && s.interceptors.size > 0) {
770
+ const original = next;
753
771
  for (const interceptor of s.interceptors) {
754
772
  next = interceptor(next, prev);
755
773
  }
774
+ if (!Object.is(original, next)) {
775
+ this._config.onIntercept?.({
776
+ key: this.key,
777
+ scope: this.scope,
778
+ original,
779
+ intercepted: next
780
+ });
781
+ }
756
782
  }
757
783
  if (this._options.isEqual?.(next, prev)) return;
758
784
  s.lastValue = next;
@@ -763,6 +789,8 @@ var StateImpl = class {
763
789
  hook(next, prev);
764
790
  }
765
791
  }
792
+ this._config.onReset?.({ key: this.key, scope: this.scope, previousValue: prev });
793
+ this._config.onChange?.({ key: this.key, scope: this.scope, value: next, previousValue: prev });
766
794
  }
767
795
  get ready() {
768
796
  return this._adapter.ready;
@@ -901,9 +929,18 @@ var RenderStateImpl = class extends StateImpl {
901
929
  const prev = s.current;
902
930
  let next = typeof valueOrUpdater === "function" ? valueOrUpdater(prev) : valueOrUpdater;
903
931
  if (s.interceptors !== void 0 && s.interceptors.size > 0) {
932
+ const original = next;
904
933
  for (const interceptor of s.interceptors) {
905
934
  next = interceptor(next, prev);
906
935
  }
936
+ if (!Object.is(original, next)) {
937
+ this._config.onIntercept?.({
938
+ key: this.key,
939
+ scope: this.scope,
940
+ original,
941
+ intercepted: next
942
+ });
943
+ }
907
944
  }
908
945
  if (this._hasIsEqual && this._options.isEqual?.(next, prev)) return;
909
946
  s.current = next;
@@ -915,6 +952,7 @@ var RenderStateImpl = class extends StateImpl {
915
952
  hook(next, prev);
916
953
  }
917
954
  }
955
+ this._config.onChange?.({ key: this.key, scope: this.scope, value: next, previousValue: prev });
918
956
  }
919
957
  subscribe(listener) {
920
958
  const s = this._r;
@@ -943,9 +981,18 @@ var RenderStateImpl = class extends StateImpl {
943
981
  const prev = s.current;
944
982
  let next = this._defaultValue;
945
983
  if (s.interceptors !== void 0 && s.interceptors.size > 0) {
984
+ const original = next;
946
985
  for (const interceptor of s.interceptors) {
947
986
  next = interceptor(next, prev);
948
987
  }
988
+ if (!Object.is(original, next)) {
989
+ this._config.onIntercept?.({
990
+ key: this.key,
991
+ scope: this.scope,
992
+ original,
993
+ intercepted: next
994
+ });
995
+ }
949
996
  }
950
997
  if (this._hasIsEqual && this._options.isEqual?.(next, prev)) return;
951
998
  s.current = next;
@@ -957,6 +1004,8 @@ var RenderStateImpl = class extends StateImpl {
957
1004
  hook(next, prev);
958
1005
  }
959
1006
  }
1007
+ this._config.onReset?.({ key: this.key, scope: this.scope, previousValue: prev });
1008
+ this._config.onChange?.({ key: this.key, scope: this.scope, value: next, previousValue: prev });
960
1009
  }
961
1010
  get ready() {
962
1011
  return RESOLVED2;
@@ -181,6 +181,9 @@ function readAndMigrate(raw, options, key, scope) {
181
181
  }
182
182
  }
183
183
  if (options.validate && !options.validate(data)) {
184
+ if (key && scope) {
185
+ getConfig().onValidationFail?.({ key, scope, value: data });
186
+ }
184
187
  return defaultValue;
185
188
  }
186
189
  return data;
@@ -405,6 +408,10 @@ function createBucketAdapter(key, bucketOptions, options) {
405
408
  const hadUserWrite = !shallowEqual(currentValue, defaultValue);
406
409
  delegate.destroy?.();
407
410
  delegate = createStorageAdapter(storage, key, options);
411
+ const bucketValue = delegate.get();
412
+ if (hadUserWrite && shallowEqual(bucketValue, defaultValue)) {
413
+ getConfig().onExpire?.({ key, scope: "bucket", expiredAt: Date.now() });
414
+ }
408
415
  if (hadUserWrite) {
409
416
  delegate.set(currentValue);
410
417
  }
@@ -725,9 +732,18 @@ var StateImpl = class {
725
732
  const prev = this._adapter.get();
726
733
  let next = typeof valueOrUpdater === "function" ? valueOrUpdater(prev) : valueOrUpdater;
727
734
  if (s.interceptors !== void 0 && s.interceptors.size > 0) {
735
+ const original = next;
728
736
  for (const interceptor of s.interceptors) {
729
737
  next = interceptor(next, prev);
730
738
  }
739
+ if (!Object.is(original, next)) {
740
+ this._config.onIntercept?.({
741
+ key: this.key,
742
+ scope: this.scope,
743
+ original,
744
+ intercepted: next
745
+ });
746
+ }
731
747
  }
732
748
  if (this._options.isEqual?.(next, prev)) return;
733
749
  s.lastValue = next;
@@ -738,6 +754,7 @@ var StateImpl = class {
738
754
  hook(next, prev);
739
755
  }
740
756
  }
757
+ this._config.onChange?.({ key: this.key, scope: this.scope, value: next, previousValue: prev });
741
758
  }
742
759
  subscribe(listener) {
743
760
  return this._adapter.subscribe(listener);
@@ -748,9 +765,18 @@ var StateImpl = class {
748
765
  const prev = this._adapter.get();
749
766
  let next = this._defaultValue;
750
767
  if (s.interceptors !== void 0 && s.interceptors.size > 0) {
768
+ const original = next;
751
769
  for (const interceptor of s.interceptors) {
752
770
  next = interceptor(next, prev);
753
771
  }
772
+ if (!Object.is(original, next)) {
773
+ this._config.onIntercept?.({
774
+ key: this.key,
775
+ scope: this.scope,
776
+ original,
777
+ intercepted: next
778
+ });
779
+ }
754
780
  }
755
781
  if (this._options.isEqual?.(next, prev)) return;
756
782
  s.lastValue = next;
@@ -761,6 +787,8 @@ var StateImpl = class {
761
787
  hook(next, prev);
762
788
  }
763
789
  }
790
+ this._config.onReset?.({ key: this.key, scope: this.scope, previousValue: prev });
791
+ this._config.onChange?.({ key: this.key, scope: this.scope, value: next, previousValue: prev });
764
792
  }
765
793
  get ready() {
766
794
  return this._adapter.ready;
@@ -899,9 +927,18 @@ var RenderStateImpl = class extends StateImpl {
899
927
  const prev = s.current;
900
928
  let next = typeof valueOrUpdater === "function" ? valueOrUpdater(prev) : valueOrUpdater;
901
929
  if (s.interceptors !== void 0 && s.interceptors.size > 0) {
930
+ const original = next;
902
931
  for (const interceptor of s.interceptors) {
903
932
  next = interceptor(next, prev);
904
933
  }
934
+ if (!Object.is(original, next)) {
935
+ this._config.onIntercept?.({
936
+ key: this.key,
937
+ scope: this.scope,
938
+ original,
939
+ intercepted: next
940
+ });
941
+ }
905
942
  }
906
943
  if (this._hasIsEqual && this._options.isEqual?.(next, prev)) return;
907
944
  s.current = next;
@@ -913,6 +950,7 @@ var RenderStateImpl = class extends StateImpl {
913
950
  hook(next, prev);
914
951
  }
915
952
  }
953
+ this._config.onChange?.({ key: this.key, scope: this.scope, value: next, previousValue: prev });
916
954
  }
917
955
  subscribe(listener) {
918
956
  const s = this._r;
@@ -941,9 +979,18 @@ var RenderStateImpl = class extends StateImpl {
941
979
  const prev = s.current;
942
980
  let next = this._defaultValue;
943
981
  if (s.interceptors !== void 0 && s.interceptors.size > 0) {
982
+ const original = next;
944
983
  for (const interceptor of s.interceptors) {
945
984
  next = interceptor(next, prev);
946
985
  }
986
+ if (!Object.is(original, next)) {
987
+ this._config.onIntercept?.({
988
+ key: this.key,
989
+ scope: this.scope,
990
+ original,
991
+ intercepted: next
992
+ });
993
+ }
947
994
  }
948
995
  if (this._hasIsEqual && this._options.isEqual?.(next, prev)) return;
949
996
  s.current = next;
@@ -955,6 +1002,8 @@ var RenderStateImpl = class extends StateImpl {
955
1002
  hook(next, prev);
956
1003
  }
957
1004
  }
1005
+ this._config.onReset?.({ key: this.key, scope: this.scope, previousValue: prev });
1006
+ this._config.onChange?.({ key: this.key, scope: this.scope, value: next, previousValue: prev });
958
1007
  }
959
1008
  get ready() {
960
1009
  return RESOLVED2;
package/dist/index.cjs CHANGED
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- var chunkM74MSFH6_cjs = require('./chunk-M74MSFH6.cjs');
3
+ var chunkXACGL7LY_cjs = require('./chunk-XACGL7LY.cjs');
4
4
 
5
5
  // src/collection.ts
6
6
  function collection(key, options) {
7
- const base = chunkM74MSFH6_cjs.createBase(key, options);
7
+ const base = chunkXACGL7LY_cjs.createBase(key, options);
8
8
  const watchers = /* @__PURE__ */ new Map();
9
9
  let prevItems = base.get();
10
10
  const unsubscribe = base.subscribe((next) => {
@@ -139,7 +139,7 @@ function collection(key, options) {
139
139
  // src/computed.ts
140
140
  var computedCounter = 0;
141
141
  function computed(deps, fn, options) {
142
- const listeners = chunkM74MSFH6_cjs.createListeners();
142
+ const listeners = chunkXACGL7LY_cjs.createListeners();
143
143
  const instanceKey = options?.key ?? `computed:${computedCounter++}`;
144
144
  let cached;
145
145
  let isDirty = true;
@@ -164,7 +164,7 @@ function computed(deps, fn, options) {
164
164
  };
165
165
  const markDirty = () => {
166
166
  isDirty = true;
167
- chunkM74MSFH6_cjs.notify(notifyListeners);
167
+ chunkXACGL7LY_cjs.notify(notifyListeners);
168
168
  };
169
169
  const unsubscribers = new Array(depLen);
170
170
  for (let i = 0; i < depLen; i++) {
@@ -224,7 +224,7 @@ function computed(deps, fn, options) {
224
224
 
225
225
  // src/devtools.ts
226
226
  function snapshot() {
227
- const registry = chunkM74MSFH6_cjs.getRegistry();
227
+ const registry = chunkXACGL7LY_cjs.getRegistry();
228
228
  const result = [];
229
229
  for (const instance of registry.values()) {
230
230
  result.push({
@@ -407,7 +407,7 @@ function withWatch(instance) {
407
407
  // src/previous.ts
408
408
  var previousCounter = 0;
409
409
  function previous(source, options) {
410
- const listeners = chunkM74MSFH6_cjs.createListeners();
410
+ const listeners = chunkXACGL7LY_cjs.createListeners();
411
411
  const instanceKey = options?.key ?? `previous:${previousCounter++}`;
412
412
  let prev;
413
413
  let current = source.get();
@@ -417,7 +417,7 @@ function previous(source, options) {
417
417
  prev = current;
418
418
  current = next;
419
419
  if (old !== prev) {
420
- chunkM74MSFH6_cjs.notify(() => listeners.notify(prev));
420
+ chunkXACGL7LY_cjs.notify(() => listeners.notify(prev));
421
421
  }
422
422
  });
423
423
  let destroyedPromise;
@@ -479,7 +479,7 @@ function readonly(instance) {
479
479
  // src/select.ts
480
480
  var selectCounter = 0;
481
481
  function select(source, fn, options) {
482
- const listeners = chunkM74MSFH6_cjs.createListeners();
482
+ const listeners = chunkXACGL7LY_cjs.createListeners();
483
483
  const instanceKey = options?.key ?? `select:${selectCounter++}`;
484
484
  let cached;
485
485
  let isDirty = true;
@@ -498,7 +498,7 @@ function select(source, fn, options) {
498
498
  };
499
499
  const markDirty = () => {
500
500
  isDirty = true;
501
- chunkM74MSFH6_cjs.notify(notifyListeners);
501
+ chunkXACGL7LY_cjs.notify(notifyListeners);
502
502
  };
503
503
  const unsubscribe = source.subscribe(markDirty);
504
504
  recompute();
@@ -553,7 +553,7 @@ function createState(key, options) {
553
553
  if (!key) {
554
554
  throw new Error("[state] key must be a non-empty string.");
555
555
  }
556
- const config = chunkM74MSFH6_cjs.getConfig();
556
+ const config = chunkXACGL7LY_cjs.getConfig();
557
557
  if (config.keyPattern && !config.keyPattern.test(key)) {
558
558
  throw new Error(
559
559
  `[gjendje] Key "${key}" does not match the configured keyPattern ${config.keyPattern}.`
@@ -561,26 +561,26 @@ function createState(key, options) {
561
561
  }
562
562
  const rawScope = options.scope ?? config.scope ?? "render";
563
563
  const scope = rawScope === "memory" ? "render" : rawScope === "session" ? "tab" : rawScope;
564
- const rKey = chunkM74MSFH6_cjs.scopedKey(key, scope);
565
- const existing = chunkM74MSFH6_cjs.getRegistered(rKey);
564
+ const rKey = chunkXACGL7LY_cjs.scopedKey(key, scope);
565
+ const existing = chunkXACGL7LY_cjs.getRegistered(rKey);
566
566
  if (existing && !existing.isDestroyed) {
567
567
  if (config.warnOnDuplicate) {
568
- chunkM74MSFH6_cjs.log("warn", `Duplicate state("${key}") with scope "${scope}". Returning cached instance.`);
568
+ chunkXACGL7LY_cjs.log("warn", `Duplicate state("${key}") with scope "${scope}". Returning cached instance.`);
569
569
  }
570
570
  return existing;
571
571
  }
572
572
  if (scope === "render" && !options.ssr && !config.ssr) {
573
573
  if (options.sync) {
574
- chunkM74MSFH6_cjs.log(
574
+ chunkXACGL7LY_cjs.log(
575
575
  "warn",
576
576
  `sync: true is ignored for scope "render". Only "local" and "bucket" scopes support cross-tab sync.`
577
577
  );
578
578
  }
579
- const instance = chunkM74MSFH6_cjs.createRenderState(key, rKey, options, config);
580
- chunkM74MSFH6_cjs.registerByKey(rKey, key, scope, instance, config);
579
+ const instance = chunkXACGL7LY_cjs.createRenderState(key, rKey, options, config);
580
+ chunkXACGL7LY_cjs.registerByKey(rKey, key, scope, instance, config);
581
581
  return instance;
582
582
  }
583
- return chunkM74MSFH6_cjs.createBase(key, options);
583
+ return chunkXACGL7LY_cjs.createBase(key, options);
584
584
  }
585
585
 
586
586
  // src/shortcuts.ts
@@ -664,15 +664,15 @@ function bucket(entry, options) {
664
664
 
665
665
  Object.defineProperty(exports, "batch", {
666
666
  enumerable: true,
667
- get: function () { return chunkM74MSFH6_cjs.batch; }
667
+ get: function () { return chunkXACGL7LY_cjs.batch; }
668
668
  });
669
669
  Object.defineProperty(exports, "configure", {
670
670
  enumerable: true,
671
- get: function () { return chunkM74MSFH6_cjs.configure; }
671
+ get: function () { return chunkXACGL7LY_cjs.configure; }
672
672
  });
673
673
  Object.defineProperty(exports, "shallowEqual", {
674
674
  enumerable: true,
675
- get: function () { return chunkM74MSFH6_cjs.shallowEqual; }
675
+ get: function () { return chunkXACGL7LY_cjs.shallowEqual; }
676
676
  });
677
677
  exports.bucket = bucket;
678
678
  exports.collection = collection;
package/dist/index.d.cts CHANGED
@@ -145,6 +145,33 @@ interface RegisterContext {
145
145
  key: string;
146
146
  scope: Scope;
147
147
  }
148
+ interface ChangeContext {
149
+ key: string;
150
+ scope: Scope;
151
+ value: unknown;
152
+ previousValue: unknown;
153
+ }
154
+ interface ResetContext {
155
+ key: string;
156
+ scope: Scope;
157
+ previousValue: unknown;
158
+ }
159
+ interface InterceptContext {
160
+ key: string;
161
+ scope: Scope;
162
+ original: unknown;
163
+ intercepted: unknown;
164
+ }
165
+ interface ValidationFailContext {
166
+ key: string;
167
+ scope: Scope;
168
+ value: unknown;
169
+ }
170
+ interface ExpireContext {
171
+ key: string;
172
+ scope: Scope;
173
+ expiredAt: number;
174
+ }
148
175
  interface GjendjeConfig {
149
176
  /** Default scope for all state instances. Defaults to `'render'`. */
150
177
  scope?: Scope | undefined;
@@ -178,6 +205,16 @@ interface GjendjeConfig {
178
205
  onRegister?: ((context: RegisterContext) => void) | undefined;
179
206
  /** Fires when a cross-tab sync event updates a value. */
180
207
  onSync?: ((context: SyncContext) => void) | undefined;
208
+ /** Fires when any state instance's value changes (via set or reset). */
209
+ onChange?: ((context: ChangeContext) => void) | undefined;
210
+ /** Fires when any state instance is reset to its default value. */
211
+ onReset?: ((context: ResetContext) => void) | undefined;
212
+ /** Fires when an interceptor modifies a value. */
213
+ onIntercept?: ((context: InterceptContext) => void) | undefined;
214
+ /** Fires when a validate function rejects a value read from storage. */
215
+ onValidationFail?: ((context: ValidationFailContext) => void) | undefined;
216
+ /** Fires when a storage bucket's data has expired. */
217
+ onExpire?: ((context: ExpireContext) => void) | undefined;
181
218
  }
182
219
  declare function configure(config: GjendjeConfig): void;
183
220
 
@@ -443,4 +480,4 @@ declare function bucket<T>(entry: Record<string, T>, options: BucketShortcutOpti
443
480
  */
444
481
  declare function shallowEqual(a: unknown, b: unknown): boolean;
445
482
 
446
- export { BaseInstance, type CollectionInstance, type ComputedInstance, type ComputedOptions, type DestroyContext, type EffectHandle, type ErrorContext, type GjendjeConfig, type HistoryOptions, type HydrateContext, type LogLevel, type MigrateContext, type PreviousInstance, type PreviousOptions, type QuotaExceededContext, ReadonlyInstance, type RegisterContext, Scope, type SelectInstance, type SelectOptions, type StateFunction, StateInstance, StateOptions, type StateSnapshot, type SyncContext, Unsubscribe, type WithHistoryInstance, type WithWatch, batch, bucket, collection, computed, configure, effect, local, previous, readonly, select, server, session, shallowEqual, snapshot, state, url, withHistory, withWatch };
483
+ export { BaseInstance, type ChangeContext, type CollectionInstance, type ComputedInstance, type ComputedOptions, type DestroyContext, type EffectHandle, type ErrorContext, type ExpireContext, type GjendjeConfig, type HistoryOptions, type HydrateContext, type InterceptContext, type LogLevel, type MigrateContext, type PreviousInstance, type PreviousOptions, type QuotaExceededContext, ReadonlyInstance, type RegisterContext, type ResetContext, Scope, type SelectInstance, type SelectOptions, type StateFunction, StateInstance, StateOptions, type StateSnapshot, type SyncContext, Unsubscribe, type ValidationFailContext, type WithHistoryInstance, type WithWatch, batch, bucket, collection, computed, configure, effect, local, previous, readonly, select, server, session, shallowEqual, snapshot, state, url, withHistory, withWatch };
package/dist/index.d.ts CHANGED
@@ -145,6 +145,33 @@ interface RegisterContext {
145
145
  key: string;
146
146
  scope: Scope;
147
147
  }
148
+ interface ChangeContext {
149
+ key: string;
150
+ scope: Scope;
151
+ value: unknown;
152
+ previousValue: unknown;
153
+ }
154
+ interface ResetContext {
155
+ key: string;
156
+ scope: Scope;
157
+ previousValue: unknown;
158
+ }
159
+ interface InterceptContext {
160
+ key: string;
161
+ scope: Scope;
162
+ original: unknown;
163
+ intercepted: unknown;
164
+ }
165
+ interface ValidationFailContext {
166
+ key: string;
167
+ scope: Scope;
168
+ value: unknown;
169
+ }
170
+ interface ExpireContext {
171
+ key: string;
172
+ scope: Scope;
173
+ expiredAt: number;
174
+ }
148
175
  interface GjendjeConfig {
149
176
  /** Default scope for all state instances. Defaults to `'render'`. */
150
177
  scope?: Scope | undefined;
@@ -178,6 +205,16 @@ interface GjendjeConfig {
178
205
  onRegister?: ((context: RegisterContext) => void) | undefined;
179
206
  /** Fires when a cross-tab sync event updates a value. */
180
207
  onSync?: ((context: SyncContext) => void) | undefined;
208
+ /** Fires when any state instance's value changes (via set or reset). */
209
+ onChange?: ((context: ChangeContext) => void) | undefined;
210
+ /** Fires when any state instance is reset to its default value. */
211
+ onReset?: ((context: ResetContext) => void) | undefined;
212
+ /** Fires when an interceptor modifies a value. */
213
+ onIntercept?: ((context: InterceptContext) => void) | undefined;
214
+ /** Fires when a validate function rejects a value read from storage. */
215
+ onValidationFail?: ((context: ValidationFailContext) => void) | undefined;
216
+ /** Fires when a storage bucket's data has expired. */
217
+ onExpire?: ((context: ExpireContext) => void) | undefined;
181
218
  }
182
219
  declare function configure(config: GjendjeConfig): void;
183
220
 
@@ -443,4 +480,4 @@ declare function bucket<T>(entry: Record<string, T>, options: BucketShortcutOpti
443
480
  */
444
481
  declare function shallowEqual(a: unknown, b: unknown): boolean;
445
482
 
446
- export { BaseInstance, type CollectionInstance, type ComputedInstance, type ComputedOptions, type DestroyContext, type EffectHandle, type ErrorContext, type GjendjeConfig, type HistoryOptions, type HydrateContext, type LogLevel, type MigrateContext, type PreviousInstance, type PreviousOptions, type QuotaExceededContext, ReadonlyInstance, type RegisterContext, Scope, type SelectInstance, type SelectOptions, type StateFunction, StateInstance, StateOptions, type StateSnapshot, type SyncContext, Unsubscribe, type WithHistoryInstance, type WithWatch, batch, bucket, collection, computed, configure, effect, local, previous, readonly, select, server, session, shallowEqual, snapshot, state, url, withHistory, withWatch };
483
+ export { BaseInstance, type ChangeContext, type CollectionInstance, type ComputedInstance, type ComputedOptions, type DestroyContext, type EffectHandle, type ErrorContext, type ExpireContext, type GjendjeConfig, type HistoryOptions, type HydrateContext, type InterceptContext, type LogLevel, type MigrateContext, type PreviousInstance, type PreviousOptions, type QuotaExceededContext, ReadonlyInstance, type RegisterContext, type ResetContext, Scope, type SelectInstance, type SelectOptions, type StateFunction, StateInstance, StateOptions, type StateSnapshot, type SyncContext, Unsubscribe, type ValidationFailContext, type WithHistoryInstance, type WithWatch, batch, bucket, collection, computed, configure, effect, local, previous, readonly, select, server, session, shallowEqual, snapshot, state, url, withHistory, withWatch };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createBase, createListeners, getRegistry, notify, getConfig, scopedKey, getRegistered, log, createRenderState, registerByKey } from './chunk-DVYSYVYB.js';
2
- export { batch, configure, shallowEqual } from './chunk-DVYSYVYB.js';
1
+ import { createBase, createListeners, getRegistry, notify, getConfig, scopedKey, getRegistered, log, createRenderState, registerByKey } from './chunk-ZCZG3Y2B.js';
2
+ export { batch, configure, shallowEqual } from './chunk-ZCZG3Y2B.js';
3
3
 
4
4
  // src/collection.ts
5
5
  function collection(key, options) {
package/dist/server.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkM74MSFH6_cjs = require('./chunk-M74MSFH6.cjs');
3
+ var chunkXACGL7LY_cjs = require('./chunk-XACGL7LY.cjs');
4
4
  var async_hooks = require('async_hooks');
5
5
 
6
6
  var als = new async_hooks.AsyncLocalStorage();
@@ -9,7 +9,7 @@ async function withServerSession(fn) {
9
9
  return als.run(store, fn);
10
10
  }
11
11
  function createServerAdapter(key, defaultValue) {
12
- const listeners = chunkM74MSFH6_cjs.createListeners();
12
+ const listeners = chunkXACGL7LY_cjs.createListeners();
13
13
  function getStore() {
14
14
  return als.getStore();
15
15
  }
@@ -31,7 +31,7 @@ function createServerAdapter(key, defaultValue) {
31
31
  }
32
32
  store.set(key, value);
33
33
  lastNotifiedValue = value;
34
- chunkM74MSFH6_cjs.notify(notifyListeners);
34
+ chunkXACGL7LY_cjs.notify(notifyListeners);
35
35
  },
36
36
  subscribe: listeners.subscribe,
37
37
  destroy() {
@@ -39,7 +39,7 @@ function createServerAdapter(key, defaultValue) {
39
39
  }
40
40
  };
41
41
  }
42
- chunkM74MSFH6_cjs.registerServerAdapter(createServerAdapter);
42
+ chunkXACGL7LY_cjs.registerServerAdapter(createServerAdapter);
43
43
 
44
44
  exports.createServerAdapter = createServerAdapter;
45
45
  exports.withServerSession = withServerSession;
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { registerServerAdapter, createListeners, notify } from './chunk-DVYSYVYB.js';
1
+ import { registerServerAdapter, createListeners, notify } from './chunk-ZCZG3Y2B.js';
2
2
  import { AsyncLocalStorage } from 'async_hooks';
3
3
 
4
4
  var als = new AsyncLocalStorage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gjendje",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "TypeScript state management",
5
5
  "keywords": [
6
6
  "state",