coaction 2.0.0 → 2.1.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
@@ -1,21 +1,17 @@
1
1
  # coaction
2
2
 
3
- ![Node CI](https://github.com/coactionjs/coaction/workflows/Node%20CI/badge.svg)
4
- [![npm](https://img.shields.io/npm/v/coaction.svg)](https://www.npmjs.com/package/coaction)
5
- ![license](https://img.shields.io/npm/l/coaction)
3
+ ![Node CI](https://github.com/coactionjs/coaction/workflows/Node%20CI/badge.svg) [![npm](https://img.shields.io/npm/v/coaction.svg)](https://www.npmjs.com/package/coaction) ![license](https://img.shields.io/npm/l/coaction)
6
4
 
7
5
  An efficient and flexible state management library for building high-performance, multithreading web applications.
8
6
 
9
- Coaction 2.0 uses `alien-signals` internally for cached getter/computed state,
10
- React selector reactivity, and adapter-facing subscriptions. The core package
11
- also re-exports the signal primitives for advanced integrations.
7
+ Coaction uses `alien-signals` internally for cached getter/computed state, React selector reactivity, and adapter-facing subscriptions. The core package also re-exports the signal primitives for advanced integrations.
12
8
 
13
9
  ## Installation
14
10
 
15
- You can install it via npm, yarn or pnpm.
11
+ Install it with pnpm:
16
12
 
17
13
  ```sh
18
- npm install coaction
14
+ pnpm add coaction
19
15
  ```
20
16
 
21
17
  ## Usage
@@ -36,8 +32,13 @@ const store = create((set) => ({
36
32
  }));
37
33
  ```
38
34
 
39
- Accessor getters are cached automatically through the built-in signal runtime.
40
- Use `get(deps, selector)` when you want to declare dependencies manually:
35
+ Core stores are immutable by default. Getters and methods can read through `this`, but writes to Coaction-owned state must happen inside `set()` or `set((draft) => ...)`. Direct writes such as `this.count += 1` in a store method throw because they bypass the commit path that notifies subscribers, produces patches when enabled, and synchronizes worker/client mirrors in shared mode.
36
+
37
+ Coaction fixes the public state schema after initialization. A single store cannot add new top-level state keys later, and a slices store cannot add new slice keys or new top-level fields inside a slice. Replacement-style APIs such as `apply()` may omit a known single-store root key; the public getter remains present and reads as `undefined`, but no unknown key is promoted into the public module. Slice root keys are stricter and cannot be removed or replaced with non-object values. Keep dynamic data inside an existing object or array field.
38
+
39
+ Mutable adapters such as MobX, Pinia, and Valtio keep Coaction raw state, public state, and the external mutable runtime synchronized for known schema keys. Coaction still treats its raw/public schema as authoritative: out-of-band unknown properties written directly onto a third-party mutable runtime are not promoted into Coaction state, and adapter-specific docs define whether that external runtime property is pruned, restored, or left to the underlying library.
40
+
41
+ Accessor getters are cached automatically through the built-in signal runtime. Use `get(deps, selector)` when you want to declare dependencies manually:
41
42
 
42
43
  ```ts
43
44
  const store = create((set, get) => ({
@@ -54,15 +55,24 @@ const store = create((set, get) => ({
54
55
  }));
55
56
  ```
56
57
 
57
- Advanced integrations can import the native signal primitives and adapter helper
58
- directly from `coaction`:
58
+ Advanced integrations can import the native signal primitives and adapter helper directly from `coaction`:
59
59
 
60
60
  ```ts
61
61
  import { computed, defineExternalStoreAdapter, effect, signal } from 'coaction';
62
62
  ```
63
63
 
64
- Store methods using `this` are rebound to the latest state when invoked from
65
- `getState()`, so destructuring remains safe:
64
+ ### Adapter and Middleware Utilities
65
+
66
+ `coaction` also exports utilities for adapter and middleware authors. These are not needed for normal application state updates, but they are part of the supported integration surface used by the official packages:
67
+
68
+ - Mutable adapter helpers: `applyMutableAdapterPatches`, `replaceMutableAdapterState`, `toMutableAdapterSnapshot`, `snapshotMutableAdapterPureState`, `isEqualMutableAdapterSnapshot`, `getMutableAdapterOwnEnumerableKeys`, `isMutableAdapterUnsafeKey`.
69
+ - Root replacement helpers: `createRootReplacementPatches`, `applyRootReplacementWithPatches`.
70
+ - Patch safety helpers: `assertSafePatches`, `sanitizePatches`, `UnsafePatchPathError`.
71
+ - State shape helpers: `StateSchemaError`, `isStateSchemaError`, `sanitizeReplacementState`, `sanitizeInitialStateValue`, `replaceOwnEnumerable`.
72
+
73
+ Runtime mutation paths reject unsafe patch paths before applying state changes. If a `store.patch()` hook returns a path containing `__proto__`, `prototype`, or `constructor`, Coaction throws `UnsafePatchPathError` instead of silently dropping that patch and applying the rest.
74
+
75
+ Store methods using `this` are rebound to the latest state when invoked from `getState()`, so destructuring remains safe:
66
76
 
67
77
  ```ts
68
78
  const store = create((set) => ({
@@ -85,11 +95,7 @@ increment();
85
95
 
86
96
  ### Store Shape Mode (`sliceMode`)
87
97
 
88
- `create()` uses `sliceMode: 'auto'` by default. For backward compatibility,
89
- `auto` still treats a non-empty object whose enumerable values are all
90
- functions as slices. That shape is ambiguous with a plain store that only
91
- contains methods, so development builds warn and you should set `sliceMode`
92
- explicitly.
98
+ `create()` uses `sliceMode: 'auto'` by default. For backward compatibility, `auto` still treats a non-empty object whose enumerable values are all functions as slices. That shape is ambiguous with a plain store that only contains methods, so development builds warn and you should set `sliceMode` explicitly.
93
99
 
94
100
  You can force behavior explicitly:
95
101
 
package/dist/index.d.mts CHANGED
@@ -393,9 +393,14 @@ declare const create: Creator;
393
393
  //#endregion
394
394
  //#region packages/core/src/internal.d.ts
395
395
  type MutationOperation = 'setState' | 'apply';
396
+ type StoreOperation = MutationOperation | 'subscribe' | `action ${string}`;
396
397
  type SignalSlot = {
397
398
  refresh: () => void;
398
399
  };
400
+ type StateSchema = {
401
+ rootKeys: Set<PropertyKey>;
402
+ sliceKeys?: Map<PropertyKey, Set<PropertyKey>>;
403
+ };
399
404
  interface Internal<T extends CreateState = CreateState> {
400
405
  /**
401
406
  * The store module.
@@ -438,6 +443,10 @@ interface Internal<T extends CreateState = CreateState> {
438
443
  * Reactive state slots used by computed getters/selectors.
439
444
  */
440
445
  signalSlots?: Set<SignalSlot>;
446
+ /**
447
+ * State keys that are allowed after initialization.
448
+ */
449
+ stateSchema?: StateSchema;
441
450
  /**
442
451
  * The act is used to run the function in the action for mutable state.
443
452
  */
@@ -454,6 +463,10 @@ interface Internal<T extends CreateState = CreateState> {
454
463
  * Adapter-level authority check for low-level mutations.
455
464
  */
456
465
  assertMutationAllowed?: (operation: MutationOperation) => void;
466
+ /**
467
+ * Store lifecycle guard.
468
+ */
469
+ assertAlive?: (operation: StoreOperation) => void;
457
470
  /**
458
471
  * Authorized client-mirror state application used by transports.
459
472
  */
@@ -555,7 +568,45 @@ declare const replaceExternalStoreState: <T extends CreateState>(store: Middlewa
555
568
  syncImmutable
556
569
  }?: ReplaceExternalStoreStateOptions) => void;
557
570
  //#endregion
571
+ //#region packages/core/src/externalMutableAdapterUtils.d.ts
572
+ declare const getMutableAdapterOwnEnumerableKeys: (value: object) => (string | symbol)[];
573
+ declare const isMutableAdapterUnsafeKey: (key: PropertyKey) => key is "__proto__" | "prototype" | "constructor";
574
+ declare const replaceMutableAdapterState: (rawState: Record<PropertyKey, unknown>, mutableState: Record<PropertyKey, unknown>, publicState: Record<PropertyKey, unknown>, source: Record<PropertyKey, unknown>) => void;
575
+ declare const applyMutableAdapterPatches: (baseState: unknown, patches: Patches, rawState: Record<PropertyKey, unknown>, mutableState: Record<PropertyKey, unknown>, publicState: Record<PropertyKey, unknown>) => void;
576
+ declare const toMutableAdapterSnapshot: (value: unknown, visited?: WeakMap<object, unknown>) => unknown;
577
+ declare const snapshotMutableAdapterPureState: (store: Store<object>) => Record<PropertyKey, unknown>;
578
+ declare const isEqualMutableAdapterSnapshot: (left: unknown, right: unknown, visited?: WeakMap<object, WeakSet<object>>) => boolean;
579
+ //#endregion
558
580
  //#region packages/core/src/utils.d.ts
581
+ declare class UnsafePatchPathError extends Error {
582
+ name: string;
583
+ }
584
+ declare class StateSchemaError extends Error {
585
+ name: string;
586
+ }
587
+ declare const isStateSchemaError: (error: unknown) => error is StateSchemaError;
588
+ declare const assertSafePatches: <T extends {
589
+ path: unknown;
590
+ }>(patches: T[] | undefined, source?: string) => void;
591
+ declare const sanitizePatches: <T extends {
592
+ path: unknown;
593
+ value?: unknown;
594
+ }>(patches: T[] | undefined, options?: {
595
+ source?: string;
596
+ warnOnDropped?: boolean;
597
+ }) => T[] | undefined;
598
+ type RootReplacementPatch = {
599
+ op: 'add' | 'remove' | 'replace';
600
+ path: PropertyKey[];
601
+ value?: unknown;
602
+ };
603
+ declare const createRootReplacementPatches: (currentState: Record<PropertyKey, unknown>, nextState: Record<PropertyKey, unknown>) => {
604
+ patches: RootReplacementPatch[];
605
+ inversePatches: RootReplacementPatch[];
606
+ };
607
+ declare const applyRootReplacementWithPatches: <T extends object>(store: MiddlewareStore<T>, nextState: Record<PropertyKey, unknown>, options?: {
608
+ applyExactReplacement?: (state: T) => void;
609
+ }) => [T, Patches, Patches];
559
610
  declare const replaceOwnEnumerable: (target: Record<PropertyKey, unknown>, source: Record<PropertyKey, unknown>) => void;
560
611
  declare const sanitizeReplacementState: <T>(source: T, seen?: WeakMap<object, unknown>) => T;
561
612
  declare const sanitizeInitialStateValue: <T>(source: T, seen?: WeakMap<object, unknown>) => T;
@@ -572,4 +623,4 @@ declare const sanitizeInitialStateValue: <T>(source: T, seen?: WeakMap<object, u
572
623
  */
573
624
  declare const wrapStore: <T extends object>(store: Store<T>, getState?: (...args: unknown[]) => T) => StoreReturn<T>;
574
625
  //#endregion
575
- export { type StoreWithAsyncFunction as AsyncStore, type Asyncify, type ClientStoreOptions, type ExternalStoreAdapterOptions, type ISlices, type Middleware, type MiddlewareStore, type PatchTransform, type ReactiveTracker, type Slice, type SliceState, type Slices, type Store, type StoreOptions, type StoreTraceEvent, computed, create, createBinder, createReactiveTracker, defineExternalStoreAdapter, effect, effectScope, endBatch, isComputed, isEffect, isEffectScope, isSignal, onStoreReady, replaceExternalStoreState, replaceOwnEnumerable, sanitizeInitialStateValue, sanitizeReplacementState, signal, startBatch, trigger, wrapStore };
626
+ export { type StoreWithAsyncFunction as AsyncStore, type Asyncify, type ClientStoreOptions, type ExternalStoreAdapterOptions, type ISlices, type Middleware, type MiddlewareStore, type PatchTransform, type ReactiveTracker, type Slice, type SliceState, type Slices, StateSchemaError, type Store, type StoreOptions, type StoreTraceEvent, UnsafePatchPathError, applyMutableAdapterPatches, applyRootReplacementWithPatches, assertSafePatches, computed, create, createBinder, createReactiveTracker, createRootReplacementPatches, defineExternalStoreAdapter, effect, effectScope, endBatch, getMutableAdapterOwnEnumerableKeys, isComputed, isEffect, isEffectScope, isEqualMutableAdapterSnapshot, isMutableAdapterUnsafeKey, isSignal, isStateSchemaError, onStoreReady, replaceExternalStoreState, replaceMutableAdapterState, replaceOwnEnumerable, sanitizeInitialStateValue, sanitizePatches, sanitizeReplacementState, signal, snapshotMutableAdapterPureState, startBatch, toMutableAdapterSnapshot, trigger, wrapStore };
package/dist/index.d.ts CHANGED
@@ -393,9 +393,14 @@ declare const create: Creator;
393
393
  //#endregion
394
394
  //#region packages/core/src/internal.d.ts
395
395
  type MutationOperation = 'setState' | 'apply';
396
+ type StoreOperation = MutationOperation | 'subscribe' | `action ${string}`;
396
397
  type SignalSlot = {
397
398
  refresh: () => void;
398
399
  };
400
+ type StateSchema = {
401
+ rootKeys: Set<PropertyKey>;
402
+ sliceKeys?: Map<PropertyKey, Set<PropertyKey>>;
403
+ };
399
404
  interface Internal<T extends CreateState = CreateState> {
400
405
  /**
401
406
  * The store module.
@@ -438,6 +443,10 @@ interface Internal<T extends CreateState = CreateState> {
438
443
  * Reactive state slots used by computed getters/selectors.
439
444
  */
440
445
  signalSlots?: Set<SignalSlot>;
446
+ /**
447
+ * State keys that are allowed after initialization.
448
+ */
449
+ stateSchema?: StateSchema;
441
450
  /**
442
451
  * The act is used to run the function in the action for mutable state.
443
452
  */
@@ -454,6 +463,10 @@ interface Internal<T extends CreateState = CreateState> {
454
463
  * Adapter-level authority check for low-level mutations.
455
464
  */
456
465
  assertMutationAllowed?: (operation: MutationOperation) => void;
466
+ /**
467
+ * Store lifecycle guard.
468
+ */
469
+ assertAlive?: (operation: StoreOperation) => void;
457
470
  /**
458
471
  * Authorized client-mirror state application used by transports.
459
472
  */
@@ -555,7 +568,45 @@ declare const replaceExternalStoreState: <T extends CreateState>(store: Middlewa
555
568
  syncImmutable
556
569
  }?: ReplaceExternalStoreStateOptions) => void;
557
570
  //#endregion
571
+ //#region packages/core/src/externalMutableAdapterUtils.d.ts
572
+ declare const getMutableAdapterOwnEnumerableKeys: (value: object) => (string | symbol)[];
573
+ declare const isMutableAdapterUnsafeKey: (key: PropertyKey) => key is "__proto__" | "prototype" | "constructor";
574
+ declare const replaceMutableAdapterState: (rawState: Record<PropertyKey, unknown>, mutableState: Record<PropertyKey, unknown>, publicState: Record<PropertyKey, unknown>, source: Record<PropertyKey, unknown>) => void;
575
+ declare const applyMutableAdapterPatches: (baseState: unknown, patches: Patches, rawState: Record<PropertyKey, unknown>, mutableState: Record<PropertyKey, unknown>, publicState: Record<PropertyKey, unknown>) => void;
576
+ declare const toMutableAdapterSnapshot: (value: unknown, visited?: WeakMap<object, unknown>) => unknown;
577
+ declare const snapshotMutableAdapterPureState: (store: Store<object>) => Record<PropertyKey, unknown>;
578
+ declare const isEqualMutableAdapterSnapshot: (left: unknown, right: unknown, visited?: WeakMap<object, WeakSet<object>>) => boolean;
579
+ //#endregion
558
580
  //#region packages/core/src/utils.d.ts
581
+ declare class UnsafePatchPathError extends Error {
582
+ name: string;
583
+ }
584
+ declare class StateSchemaError extends Error {
585
+ name: string;
586
+ }
587
+ declare const isStateSchemaError: (error: unknown) => error is StateSchemaError;
588
+ declare const assertSafePatches: <T extends {
589
+ path: unknown;
590
+ }>(patches: T[] | undefined, source?: string) => void;
591
+ declare const sanitizePatches: <T extends {
592
+ path: unknown;
593
+ value?: unknown;
594
+ }>(patches: T[] | undefined, options?: {
595
+ source?: string;
596
+ warnOnDropped?: boolean;
597
+ }) => T[] | undefined;
598
+ type RootReplacementPatch = {
599
+ op: 'add' | 'remove' | 'replace';
600
+ path: PropertyKey[];
601
+ value?: unknown;
602
+ };
603
+ declare const createRootReplacementPatches: (currentState: Record<PropertyKey, unknown>, nextState: Record<PropertyKey, unknown>) => {
604
+ patches: RootReplacementPatch[];
605
+ inversePatches: RootReplacementPatch[];
606
+ };
607
+ declare const applyRootReplacementWithPatches: <T extends object>(store: MiddlewareStore<T>, nextState: Record<PropertyKey, unknown>, options?: {
608
+ applyExactReplacement?: (state: T) => void;
609
+ }) => [T, Patches, Patches];
559
610
  declare const replaceOwnEnumerable: (target: Record<PropertyKey, unknown>, source: Record<PropertyKey, unknown>) => void;
560
611
  declare const sanitizeReplacementState: <T>(source: T, seen?: WeakMap<object, unknown>) => T;
561
612
  declare const sanitizeInitialStateValue: <T>(source: T, seen?: WeakMap<object, unknown>) => T;
@@ -572,4 +623,4 @@ declare const sanitizeInitialStateValue: <T>(source: T, seen?: WeakMap<object, u
572
623
  */
573
624
  declare const wrapStore: <T extends object>(store: Store<T>, getState?: (...args: unknown[]) => T) => StoreReturn<T>;
574
625
  //#endregion
575
- export { type StoreWithAsyncFunction as AsyncStore, type Asyncify, type ClientStoreOptions, type ExternalStoreAdapterOptions, type ISlices, type Middleware, type MiddlewareStore, type PatchTransform, type ReactiveTracker, type Slice, type SliceState, type Slices, type Store, type StoreOptions, type StoreTraceEvent, computed, create, createBinder, createReactiveTracker, defineExternalStoreAdapter, effect, effectScope, endBatch, isComputed, isEffect, isEffectScope, isSignal, onStoreReady, replaceExternalStoreState, replaceOwnEnumerable, sanitizeInitialStateValue, sanitizeReplacementState, signal, startBatch, trigger, wrapStore };
626
+ export { type StoreWithAsyncFunction as AsyncStore, type Asyncify, type ClientStoreOptions, type ExternalStoreAdapterOptions, type ISlices, type Middleware, type MiddlewareStore, type PatchTransform, type ReactiveTracker, type Slice, type SliceState, type Slices, StateSchemaError, type Store, type StoreOptions, type StoreTraceEvent, UnsafePatchPathError, applyMutableAdapterPatches, applyRootReplacementWithPatches, assertSafePatches, computed, create, createBinder, createReactiveTracker, createRootReplacementPatches, defineExternalStoreAdapter, effect, effectScope, endBatch, getMutableAdapterOwnEnumerableKeys, isComputed, isEffect, isEffectScope, isEqualMutableAdapterSnapshot, isMutableAdapterUnsafeKey, isSignal, isStateSchemaError, onStoreReady, replaceExternalStoreState, replaceMutableAdapterState, replaceOwnEnumerable, sanitizeInitialStateValue, sanitizePatches, sanitizeReplacementState, signal, snapshotMutableAdapterPureState, startBatch, toMutableAdapterSnapshot, trigger, wrapStore };