state-sync-log 0.9.0 → 0.10.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/CHANGELOG.md +5 -0
- package/README.md +368 -277
- package/dist/state-sync-log.esm.js +929 -136
- package/dist/state-sync-log.esm.mjs +929 -136
- package/dist/state-sync-log.umd.js +928 -135
- package/dist/types/createOps/constant.d.ts +6 -0
- package/dist/types/createOps/createOps.d.ts +25 -0
- package/dist/types/createOps/current.d.ts +13 -0
- package/dist/types/createOps/draft.d.ts +14 -0
- package/dist/types/createOps/draftify.d.ts +5 -0
- package/dist/types/createOps/index.d.ts +12 -0
- package/dist/types/createOps/interface.d.ts +74 -0
- package/dist/types/createOps/original.d.ts +15 -0
- package/dist/types/createOps/pushOp.d.ts +9 -0
- package/dist/types/createOps/setHelpers.d.ts +25 -0
- package/dist/types/createOps/utils.d.ts +95 -0
- package/dist/types/draft.d.ts +2 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/json.d.ts +1 -1
- package/dist/types/operations.d.ts +2 -2
- package/dist/types/utils.d.ts +5 -0
- package/package.json +1 -1
- package/src/createOps/constant.ts +10 -0
- package/src/createOps/createOps.ts +97 -0
- package/src/createOps/current.ts +85 -0
- package/src/createOps/draft.ts +606 -0
- package/src/createOps/draftify.ts +45 -0
- package/src/createOps/index.ts +18 -0
- package/src/createOps/interface.ts +95 -0
- package/src/createOps/original.ts +24 -0
- package/src/createOps/pushOp.ts +42 -0
- package/src/createOps/setHelpers.ts +93 -0
- package/src/createOps/utils.ts +325 -0
- package/src/draft.ts +306 -288
- package/src/index.ts +1 -0
- package/src/json.ts +1 -1
- package/src/operations.ts +33 -11
- package/src/utils.ts +67 -55
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CreateOpsResult, Draft } from './interface';
|
|
2
|
+
/**
|
|
3
|
+
* Create operations from mutable-style mutations.
|
|
4
|
+
*
|
|
5
|
+
* @param base - The base state (will not be mutated)
|
|
6
|
+
* @param mutate - A function that mutates the draft
|
|
7
|
+
* @returns An object containing the next state and the operations performed
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const state = { list: [{ text: 'Learn', done: false }] };
|
|
12
|
+
*
|
|
13
|
+
* const { nextState, ops } = createOps(state, (draft) => {
|
|
14
|
+
* draft.list[0].done = true;
|
|
15
|
+
* draft.list.push({ text: 'Practice', done: false });
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // ops contains the operations that were performed:
|
|
19
|
+
* // [
|
|
20
|
+
* // { kind: 'set', path: ['list', 0], key: 'done', value: true },
|
|
21
|
+
* // { kind: 'splice', path: ['list'], index: 1, deleteCount: 0, inserts: [{ text: 'Practice', done: false }] }
|
|
22
|
+
* // ]
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function createOps<T extends object>(base: T, mutate: (draft: Draft<T>) => void): CreateOpsResult<T>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Draft } from './interface';
|
|
2
|
+
/**
|
|
3
|
+
* `current(draft)` to get current state in the draft mutation function.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const { nextState, ops } = createOps(baseState, (draft) => {
|
|
8
|
+
* draft.foo.bar = 'new value';
|
|
9
|
+
* console.log(current(draft.foo)); // { bar: 'new value' }
|
|
10
|
+
* });
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function current<T extends object>(target: Draft<T>): T;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Finalities, Op, ProxyDraft } from './interface';
|
|
2
|
+
/**
|
|
3
|
+
* Create a draft proxy for a value
|
|
4
|
+
*/
|
|
5
|
+
export declare function createDraft<T extends object>(options: {
|
|
6
|
+
original: T;
|
|
7
|
+
parentDraft?: ProxyDraft | null;
|
|
8
|
+
key?: string | number;
|
|
9
|
+
finalities: Finalities;
|
|
10
|
+
}): T;
|
|
11
|
+
/**
|
|
12
|
+
* Finalize a draft and return the result with ops
|
|
13
|
+
*/
|
|
14
|
+
export declare function finalizeDraft<T>(result: T, returnedValue: [T] | []): [T, Op[]];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createOps - Proxy-based mutable-style API for generating operations.
|
|
3
|
+
*
|
|
4
|
+
* Forked from mutative (https://github.com/unadlib/mutative)
|
|
5
|
+
* MIT License
|
|
6
|
+
*/
|
|
7
|
+
export { createOps } from './createOps';
|
|
8
|
+
export { current } from './current';
|
|
9
|
+
export type { CreateOpsResult, Draft, Immutable, Op, Path } from './interface';
|
|
10
|
+
export { original } from './original';
|
|
11
|
+
export { addToSet, deleteFromSet } from './setHelpers';
|
|
12
|
+
export { isDraft, isDraftable } from './utils';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { JSONPrimitive, JSONValue, Path } from '../json';
|
|
2
|
+
import { Op } from '../operations';
|
|
3
|
+
export type { Op, Path, JSONValue };
|
|
4
|
+
export declare enum DraftType {
|
|
5
|
+
Object = 0,
|
|
6
|
+
Array = 1
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Finalities - shared state for the draft tree
|
|
10
|
+
*/
|
|
11
|
+
export interface Finalities {
|
|
12
|
+
/** Finalization callbacks (for unwrapping child drafts) */
|
|
13
|
+
draft: (() => void)[];
|
|
14
|
+
/** Revoke functions for all proxies */
|
|
15
|
+
revoke: (() => void)[];
|
|
16
|
+
/** Set of handled objects (for cycle detection) */
|
|
17
|
+
handledSet: WeakSet<object>;
|
|
18
|
+
/** Cache of created drafts */
|
|
19
|
+
draftsCache: WeakSet<object>;
|
|
20
|
+
/** List of operations performed in this draft session (eager logging) */
|
|
21
|
+
ops: Op[];
|
|
22
|
+
/** Root draft of the tree (set when creating the root draft) */
|
|
23
|
+
rootDraft: ProxyDraft | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Internal proxy draft state
|
|
27
|
+
*/
|
|
28
|
+
export interface ProxyDraft<T = any> {
|
|
29
|
+
/** Type of the draft (Object or Array) */
|
|
30
|
+
type: DraftType;
|
|
31
|
+
/** Whether this draft has been mutated */
|
|
32
|
+
operated?: boolean;
|
|
33
|
+
/** Whether finalization has been completed */
|
|
34
|
+
finalized: boolean;
|
|
35
|
+
/** The original (unmodified) value */
|
|
36
|
+
original: T;
|
|
37
|
+
/** The shallow copy (created on first mutation) */
|
|
38
|
+
copy: T | null;
|
|
39
|
+
/** The proxy instance */
|
|
40
|
+
proxy: T | null;
|
|
41
|
+
/** Finalities container (shared across draft tree) */
|
|
42
|
+
finalities: Finalities;
|
|
43
|
+
/** Parent draft (for path tracking) */
|
|
44
|
+
parent?: ProxyDraft | null;
|
|
45
|
+
/** Key in parent */
|
|
46
|
+
key?: string | number;
|
|
47
|
+
/** Track which keys have been assigned (key -> true=assigned, false=deleted) */
|
|
48
|
+
assignedMap?: Map<PropertyKey, boolean>;
|
|
49
|
+
/** Count of positions this draft exists at (for aliasing optimization) */
|
|
50
|
+
aliasCount: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Result of createOps
|
|
54
|
+
*/
|
|
55
|
+
export interface CreateOpsResult<T> {
|
|
56
|
+
/** The new immutable state */
|
|
57
|
+
nextState: T;
|
|
58
|
+
/** The operations that were performed */
|
|
59
|
+
ops: Op[];
|
|
60
|
+
}
|
|
61
|
+
/** Primitive types that don't need drafting */
|
|
62
|
+
type Primitive = JSONPrimitive;
|
|
63
|
+
/**
|
|
64
|
+
* Draft type - makes all properties mutable for editing
|
|
65
|
+
*/
|
|
66
|
+
export type Draft<T> = T extends Primitive ? T : T extends object ? {
|
|
67
|
+
-readonly [K in keyof T]: Draft<T[K]>;
|
|
68
|
+
} : T;
|
|
69
|
+
/**
|
|
70
|
+
* Immutable type - makes all properties readonly
|
|
71
|
+
*/
|
|
72
|
+
export type Immutable<T> = T extends Primitive ? T : T extends object ? {
|
|
73
|
+
readonly [K in keyof T]: Immutable<T[K]>;
|
|
74
|
+
} : T;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the original value from a draft.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* `original(draft)` to get original state in the draft mutation function.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const { nextState, ops } = createOps(baseState, (draft) => {
|
|
10
|
+
* draft.foo.bar = 'new value';
|
|
11
|
+
* console.log(original(draft.foo)); // { bar: 'old value' }
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function original<T>(target: T): T;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Op, ProxyDraft } from './interface';
|
|
2
|
+
/**
|
|
3
|
+
* Push an operation to the ops log.
|
|
4
|
+
* Values should already be cloned by the caller to avoid aliasing issues.
|
|
5
|
+
*
|
|
6
|
+
* When the target draft exists at multiple positions (due to aliasing),
|
|
7
|
+
* this function emits ops for all positions to maintain consistency.
|
|
8
|
+
*/
|
|
9
|
+
export declare function pushOp(proxyDraft: ProxyDraft, op: Op): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { JSONValue } from '../json';
|
|
2
|
+
/**
|
|
3
|
+
* Add a value to an array if it doesn't already exist (set semantics).
|
|
4
|
+
* Generates an `addToSet` operation.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* createOps(state, (draft) => {
|
|
9
|
+
* addToSet(draft.tags, 'newTag'); // Only adds if not present
|
|
10
|
+
* });
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function addToSet<T extends JSONValue>(draft: T[], value: T): void;
|
|
14
|
+
/**
|
|
15
|
+
* Remove a value from an array (set semantics).
|
|
16
|
+
* Generates a `deleteFromSet` operation.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* createOps(state, (draft) => {
|
|
21
|
+
* deleteFromSet(draft.tags, 'oldTag'); // Removes all matching items
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function deleteFromSet<T extends JSONValue>(draft: T[], value: T): void;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DraftType, ProxyDraft } from './interface';
|
|
2
|
+
/**
|
|
3
|
+
* Get the latest value (copy if exists, otherwise original)
|
|
4
|
+
*/
|
|
5
|
+
export declare function latest<T>(proxyDraft: ProxyDraft<T>): T;
|
|
6
|
+
/**
|
|
7
|
+
* Check if the value is a draft
|
|
8
|
+
*/
|
|
9
|
+
export declare function isDraft(target: unknown): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Get the ProxyDraft from a draft value
|
|
12
|
+
*/
|
|
13
|
+
export declare function getProxyDraft<T>(value: unknown): ProxyDraft<T> | null;
|
|
14
|
+
/**
|
|
15
|
+
* Get the actual value from a draft (copy or original)
|
|
16
|
+
*/
|
|
17
|
+
export declare function getValue<T extends object>(value: T): T;
|
|
18
|
+
/**
|
|
19
|
+
* Check if a value is draftable (plain object or array)
|
|
20
|
+
* We only support plain objects and arrays - no Map, Set, Date, etc.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isDraftable(value: unknown): value is object;
|
|
23
|
+
/**
|
|
24
|
+
* Get the draft type
|
|
25
|
+
*/
|
|
26
|
+
export declare function getType(target: unknown): DraftType;
|
|
27
|
+
/**
|
|
28
|
+
* Get a value by key
|
|
29
|
+
*/
|
|
30
|
+
export declare function get(target: object, key: PropertyKey): unknown;
|
|
31
|
+
/**
|
|
32
|
+
* Set a value by key
|
|
33
|
+
*/
|
|
34
|
+
export declare function set(target: object, key: PropertyKey, value: unknown): void;
|
|
35
|
+
/**
|
|
36
|
+
* Check if a key exists (own property)
|
|
37
|
+
*/
|
|
38
|
+
export declare function has(target: object, key: PropertyKey): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Peek at a value (through drafts)
|
|
41
|
+
*/
|
|
42
|
+
export declare function peek(target: object, key: PropertyKey): unknown;
|
|
43
|
+
/**
|
|
44
|
+
* SameValue comparison (handles -0 and NaN)
|
|
45
|
+
*/
|
|
46
|
+
export declare function isEqual(x: unknown, y: unknown): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Revoke all proxies in a draft tree
|
|
49
|
+
*/
|
|
50
|
+
export declare function revokeProxy(proxyDraft: ProxyDraft | null): void;
|
|
51
|
+
/**
|
|
52
|
+
* Get the path from root to this draft
|
|
53
|
+
*/
|
|
54
|
+
export declare function getPath(target: ProxyDraft, path?: (string | number)[]): (string | number)[] | null;
|
|
55
|
+
/**
|
|
56
|
+
* Get the path from root to this draft, or throw if not available
|
|
57
|
+
*/
|
|
58
|
+
export declare function getPathOrThrow(target: ProxyDraft): (string | number)[];
|
|
59
|
+
/**
|
|
60
|
+
* Find all paths to a given draft by searching the tree.
|
|
61
|
+
* This handles aliasing where the same draft exists at multiple positions.
|
|
62
|
+
*/
|
|
63
|
+
export declare function getAllPathsForDraft(rootDraft: ProxyDraft, targetDraft: ProxyDraft): (string | number)[][];
|
|
64
|
+
/**
|
|
65
|
+
* Get a property descriptor from the prototype chain
|
|
66
|
+
*/
|
|
67
|
+
export declare function getDescriptor(target: object, key: PropertyKey): PropertyDescriptor | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Create a shallow copy of an object or array
|
|
70
|
+
*/
|
|
71
|
+
export declare function shallowCopy<T>(original: T): T;
|
|
72
|
+
/**
|
|
73
|
+
* Ensure a draft has a shallow copy
|
|
74
|
+
*/
|
|
75
|
+
export declare function ensureShallowCopy(target: ProxyDraft): void;
|
|
76
|
+
/**
|
|
77
|
+
* Deep clone a value, unwrapping any drafts
|
|
78
|
+
*/
|
|
79
|
+
export declare function deepClone<T>(target: T): T;
|
|
80
|
+
/**
|
|
81
|
+
* Clone if the value is a draft, otherwise return as-is
|
|
82
|
+
*/
|
|
83
|
+
export declare function cloneIfNeeded<T>(target: T): T;
|
|
84
|
+
/**
|
|
85
|
+
* Mark a draft as changed (operated)
|
|
86
|
+
*/
|
|
87
|
+
export declare function markChanged(target: ProxyDraft): void;
|
|
88
|
+
/**
|
|
89
|
+
* Iterate over object/array entries
|
|
90
|
+
*/
|
|
91
|
+
export declare function forEach<T extends object>(target: T, callback: (key: PropertyKey, value: unknown, target: T) => void): void;
|
|
92
|
+
/**
|
|
93
|
+
* Handle nested values during finalization
|
|
94
|
+
*/
|
|
95
|
+
export declare function handleValue(target: unknown, handledSet: WeakSet<object>): void;
|
package/dist/types/draft.d.ts
CHANGED
|
@@ -32,11 +32,11 @@ export declare function isDraftModified<T extends JSONObject>(ctx: DraftContext<
|
|
|
32
32
|
/**
|
|
33
33
|
* Applies a single "set" operation to the draft with copy-on-write.
|
|
34
34
|
*/
|
|
35
|
-
export declare function draftSet<T extends JSONObject>(ctx: DraftContext<T>, path: Path, key: string, value: JSONValue): void;
|
|
35
|
+
export declare function draftSet<T extends JSONObject>(ctx: DraftContext<T>, path: Path, key: string | number, value: JSONValue): void;
|
|
36
36
|
/**
|
|
37
37
|
* Applies a single "delete" operation to the draft with copy-on-write.
|
|
38
38
|
*/
|
|
39
|
-
export declare function draftDelete<T extends JSONObject>(ctx: DraftContext<T>, path: Path, key: string): void;
|
|
39
|
+
export declare function draftDelete<T extends JSONObject>(ctx: DraftContext<T>, path: Path, key: string | number): void;
|
|
40
40
|
/**
|
|
41
41
|
* Applies a single "splice" operation to the draft with copy-on-write.
|
|
42
42
|
*/
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type { CheckpointKey, CheckpointRecord } from './checkpoints';
|
|
2
|
+
export * from './createOps';
|
|
2
3
|
export { createStateSyncLog, type StateSyncLogController, type StateSyncLogOptions, } from './createStateSyncLog';
|
|
3
4
|
export type { JSONObject, JSONValue, Path } from './json';
|
|
4
5
|
export { type ApplyOpsOptions, applyOps, type Op, type ValidateFn } from './operations';
|
package/dist/types/json.d.ts
CHANGED
|
@@ -6,12 +6,12 @@ import { JSONObject, JSONValue, Path } from './json';
|
|
|
6
6
|
export type Op = {
|
|
7
7
|
kind: "set";
|
|
8
8
|
path: Path;
|
|
9
|
-
key: string;
|
|
9
|
+
key: string | number;
|
|
10
10
|
value: JSONValue;
|
|
11
11
|
} | {
|
|
12
12
|
kind: "delete";
|
|
13
13
|
path: Path;
|
|
14
|
-
key: string;
|
|
14
|
+
key: string | number;
|
|
15
15
|
} | {
|
|
16
16
|
kind: "splice";
|
|
17
17
|
path: Path;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -21,3 +21,8 @@ export declare function deepClone<T>(value: T): T;
|
|
|
21
21
|
* Creates a lazy memoized getter.
|
|
22
22
|
*/
|
|
23
23
|
export declare function lazy<T>(fn: () => T): () => T;
|
|
24
|
+
/**
|
|
25
|
+
* Checks if a string is a valid non-negative integer array index.
|
|
26
|
+
* Returns the numeric value if valid, or null if invalid.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseArrayIndex(key: string): number | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "state-sync-log",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Validated Replicated State Machine built on Yjs. Combine CRDT offline capabilities with strict business logic validation, state reconciliation, and audit trails.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"state-sync",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for createOps.
|
|
3
|
+
* Simplified from original source - removed dataTypes (mark feature).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Symbol to identify proxy drafts - accessible for 3rd party
|
|
7
|
+
export const PROXY_DRAFT = Symbol.for("__CREATEOPS_PROXY_DRAFT__")
|
|
8
|
+
|
|
9
|
+
// Symbol iterator
|
|
10
|
+
export const iteratorSymbol: typeof Symbol.iterator = Symbol.iterator
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createOps - Main API for generating operations from mutable-style mutations.
|
|
3
|
+
*
|
|
4
|
+
* Forked from mutative (https://github.com/unadlib/mutative)
|
|
5
|
+
* MIT License
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { current } from "./current"
|
|
9
|
+
import { draftify } from "./draftify"
|
|
10
|
+
import type { CreateOpsResult, Draft } from "./interface"
|
|
11
|
+
import { getProxyDraft, isDraft, isDraftable, isEqual, revokeProxy } from "./utils"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create operations from mutable-style mutations.
|
|
15
|
+
*
|
|
16
|
+
* @param base - The base state (will not be mutated)
|
|
17
|
+
* @param mutate - A function that mutates the draft
|
|
18
|
+
* @returns An object containing the next state and the operations performed
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const state = { list: [{ text: 'Learn', done: false }] };
|
|
23
|
+
*
|
|
24
|
+
* const { nextState, ops } = createOps(state, (draft) => {
|
|
25
|
+
* draft.list[0].done = true;
|
|
26
|
+
* draft.list.push({ text: 'Practice', done: false });
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // ops contains the operations that were performed:
|
|
30
|
+
* // [
|
|
31
|
+
* // { kind: 'set', path: ['list', 0], key: 'done', value: true },
|
|
32
|
+
* // { kind: 'splice', path: ['list'], index: 1, deleteCount: 0, inserts: [{ text: 'Practice', done: false }] }
|
|
33
|
+
* // ]
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function createOps<T extends object>(
|
|
37
|
+
base: T,
|
|
38
|
+
mutate: (draft: Draft<T>) => void
|
|
39
|
+
): CreateOpsResult<T> {
|
|
40
|
+
// Handle case where base is already a draft
|
|
41
|
+
const state = isDraft(base) ? current(base as Draft<T>) : base
|
|
42
|
+
|
|
43
|
+
// Validate that state is draftable
|
|
44
|
+
if (!isDraftable(state)) {
|
|
45
|
+
throw new Error(`createOps() only supports plain objects and arrays.`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create draft
|
|
49
|
+
const [draft, finalize] = draftify(state)
|
|
50
|
+
|
|
51
|
+
// Run mutation
|
|
52
|
+
let result: unknown
|
|
53
|
+
try {
|
|
54
|
+
result = mutate(draft as Draft<T>)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
revokeProxy(getProxyDraft(draft))
|
|
57
|
+
throw error
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle return value
|
|
61
|
+
const proxyDraft = getProxyDraft(draft)!
|
|
62
|
+
|
|
63
|
+
// Check for invalid return values
|
|
64
|
+
if (result !== undefined && !isDraft(result)) {
|
|
65
|
+
if (!isEqual(result, draft) && proxyDraft.operated) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Either the value is returned as a new non-draft value, or only the draft is modified without returning any value.`
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
// User returned a new value - use it as the next state
|
|
71
|
+
// Note: We don't support rawReturn, so returning a non-draft value replaces the state
|
|
72
|
+
// but we can't generate meaningful ops for this case
|
|
73
|
+
if (result !== undefined) {
|
|
74
|
+
const [, ops] = finalize([])
|
|
75
|
+
return { nextState: result as T, ops }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Standard flow - finalize the draft
|
|
80
|
+
if (result === draft || result === undefined) {
|
|
81
|
+
const [nextState, ops] = finalize([])
|
|
82
|
+
return { nextState, ops }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Returned a different draft (child)
|
|
86
|
+
const returnedProxyDraft = getProxyDraft(result)
|
|
87
|
+
if (returnedProxyDraft) {
|
|
88
|
+
if (returnedProxyDraft.operated) {
|
|
89
|
+
throw new Error(`Cannot return a modified child draft.`)
|
|
90
|
+
}
|
|
91
|
+
const [, ops] = finalize([])
|
|
92
|
+
return { nextState: current(result as object) as T, ops }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const [nextState, ops] = finalize([])
|
|
96
|
+
return { nextState, ops }
|
|
97
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the current value from a draft (snapshot).
|
|
3
|
+
* Adapted from mutative - simplified for plain objects/arrays only.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Draft } from "./interface"
|
|
7
|
+
import {
|
|
8
|
+
forEach,
|
|
9
|
+
get,
|
|
10
|
+
getProxyDraft,
|
|
11
|
+
isDraft,
|
|
12
|
+
isDraftable,
|
|
13
|
+
isEqual,
|
|
14
|
+
set,
|
|
15
|
+
shallowCopy,
|
|
16
|
+
} from "./utils"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get current state from a value (handles nested drafts)
|
|
20
|
+
*/
|
|
21
|
+
function getCurrent<T>(target: T): T {
|
|
22
|
+
const proxyDraft = getProxyDraft(target)
|
|
23
|
+
|
|
24
|
+
// Not draftable - return as-is
|
|
25
|
+
if (!isDraftable(target)) return target
|
|
26
|
+
|
|
27
|
+
// Draft that hasn't been modified - return original
|
|
28
|
+
if (proxyDraft && !proxyDraft.operated) {
|
|
29
|
+
return proxyDraft.original as T
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let currentValue: T | undefined
|
|
33
|
+
|
|
34
|
+
function ensureShallowCopyLocal() {
|
|
35
|
+
currentValue = shallowCopy(target)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (proxyDraft) {
|
|
39
|
+
// It's a draft - create a shallow copy eagerly
|
|
40
|
+
proxyDraft.finalized = true
|
|
41
|
+
try {
|
|
42
|
+
ensureShallowCopyLocal()
|
|
43
|
+
} finally {
|
|
44
|
+
proxyDraft.finalized = false
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
// Not a draft - use target directly, copy lazily if needed
|
|
48
|
+
currentValue = target
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Recursively process children
|
|
52
|
+
forEach(currentValue as object, (key, value) => {
|
|
53
|
+
if (proxyDraft && isEqual(get(proxyDraft.original as object, key), value)) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
const newValue = getCurrent(value)
|
|
57
|
+
if (newValue !== value) {
|
|
58
|
+
if (currentValue === target) {
|
|
59
|
+
ensureShallowCopyLocal()
|
|
60
|
+
}
|
|
61
|
+
set(currentValue as object, key, newValue)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return currentValue as T
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `current(draft)` to get current state in the draft mutation function.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const { nextState, ops } = createOps(baseState, (draft) => {
|
|
74
|
+
* draft.foo.bar = 'new value';
|
|
75
|
+
* console.log(current(draft.foo)); // { bar: 'new value' }
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function current<T extends object>(target: Draft<T>): T
|
|
80
|
+
export function current<T extends object>(target: T | Draft<T>): T {
|
|
81
|
+
if (!isDraft(target)) {
|
|
82
|
+
throw new Error(`current() is only used for Draft, parameter: ${target}`)
|
|
83
|
+
}
|
|
84
|
+
return getCurrent(target) as T
|
|
85
|
+
}
|