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
package/src/draft.ts
CHANGED
|
@@ -1,288 +1,306 @@
|
|
|
1
|
-
import { failure } from "./error"
|
|
2
|
-
import type { JSONObject, JSONRecord, JSONValue, Path } from "./json"
|
|
3
|
-
import type { Op, ValidateFn } from "./operations"
|
|
4
|
-
import { TxRecord } from "./TxRecord"
|
|
5
|
-
import { deepEqual, isObject } from "./utils"
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* A draft context for copy-on-write immutable updates.
|
|
9
|
-
*
|
|
10
|
-
* This provides Immer/Mutative-like semantics without the proxy overhead:
|
|
11
|
-
* - The original base state is never mutated
|
|
12
|
-
* - Objects are cloned lazily on first mutation (copy-on-write)
|
|
13
|
-
* - Once an object is cloned ("owned"), it can be mutated directly
|
|
14
|
-
* - If no changes are made, the original reference is preserved (structural sharing)
|
|
15
|
-
*/
|
|
16
|
-
export interface DraftContext<T extends JSONObject> {
|
|
17
|
-
/** The current root (may be the original base or a cloned version) */
|
|
18
|
-
root: T
|
|
19
|
-
/** The original base state (never mutated) */
|
|
20
|
-
base: T
|
|
21
|
-
/** Set of objects that have been cloned and are safe to mutate */
|
|
22
|
-
ownedObjects: WeakSet<object>
|
|
23
|
-
/** Optimization: fast check if root is already owned */
|
|
24
|
-
isRootOwned: boolean
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Creates a new draft context from a base state.
|
|
29
|
-
* The base state will never be mutated.
|
|
30
|
-
*/
|
|
31
|
-
export function createDraft<T extends JSONObject>(base: T): DraftContext<T> {
|
|
32
|
-
return {
|
|
33
|
-
root: base,
|
|
34
|
-
base,
|
|
35
|
-
ownedObjects: new Set(),
|
|
36
|
-
isRootOwned: false,
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Checks if the draft was modified (root !== base).
|
|
42
|
-
*/
|
|
43
|
-
export function isDraftModified<T extends JSONObject>(ctx: DraftContext<T>): boolean {
|
|
44
|
-
return ctx.root !== ctx.base
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function shallowClone<T extends object>(obj: T): T {
|
|
48
|
-
if (Array.isArray(obj)) {
|
|
49
|
-
return obj.slice() as unknown as T
|
|
50
|
-
}
|
|
51
|
-
const clone = {} as T
|
|
52
|
-
const keys = Object.keys(obj)
|
|
53
|
-
for (let i = 0; i < keys.length; i++) {
|
|
54
|
-
const key = keys[i]
|
|
55
|
-
;(clone as any)[key] = (obj as any)[key]
|
|
56
|
-
}
|
|
57
|
-
return clone
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Ensures an object is owned (cloned if necessary) and returns the owned version.
|
|
62
|
-
* Also updates the parent to point to the cloned child.
|
|
63
|
-
*/
|
|
64
|
-
function ensureOwned<T extends JSONObject>(
|
|
65
|
-
ctx: DraftContext<T>,
|
|
66
|
-
parent: JSONObject,
|
|
67
|
-
key: string | number,
|
|
68
|
-
child: JSONRecord | JSONValue[]
|
|
69
|
-
): JSONRecord | JSONValue[] {
|
|
70
|
-
if (ctx.ownedObjects.has(child)) {
|
|
71
|
-
return child
|
|
72
|
-
}
|
|
73
|
-
const cloned = shallowClone(child)
|
|
74
|
-
;(parent as any)[key] = cloned
|
|
75
|
-
ctx.ownedObjects.add(cloned)
|
|
76
|
-
return cloned
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Ensures all objects along the path are owned (cloned if necessary).
|
|
81
|
-
* Returns the container at the end of the path.
|
|
82
|
-
* Throws if the path is invalid.
|
|
83
|
-
*/
|
|
84
|
-
function ensureOwnedPath<T extends JSONObject>(
|
|
85
|
-
ctx: DraftContext<T>,
|
|
86
|
-
path: Path
|
|
87
|
-
): JSONRecord | JSONValue[] {
|
|
88
|
-
// Ensure root is owned first
|
|
89
|
-
if (!ctx.isRootOwned) {
|
|
90
|
-
ctx.root = shallowClone(ctx.root as unknown as object) as T
|
|
91
|
-
ctx.ownedObjects.add(ctx.root)
|
|
92
|
-
ctx.isRootOwned = true
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (path.length === 0) {
|
|
96
|
-
return ctx.root
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let current: JSONRecord | JSONValue[] = ctx.root
|
|
100
|
-
|
|
101
|
-
for (let i = 0; i < path.length; i++) {
|
|
102
|
-
const segment = path[i]
|
|
103
|
-
const isArrayIndex = typeof segment === "number"
|
|
104
|
-
|
|
105
|
-
// Validate container type
|
|
106
|
-
if (isArrayIndex) {
|
|
107
|
-
if (!Array.isArray(current)) {
|
|
108
|
-
failure(`Expected array at path segment ${segment}`)
|
|
109
|
-
}
|
|
110
|
-
if (segment < 0 || segment >= current.length) {
|
|
111
|
-
failure(`Index ${segment} out of bounds`)
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
if (!isObject(current) || Array.isArray(current)) {
|
|
115
|
-
failure(`Expected object at path segment "${segment}"`)
|
|
116
|
-
}
|
|
117
|
-
if (!(segment in current)) {
|
|
118
|
-
failure(`Property "${segment}" does not exist`)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const child: JSONValue = (current as any)[segment]
|
|
123
|
-
|
|
124
|
-
// Validate child is traversable
|
|
125
|
-
if (child === null || typeof child !== "object") {
|
|
126
|
-
failure(`Cannot traverse through primitive at path segment ${segment}`)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Ensure child is owned and continue
|
|
130
|
-
current = ensureOwned(ctx, current, segment, child as JSONRecord | JSONValue[])
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return current
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Applies a single "set" operation to the draft with copy-on-write.
|
|
138
|
-
*/
|
|
139
|
-
export function draftSet<T extends JSONObject>(
|
|
140
|
-
ctx: DraftContext<T>,
|
|
141
|
-
path: Path,
|
|
142
|
-
key: string,
|
|
143
|
-
value: JSONValue
|
|
144
|
-
): void {
|
|
145
|
-
const container = ensureOwnedPath(ctx, path)
|
|
146
|
-
if (
|
|
147
|
-
failure("set requires object container")
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
ctx
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
1
|
+
import { failure } from "./error"
|
|
2
|
+
import type { JSONObject, JSONRecord, JSONValue, Path } from "./json"
|
|
3
|
+
import type { Op, ValidateFn } from "./operations"
|
|
4
|
+
import { TxRecord } from "./TxRecord"
|
|
5
|
+
import { deepEqual, isObject, parseArrayIndex } from "./utils"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A draft context for copy-on-write immutable updates.
|
|
9
|
+
*
|
|
10
|
+
* This provides Immer/Mutative-like semantics without the proxy overhead:
|
|
11
|
+
* - The original base state is never mutated
|
|
12
|
+
* - Objects are cloned lazily on first mutation (copy-on-write)
|
|
13
|
+
* - Once an object is cloned ("owned"), it can be mutated directly
|
|
14
|
+
* - If no changes are made, the original reference is preserved (structural sharing)
|
|
15
|
+
*/
|
|
16
|
+
export interface DraftContext<T extends JSONObject> {
|
|
17
|
+
/** The current root (may be the original base or a cloned version) */
|
|
18
|
+
root: T
|
|
19
|
+
/** The original base state (never mutated) */
|
|
20
|
+
base: T
|
|
21
|
+
/** Set of objects that have been cloned and are safe to mutate */
|
|
22
|
+
ownedObjects: WeakSet<object>
|
|
23
|
+
/** Optimization: fast check if root is already owned */
|
|
24
|
+
isRootOwned: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a new draft context from a base state.
|
|
29
|
+
* The base state will never be mutated.
|
|
30
|
+
*/
|
|
31
|
+
export function createDraft<T extends JSONObject>(base: T): DraftContext<T> {
|
|
32
|
+
return {
|
|
33
|
+
root: base,
|
|
34
|
+
base,
|
|
35
|
+
ownedObjects: new Set(),
|
|
36
|
+
isRootOwned: false,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Checks if the draft was modified (root !== base).
|
|
42
|
+
*/
|
|
43
|
+
export function isDraftModified<T extends JSONObject>(ctx: DraftContext<T>): boolean {
|
|
44
|
+
return ctx.root !== ctx.base
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function shallowClone<T extends object>(obj: T): T {
|
|
48
|
+
if (Array.isArray(obj)) {
|
|
49
|
+
return obj.slice() as unknown as T
|
|
50
|
+
}
|
|
51
|
+
const clone = {} as T
|
|
52
|
+
const keys = Object.keys(obj)
|
|
53
|
+
for (let i = 0; i < keys.length; i++) {
|
|
54
|
+
const key = keys[i]
|
|
55
|
+
;(clone as any)[key] = (obj as any)[key]
|
|
56
|
+
}
|
|
57
|
+
return clone
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Ensures an object is owned (cloned if necessary) and returns the owned version.
|
|
62
|
+
* Also updates the parent to point to the cloned child.
|
|
63
|
+
*/
|
|
64
|
+
function ensureOwned<T extends JSONObject>(
|
|
65
|
+
ctx: DraftContext<T>,
|
|
66
|
+
parent: JSONObject,
|
|
67
|
+
key: string | number,
|
|
68
|
+
child: JSONRecord | JSONValue[]
|
|
69
|
+
): JSONRecord | JSONValue[] {
|
|
70
|
+
if (ctx.ownedObjects.has(child)) {
|
|
71
|
+
return child
|
|
72
|
+
}
|
|
73
|
+
const cloned = shallowClone(child)
|
|
74
|
+
;(parent as any)[key] = cloned
|
|
75
|
+
ctx.ownedObjects.add(cloned)
|
|
76
|
+
return cloned
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Ensures all objects along the path are owned (cloned if necessary).
|
|
81
|
+
* Returns the container at the end of the path.
|
|
82
|
+
* Throws if the path is invalid.
|
|
83
|
+
*/
|
|
84
|
+
function ensureOwnedPath<T extends JSONObject>(
|
|
85
|
+
ctx: DraftContext<T>,
|
|
86
|
+
path: Path
|
|
87
|
+
): JSONRecord | JSONValue[] {
|
|
88
|
+
// Ensure root is owned first
|
|
89
|
+
if (!ctx.isRootOwned) {
|
|
90
|
+
ctx.root = shallowClone(ctx.root as unknown as object) as T
|
|
91
|
+
ctx.ownedObjects.add(ctx.root)
|
|
92
|
+
ctx.isRootOwned = true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (path.length === 0) {
|
|
96
|
+
return ctx.root
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let current: JSONRecord | JSONValue[] = ctx.root
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < path.length; i++) {
|
|
102
|
+
const segment = path[i]
|
|
103
|
+
const isArrayIndex = typeof segment === "number"
|
|
104
|
+
|
|
105
|
+
// Validate container type
|
|
106
|
+
if (isArrayIndex) {
|
|
107
|
+
if (!Array.isArray(current)) {
|
|
108
|
+
failure(`Expected array at path segment ${segment}`)
|
|
109
|
+
}
|
|
110
|
+
if (segment < 0 || segment >= current.length) {
|
|
111
|
+
failure(`Index ${segment} out of bounds`)
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
if (!isObject(current) || Array.isArray(current)) {
|
|
115
|
+
failure(`Expected object at path segment "${segment}"`)
|
|
116
|
+
}
|
|
117
|
+
if (!(segment in current)) {
|
|
118
|
+
failure(`Property "${segment}" does not exist`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const child: JSONValue = (current as any)[segment]
|
|
123
|
+
|
|
124
|
+
// Validate child is traversable
|
|
125
|
+
if (child === null || typeof child !== "object") {
|
|
126
|
+
failure(`Cannot traverse through primitive at path segment ${segment}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Ensure child is owned and continue
|
|
130
|
+
current = ensureOwned(ctx, current, segment, child as JSONRecord | JSONValue[])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return current
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Applies a single "set" operation to the draft with copy-on-write.
|
|
138
|
+
*/
|
|
139
|
+
export function draftSet<T extends JSONObject>(
|
|
140
|
+
ctx: DraftContext<T>,
|
|
141
|
+
path: Path,
|
|
142
|
+
key: string | number,
|
|
143
|
+
value: JSONValue
|
|
144
|
+
): void {
|
|
145
|
+
const container = ensureOwnedPath(ctx, path)
|
|
146
|
+
if (!isObject(container)) {
|
|
147
|
+
failure("set requires object or array container")
|
|
148
|
+
}
|
|
149
|
+
let finalKey: string | number = key
|
|
150
|
+
// For arrays, convert string keys to numbers (except "length")
|
|
151
|
+
if (Array.isArray(container) && typeof key === "string" && key !== "length") {
|
|
152
|
+
const numKey = parseArrayIndex(key)
|
|
153
|
+
if (numKey === null) {
|
|
154
|
+
failure(`Cannot set non-numeric property "${key}" on array`)
|
|
155
|
+
}
|
|
156
|
+
finalKey = numKey
|
|
157
|
+
}
|
|
158
|
+
;(container as Record<string | number, JSONValue>)[finalKey] = value
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Applies a single "delete" operation to the draft with copy-on-write.
|
|
163
|
+
*/
|
|
164
|
+
export function draftDelete<T extends JSONObject>(
|
|
165
|
+
ctx: DraftContext<T>,
|
|
166
|
+
path: Path,
|
|
167
|
+
key: string | number
|
|
168
|
+
): void {
|
|
169
|
+
const container = ensureOwnedPath(ctx, path)
|
|
170
|
+
if (!isObject(container)) {
|
|
171
|
+
failure("delete requires object or array container")
|
|
172
|
+
}
|
|
173
|
+
let finalKey: string | number = key
|
|
174
|
+
// For arrays, convert string keys to numbers
|
|
175
|
+
if (Array.isArray(container) && typeof key === "string") {
|
|
176
|
+
const numKey = parseArrayIndex(key)
|
|
177
|
+
if (numKey === null) {
|
|
178
|
+
failure(`Cannot delete non-numeric property "${key}" from array`)
|
|
179
|
+
}
|
|
180
|
+
finalKey = numKey
|
|
181
|
+
}
|
|
182
|
+
delete (container as Record<string | number, JSONValue>)[finalKey]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Applies a single "splice" operation to the draft with copy-on-write.
|
|
187
|
+
*/
|
|
188
|
+
export function draftSplice<T extends JSONObject>(
|
|
189
|
+
ctx: DraftContext<T>,
|
|
190
|
+
path: Path,
|
|
191
|
+
index: number,
|
|
192
|
+
deleteCount: number,
|
|
193
|
+
inserts: readonly JSONValue[]
|
|
194
|
+
): void {
|
|
195
|
+
const container = ensureOwnedPath(ctx, path)
|
|
196
|
+
if (!Array.isArray(container)) {
|
|
197
|
+
failure("splice requires array container")
|
|
198
|
+
}
|
|
199
|
+
const safeIndex = Math.min(index, container.length)
|
|
200
|
+
if (inserts.length === 0) {
|
|
201
|
+
container.splice(safeIndex, deleteCount)
|
|
202
|
+
} else if (inserts.length === 1) {
|
|
203
|
+
container.splice(safeIndex, deleteCount, inserts[0])
|
|
204
|
+
} else {
|
|
205
|
+
container.splice(safeIndex, deleteCount, ...inserts)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Applies a single "addToSet" operation to the draft with copy-on-write.
|
|
211
|
+
*/
|
|
212
|
+
export function draftAddToSet<T extends JSONObject>(
|
|
213
|
+
ctx: DraftContext<T>,
|
|
214
|
+
path: Path,
|
|
215
|
+
value: JSONValue
|
|
216
|
+
): void {
|
|
217
|
+
const container = ensureOwnedPath(ctx, path)
|
|
218
|
+
if (!Array.isArray(container)) {
|
|
219
|
+
failure("addToSet requires array container")
|
|
220
|
+
}
|
|
221
|
+
if (!container.some((item) => deepEqual(item, value))) {
|
|
222
|
+
container.push(value)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Applies a single "deleteFromSet" operation to the draft with copy-on-write.
|
|
228
|
+
*/
|
|
229
|
+
export function draftDeleteFromSet<T extends JSONObject>(
|
|
230
|
+
ctx: DraftContext<T>,
|
|
231
|
+
path: Path,
|
|
232
|
+
value: JSONValue
|
|
233
|
+
): void {
|
|
234
|
+
const container = ensureOwnedPath(ctx, path)
|
|
235
|
+
if (!Array.isArray(container)) {
|
|
236
|
+
failure("deleteFromSet requires array container")
|
|
237
|
+
}
|
|
238
|
+
// Remove all matching items (iterate backwards to avoid index shifting)
|
|
239
|
+
for (let i = container.length - 1; i >= 0; i--) {
|
|
240
|
+
if (deepEqual(container[i], value)) {
|
|
241
|
+
container.splice(i, 1)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Applies a single operation to the draft with copy-on-write.
|
|
248
|
+
*/
|
|
249
|
+
export function applyOpToDraft<T extends JSONObject>(ctx: DraftContext<T>, op: Op): void {
|
|
250
|
+
switch (op.kind) {
|
|
251
|
+
case "set":
|
|
252
|
+
draftSet(ctx, op.path, op.key, op.value)
|
|
253
|
+
break
|
|
254
|
+
case "delete":
|
|
255
|
+
draftDelete(ctx, op.path, op.key)
|
|
256
|
+
break
|
|
257
|
+
case "splice":
|
|
258
|
+
draftSplice(ctx, op.path, op.index, op.deleteCount, op.inserts)
|
|
259
|
+
break
|
|
260
|
+
case "addToSet":
|
|
261
|
+
draftAddToSet(ctx, op.path, op.value)
|
|
262
|
+
break
|
|
263
|
+
case "deleteFromSet":
|
|
264
|
+
draftDeleteFromSet(ctx, op.path, op.value)
|
|
265
|
+
break
|
|
266
|
+
default:
|
|
267
|
+
throw failure(`Unknown operation kind: ${(op as any).kind}`)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Applies a single transaction to a base state immutably.
|
|
273
|
+
*
|
|
274
|
+
* Key benefits over Mutative/Immer:
|
|
275
|
+
* - No proxy overhead - direct object access and copy-on-write cloning
|
|
276
|
+
* - Structural sharing - unchanged subtrees keep their references
|
|
277
|
+
* - Zero-copy on failure - if validation fails, returns original base unchanged
|
|
278
|
+
*
|
|
279
|
+
* @param base - The base state (never mutated)
|
|
280
|
+
* @param tx - The transaction to apply
|
|
281
|
+
* @param validateFn - Optional validation function
|
|
282
|
+
* @returns The final state (if valid) or the original base (if invalid or empty)
|
|
283
|
+
*/
|
|
284
|
+
export function applyTxImmutable<T extends JSONObject>(
|
|
285
|
+
base: T,
|
|
286
|
+
tx: Pick<TxRecord, "ops">,
|
|
287
|
+
validateFn?: ValidateFn<T>
|
|
288
|
+
): T {
|
|
289
|
+
if (tx.ops.length === 0) return base
|
|
290
|
+
|
|
291
|
+
const ctx = createDraft(base)
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
for (const op of tx.ops) {
|
|
295
|
+
applyOpToDraft(ctx, op)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (validateFn && !validateFn(ctx.root)) {
|
|
299
|
+
return base
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return ctx.root
|
|
303
|
+
} catch {
|
|
304
|
+
return base
|
|
305
|
+
}
|
|
306
|
+
}
|
package/src/index.ts
CHANGED
package/src/json.ts
CHANGED