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,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createOps interface types.
|
|
3
|
+
* Simplified from mutative - removed patches, autoFreeze, strict mode, mark, Map/Set support.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { JSONPrimitive, JSONValue, Path } from "../json"
|
|
7
|
+
import type { Op } from "../operations"
|
|
8
|
+
|
|
9
|
+
export type { Op, Path, JSONValue }
|
|
10
|
+
|
|
11
|
+
export enum DraftType {
|
|
12
|
+
Object = 0,
|
|
13
|
+
Array = 1,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Finalities - shared state for the draft tree
|
|
18
|
+
*/
|
|
19
|
+
export interface Finalities {
|
|
20
|
+
/** Finalization callbacks (for unwrapping child drafts) */
|
|
21
|
+
draft: (() => void)[]
|
|
22
|
+
/** Revoke functions for all proxies */
|
|
23
|
+
revoke: (() => void)[]
|
|
24
|
+
/** Set of handled objects (for cycle detection) */
|
|
25
|
+
handledSet: WeakSet<object>
|
|
26
|
+
/** Cache of created drafts */
|
|
27
|
+
draftsCache: WeakSet<object>
|
|
28
|
+
/** List of operations performed in this draft session (eager logging) */
|
|
29
|
+
ops: Op[]
|
|
30
|
+
/** Root draft of the tree (set when creating the root draft) */
|
|
31
|
+
rootDraft: ProxyDraft | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Internal proxy draft state
|
|
36
|
+
*/
|
|
37
|
+
export interface ProxyDraft<T = any> {
|
|
38
|
+
/** Type of the draft (Object or Array) */
|
|
39
|
+
type: DraftType
|
|
40
|
+
/** Whether this draft has been mutated */
|
|
41
|
+
operated?: boolean
|
|
42
|
+
/** Whether finalization has been completed */
|
|
43
|
+
finalized: boolean
|
|
44
|
+
/** The original (unmodified) value */
|
|
45
|
+
original: T
|
|
46
|
+
/** The shallow copy (created on first mutation) */
|
|
47
|
+
copy: T | null
|
|
48
|
+
/** The proxy instance */
|
|
49
|
+
proxy: T | null
|
|
50
|
+
/** Finalities container (shared across draft tree) */
|
|
51
|
+
finalities: Finalities
|
|
52
|
+
/** Parent draft (for path tracking) */
|
|
53
|
+
parent?: ProxyDraft | null
|
|
54
|
+
/** Key in parent */
|
|
55
|
+
key?: string | number
|
|
56
|
+
/** Track which keys have been assigned (key -> true=assigned, false=deleted) */
|
|
57
|
+
assignedMap?: Map<PropertyKey, boolean>
|
|
58
|
+
/** Count of positions this draft exists at (for aliasing optimization) */
|
|
59
|
+
aliasCount: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Result of createOps
|
|
64
|
+
*/
|
|
65
|
+
export interface CreateOpsResult<T> {
|
|
66
|
+
/** The new immutable state */
|
|
67
|
+
nextState: T
|
|
68
|
+
/** The operations that were performed */
|
|
69
|
+
ops: Op[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Type Utilities
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
/** Primitive types that don't need drafting */
|
|
77
|
+
type Primitive = JSONPrimitive
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Draft type - makes all properties mutable for editing
|
|
81
|
+
*/
|
|
82
|
+
export type Draft<T> = T extends Primitive
|
|
83
|
+
? T
|
|
84
|
+
: T extends object
|
|
85
|
+
? { -readonly [K in keyof T]: Draft<T[K]> }
|
|
86
|
+
: T
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Immutable type - makes all properties readonly
|
|
90
|
+
*/
|
|
91
|
+
export type Immutable<T> = T extends Primitive
|
|
92
|
+
? T
|
|
93
|
+
: T extends object
|
|
94
|
+
? { readonly [K in keyof T]: Immutable<T[K]> }
|
|
95
|
+
: T
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the original value from a draft.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getProxyDraft } from "./utils"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `original(draft)` to get original state in the draft mutation function.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const { nextState, ops } = createOps(baseState, (draft) => {
|
|
13
|
+
* draft.foo.bar = 'new value';
|
|
14
|
+
* console.log(original(draft.foo)); // { bar: 'old value' }
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function original<T>(target: T): T {
|
|
19
|
+
const proxyDraft = getProxyDraft(target)
|
|
20
|
+
if (!proxyDraft) {
|
|
21
|
+
throw new Error(`original() is only used for a draft, parameter: ${target}`)
|
|
22
|
+
}
|
|
23
|
+
return proxyDraft.original as T
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eager op generation - ops are pushed immediately when mutations happen.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the pushOp function that records operations at mutation time,
|
|
5
|
+
* rather than diffing at finalization time.
|
|
6
|
+
*
|
|
7
|
+
* When a draft exists at multiple positions (aliasing), ops are emitted for ALL positions
|
|
8
|
+
* to ensure consistency when applied.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { failure } from "../error"
|
|
12
|
+
import type { Op, ProxyDraft } from "./interface"
|
|
13
|
+
import { getAllPathsForDraft } from "./utils"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Push an operation to the ops log.
|
|
17
|
+
* Values should already be cloned by the caller to avoid aliasing issues.
|
|
18
|
+
*
|
|
19
|
+
* When the target draft exists at multiple positions (due to aliasing),
|
|
20
|
+
* this function emits ops for all positions to maintain consistency.
|
|
21
|
+
*/
|
|
22
|
+
export function pushOp(proxyDraft: ProxyDraft, op: Op): void {
|
|
23
|
+
const rootDraft = proxyDraft.finalities.rootDraft
|
|
24
|
+
if (!rootDraft) {
|
|
25
|
+
throw failure("rootDraft is not set - cannot emit op")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Fast path: no aliasing, just emit the single op
|
|
29
|
+
if (proxyDraft.aliasCount <= 1) {
|
|
30
|
+
proxyDraft.finalities.ops.push(op)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Slow path: draft exists at multiple positions, find all paths
|
|
35
|
+
const allPaths = getAllPathsForDraft(rootDraft, proxyDraft)
|
|
36
|
+
|
|
37
|
+
// Emit the op for each path where this draft exists
|
|
38
|
+
for (const path of allPaths) {
|
|
39
|
+
const adjustedOp = { ...op, path }
|
|
40
|
+
proxyDraft.finalities.ops.push(adjustedOp)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for set-like array operations.
|
|
3
|
+
* Uses eager op logging - ops are pushed immediately when mutations happen.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { JSONValue } from "../json"
|
|
7
|
+
import { deepClone, deepEqual } from "../utils"
|
|
8
|
+
import { pushOp } from "./pushOp"
|
|
9
|
+
import { ensureShallowCopy, getPathOrThrow, getProxyDraft, markChanged } from "./utils"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Add a value to an array if it doesn't already exist (set semantics).
|
|
13
|
+
* Generates an `addToSet` operation.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* createOps(state, (draft) => {
|
|
18
|
+
* addToSet(draft.tags, 'newTag'); // Only adds if not present
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function addToSet<T extends JSONValue>(draft: T[], value: T): void {
|
|
23
|
+
const proxyDraft = getProxyDraft(draft)
|
|
24
|
+
if (!proxyDraft) {
|
|
25
|
+
throw new Error(`addToSet() can only be used on draft arrays`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Mark as changed
|
|
29
|
+
ensureShallowCopy(proxyDraft)
|
|
30
|
+
markChanged(proxyDraft)
|
|
31
|
+
|
|
32
|
+
// Check if value already exists
|
|
33
|
+
const arr = proxyDraft.copy as T[]
|
|
34
|
+
if (arr.some((item) => item === value || deepEqual(item, value))) {
|
|
35
|
+
// Value already exists - no op needed
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Add the value
|
|
40
|
+
arr.push(value)
|
|
41
|
+
|
|
42
|
+
// Eager op logging
|
|
43
|
+
pushOp(proxyDraft, {
|
|
44
|
+
kind: "addToSet",
|
|
45
|
+
path: getPathOrThrow(proxyDraft),
|
|
46
|
+
value: deepClone(value) as JSONValue,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Remove a value from an array (set semantics).
|
|
52
|
+
* Generates a `deleteFromSet` operation.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* createOps(state, (draft) => {
|
|
57
|
+
* deleteFromSet(draft.tags, 'oldTag'); // Removes all matching items
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function deleteFromSet<T extends JSONValue>(draft: T[], value: T): void {
|
|
62
|
+
const proxyDraft = getProxyDraft(draft)
|
|
63
|
+
if (!proxyDraft) {
|
|
64
|
+
throw new Error(`deleteFromSet() can only be used on draft arrays`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Mark as changed
|
|
68
|
+
ensureShallowCopy(proxyDraft)
|
|
69
|
+
markChanged(proxyDraft)
|
|
70
|
+
|
|
71
|
+
// Check if value exists
|
|
72
|
+
const arr = proxyDraft.copy as T[]
|
|
73
|
+
const hasValue = arr.some((item) => item === value || deepEqual(item, value))
|
|
74
|
+
|
|
75
|
+
if (!hasValue) {
|
|
76
|
+
// Value doesn't exist - no op needed
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Remove all matching items (by value equality)
|
|
81
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
82
|
+
if (arr[i] === value || deepEqual(arr[i], value)) {
|
|
83
|
+
arr.splice(i, 1)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Eager op logging
|
|
88
|
+
pushOp(proxyDraft, {
|
|
89
|
+
kind: "deleteFromSet",
|
|
90
|
+
path: getPathOrThrow(proxyDraft),
|
|
91
|
+
value: deepClone(value) as JSONValue,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for proxy drafts.
|
|
3
|
+
* Adapted from mutative - removed Map/Set support, mark, deepFreeze.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { failure } from "../error"
|
|
7
|
+
import { PROXY_DRAFT } from "./constant"
|
|
8
|
+
import { DraftType, type ProxyDraft } from "./interface"
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Core Draft Utilities
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the latest value (copy if exists, otherwise original)
|
|
16
|
+
*/
|
|
17
|
+
export function latest<T>(proxyDraft: ProxyDraft<T>): T {
|
|
18
|
+
return (proxyDraft.copy ?? proxyDraft.original) as T
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if the value is a draft
|
|
23
|
+
*/
|
|
24
|
+
export function isDraft(target: unknown): boolean {
|
|
25
|
+
return !!getProxyDraft(target)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the ProxyDraft from a draft value
|
|
30
|
+
*/
|
|
31
|
+
export function getProxyDraft<T>(value: unknown): ProxyDraft<T> | null {
|
|
32
|
+
if (typeof value !== "object" || value === null) return null
|
|
33
|
+
return (value as { [PROXY_DRAFT]?: ProxyDraft<T> })[PROXY_DRAFT] ?? null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the actual value from a draft (copy or original)
|
|
38
|
+
*/
|
|
39
|
+
export function getValue<T extends object>(value: T): T {
|
|
40
|
+
const proxyDraft = getProxyDraft(value)
|
|
41
|
+
return proxyDraft ? ((proxyDraft.copy ?? proxyDraft.original) as T) : value
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a value is draftable (plain object or array)
|
|
46
|
+
* We only support plain objects and arrays - no Map, Set, Date, etc.
|
|
47
|
+
*/
|
|
48
|
+
export function isDraftable(value: unknown): value is object {
|
|
49
|
+
if (value === null || typeof value !== "object") return false
|
|
50
|
+
return Array.isArray(value) || Object.getPrototypeOf(value) === Object.prototype
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the draft type
|
|
55
|
+
*/
|
|
56
|
+
export function getType(target: unknown): DraftType {
|
|
57
|
+
return Array.isArray(target) ? DraftType.Array : DraftType.Object
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get a value by key
|
|
62
|
+
*/
|
|
63
|
+
export function get(target: object, key: PropertyKey): unknown {
|
|
64
|
+
return (target as Record<PropertyKey, unknown>)[key]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set a value by key
|
|
69
|
+
*/
|
|
70
|
+
export function set(target: object, key: PropertyKey, value: unknown): void {
|
|
71
|
+
;(target as Record<PropertyKey, unknown>)[key] = value
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if a key exists (own property)
|
|
76
|
+
*/
|
|
77
|
+
export function has(target: object, key: PropertyKey): boolean {
|
|
78
|
+
return Object.hasOwn(target, key as string)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Peek at a value (through drafts)
|
|
83
|
+
*/
|
|
84
|
+
export function peek(target: object, key: PropertyKey): unknown {
|
|
85
|
+
const state = getProxyDraft(target)
|
|
86
|
+
const source = state ? latest(state) : target
|
|
87
|
+
return (source as Record<PropertyKey, unknown>)[key]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* SameValue comparison (handles -0 and NaN)
|
|
92
|
+
*/
|
|
93
|
+
export function isEqual(x: unknown, y: unknown): boolean {
|
|
94
|
+
if (x === y) {
|
|
95
|
+
return x !== 0 || 1 / (x as number) === 1 / (y as number)
|
|
96
|
+
}
|
|
97
|
+
// biome-ignore lint/suspicious/noSelfCompare: NaN check pattern (NaN !== NaN)
|
|
98
|
+
return x !== x && y !== y
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Revoke all proxies in a draft tree
|
|
103
|
+
*/
|
|
104
|
+
export function revokeProxy(proxyDraft: ProxyDraft | null): void {
|
|
105
|
+
if (!proxyDraft) return
|
|
106
|
+
while (proxyDraft.finalities.revoke.length > 0) {
|
|
107
|
+
const revoke = proxyDraft.finalities.revoke.pop()!
|
|
108
|
+
revoke()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the path from root to this draft
|
|
114
|
+
*/
|
|
115
|
+
export function getPath(
|
|
116
|
+
target: ProxyDraft,
|
|
117
|
+
path: (string | number)[] = []
|
|
118
|
+
): (string | number)[] | null {
|
|
119
|
+
if (Object.hasOwn(target, "key") && target.key !== undefined) {
|
|
120
|
+
// Check if the parent still has this draft at this key
|
|
121
|
+
const parentCopy = target.parent?.copy
|
|
122
|
+
if (parentCopy) {
|
|
123
|
+
const proxyDraft = getProxyDraft(get(parentCopy as object, target.key))
|
|
124
|
+
if (proxyDraft !== null && proxyDraft.original !== target.original) {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
path.push(target.key)
|
|
129
|
+
}
|
|
130
|
+
if (target.parent) {
|
|
131
|
+
return getPath(target.parent, path)
|
|
132
|
+
}
|
|
133
|
+
// target is root draft
|
|
134
|
+
path.reverse()
|
|
135
|
+
return path
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get the path from root to this draft, or throw if not available
|
|
140
|
+
*/
|
|
141
|
+
export function getPathOrThrow(target: ProxyDraft): (string | number)[] {
|
|
142
|
+
const path = getPath(target)
|
|
143
|
+
if (!path) {
|
|
144
|
+
throw failure("Cannot determine path for operation")
|
|
145
|
+
}
|
|
146
|
+
return path
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Find all paths to a given draft by searching the tree.
|
|
151
|
+
* This handles aliasing where the same draft exists at multiple positions.
|
|
152
|
+
*/
|
|
153
|
+
export function getAllPathsForDraft(
|
|
154
|
+
rootDraft: ProxyDraft,
|
|
155
|
+
targetDraft: ProxyDraft
|
|
156
|
+
): (string | number)[][] {
|
|
157
|
+
const result: (string | number)[][] = []
|
|
158
|
+
|
|
159
|
+
function search(current: ProxyDraft, currentPath: (string | number)[]): void {
|
|
160
|
+
if (current === targetDraft) {
|
|
161
|
+
result.push([...currentPath])
|
|
162
|
+
// Don't return - the same draft could be nested inside itself (unlikely but possible)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const source = latest(current) as Record<string | number, unknown>
|
|
166
|
+
if (!source || typeof source !== "object") return
|
|
167
|
+
|
|
168
|
+
const keys: (string | number)[] = Array.isArray(source)
|
|
169
|
+
? Array.from({ length: source.length }, (_, i) => i)
|
|
170
|
+
: Object.keys(source)
|
|
171
|
+
|
|
172
|
+
for (const key of keys) {
|
|
173
|
+
const value = source[key]
|
|
174
|
+
const childDraft = getProxyDraft(value)
|
|
175
|
+
if (childDraft) {
|
|
176
|
+
search(childDraft, [...currentPath, key])
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
search(rootDraft, [])
|
|
182
|
+
return result
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get a property descriptor from the prototype chain
|
|
187
|
+
*/
|
|
188
|
+
export function getDescriptor(target: object, key: PropertyKey): PropertyDescriptor | undefined {
|
|
189
|
+
if (key in target) {
|
|
190
|
+
let prototype = Reflect.getPrototypeOf(target)
|
|
191
|
+
while (prototype) {
|
|
192
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(prototype, key)
|
|
193
|
+
if (descriptor) return descriptor
|
|
194
|
+
prototype = Reflect.getPrototypeOf(prototype)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// Copy Utilities
|
|
202
|
+
// ============================================================================
|
|
203
|
+
|
|
204
|
+
const propIsEnum = Object.prototype.propertyIsEnumerable
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create a shallow copy of an object or array
|
|
208
|
+
*/
|
|
209
|
+
export function shallowCopy<T>(original: T): T {
|
|
210
|
+
if (Array.isArray(original)) {
|
|
211
|
+
return Array.prototype.concat.call(original) as T
|
|
212
|
+
}
|
|
213
|
+
// Plain object - use optimized copy
|
|
214
|
+
const copy: Record<string | symbol, unknown> = {}
|
|
215
|
+
for (const key of Object.keys(original as object)) {
|
|
216
|
+
copy[key] = (original as Record<string, unknown>)[key]
|
|
217
|
+
}
|
|
218
|
+
for (const key of Object.getOwnPropertySymbols(original as object)) {
|
|
219
|
+
if (propIsEnum.call(original, key)) {
|
|
220
|
+
copy[key] = (original as Record<symbol, unknown>)[key]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return copy as T
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Ensure a draft has a shallow copy
|
|
228
|
+
*/
|
|
229
|
+
export function ensureShallowCopy(target: ProxyDraft): void {
|
|
230
|
+
if (target.copy) return
|
|
231
|
+
target.copy = shallowCopy(target.original)
|
|
232
|
+
target.assignedMap = target.assignedMap ?? new Map()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Deep clone a value, unwrapping any drafts
|
|
237
|
+
*/
|
|
238
|
+
export function deepClone<T>(target: T): T {
|
|
239
|
+
if (!isDraftable(target)) {
|
|
240
|
+
return isDraft(target) ? (getValue(target as object) as T) : target
|
|
241
|
+
}
|
|
242
|
+
if (Array.isArray(target)) {
|
|
243
|
+
return target.map(deepClone) as T
|
|
244
|
+
}
|
|
245
|
+
const copy: Record<string, unknown> = {}
|
|
246
|
+
for (const key in target) {
|
|
247
|
+
if (has(target, key)) {
|
|
248
|
+
copy[key] = deepClone((target as Record<string, unknown>)[key])
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return copy as T
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Clone if the value is a draft, otherwise return as-is
|
|
256
|
+
*/
|
|
257
|
+
export function cloneIfNeeded<T>(target: T): T {
|
|
258
|
+
return isDraft(target) ? deepClone(target) : target
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Draft State Utilities
|
|
263
|
+
// ============================================================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Mark a draft as changed (operated)
|
|
267
|
+
*/
|
|
268
|
+
export function markChanged(target: ProxyDraft): void {
|
|
269
|
+
if (!target.operated) {
|
|
270
|
+
target.operated = true
|
|
271
|
+
if (target.parent) {
|
|
272
|
+
markChanged(target.parent)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Iterate over object/array entries
|
|
279
|
+
*/
|
|
280
|
+
export function forEach<T extends object>(
|
|
281
|
+
target: T,
|
|
282
|
+
callback: (key: PropertyKey, value: unknown, target: T) => void
|
|
283
|
+
): void {
|
|
284
|
+
if (Array.isArray(target)) {
|
|
285
|
+
for (let i = 0; i < target.length; i++) {
|
|
286
|
+
callback(i, target[i], target)
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
for (const key of Reflect.ownKeys(target)) {
|
|
290
|
+
callback(key, (target as Record<PropertyKey, unknown>)[key], target)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// Finalization Utilities
|
|
297
|
+
// ============================================================================
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Handle nested values during finalization
|
|
301
|
+
*/
|
|
302
|
+
export function handleValue(target: unknown, handledSet: WeakSet<object>): void {
|
|
303
|
+
if (
|
|
304
|
+
!isDraftable(target) ||
|
|
305
|
+
isDraft(target) ||
|
|
306
|
+
handledSet.has(target as object) ||
|
|
307
|
+
Object.isFrozen(target)
|
|
308
|
+
) {
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
handledSet.add(target as object)
|
|
313
|
+
|
|
314
|
+
forEach(target as object, (key, value) => {
|
|
315
|
+
if (isDraft(value)) {
|
|
316
|
+
const proxyDraft = getProxyDraft(value)!
|
|
317
|
+
ensureShallowCopy(proxyDraft)
|
|
318
|
+
const updatedValue =
|
|
319
|
+
proxyDraft.assignedMap?.size || proxyDraft.operated ? proxyDraft.copy : proxyDraft.original
|
|
320
|
+
set(target as object, key, updatedValue)
|
|
321
|
+
} else {
|
|
322
|
+
handleValue(value, handledSet)
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
}
|