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,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy draft implementation.
|
|
3
|
+
* Adapted from mutative - removed Map/Set/unsafe/mark support.
|
|
4
|
+
* Modified to use eager op logging - ops are pushed immediately when mutations happen.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parseArrayIndex } from "../utils"
|
|
8
|
+
import { PROXY_DRAFT } from "./constant"
|
|
9
|
+
import { DraftType, type Finalities, type JSONValue, type Op, type ProxyDraft } from "./interface"
|
|
10
|
+
import { pushOp } from "./pushOp"
|
|
11
|
+
import {
|
|
12
|
+
deepClone,
|
|
13
|
+
ensureShallowCopy,
|
|
14
|
+
get,
|
|
15
|
+
getDescriptor,
|
|
16
|
+
getPathOrThrow,
|
|
17
|
+
getProxyDraft,
|
|
18
|
+
getType,
|
|
19
|
+
getValue,
|
|
20
|
+
handleValue,
|
|
21
|
+
has,
|
|
22
|
+
isDraft,
|
|
23
|
+
isDraftable,
|
|
24
|
+
isEqual,
|
|
25
|
+
latest,
|
|
26
|
+
markChanged,
|
|
27
|
+
peek,
|
|
28
|
+
revokeProxy,
|
|
29
|
+
set,
|
|
30
|
+
} from "./utils"
|
|
31
|
+
|
|
32
|
+
// Note: getValue is used in finalizeDraft, deepClone uses it internally for drafts
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Increment aliasCount for a draft (if the value is a draft).
|
|
36
|
+
*/
|
|
37
|
+
function incAliasCount(value: unknown): void {
|
|
38
|
+
const draft = getProxyDraft(value)
|
|
39
|
+
if (draft) draft.aliasCount++
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Decrement aliasCount for a draft (if the value is a draft).
|
|
44
|
+
*/
|
|
45
|
+
function decAliasCount(value: unknown): void {
|
|
46
|
+
const draft = getProxyDraft(value)
|
|
47
|
+
if (draft) draft.aliasCount--
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Normalize an array index like native array methods do.
|
|
52
|
+
* Handles negative indices and clamps to [0, len].
|
|
53
|
+
* @param index - The index to normalize (can be negative or undefined)
|
|
54
|
+
* @param len - The array length
|
|
55
|
+
* @param defaultValue - Value to use if index is undefined (typically 0 or len)
|
|
56
|
+
*/
|
|
57
|
+
function normalizeIndex(index: number | undefined, len: number, defaultValue: number): number {
|
|
58
|
+
if (index === undefined) return defaultValue
|
|
59
|
+
return index < 0 ? Math.max(len + index, 0) : Math.min(index, len)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Array methods that mutate the array and need to be intercepted for eager op logging.
|
|
64
|
+
*/
|
|
65
|
+
const MUTATING_ARRAY_METHODS = new Set([
|
|
66
|
+
"push",
|
|
67
|
+
"pop",
|
|
68
|
+
"shift",
|
|
69
|
+
"unshift",
|
|
70
|
+
"splice",
|
|
71
|
+
"sort",
|
|
72
|
+
"reverse",
|
|
73
|
+
"fill",
|
|
74
|
+
"copyWithin",
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a wrapped array method that logs ops eagerly.
|
|
79
|
+
* @param proxyDraft - The ProxyDraft for the array (used for ops and state)
|
|
80
|
+
* @param proxyRef - The actual proxy (used to access elements as drafts)
|
|
81
|
+
* @param method - The method name being wrapped
|
|
82
|
+
*/
|
|
83
|
+
function createArrayMethodWrapper(
|
|
84
|
+
proxyDraft: ProxyDraft,
|
|
85
|
+
proxyRef: unknown,
|
|
86
|
+
method: string
|
|
87
|
+
): (...args: unknown[]) => unknown {
|
|
88
|
+
return function (this: unknown[], ...args: unknown[]): unknown {
|
|
89
|
+
ensureShallowCopy(proxyDraft)
|
|
90
|
+
markChanged(proxyDraft)
|
|
91
|
+
|
|
92
|
+
const arr = proxyDraft.copy as unknown[]
|
|
93
|
+
const originalLength = arr.length
|
|
94
|
+
const proxy = proxyRef as unknown[]
|
|
95
|
+
|
|
96
|
+
switch (method) {
|
|
97
|
+
case "push": {
|
|
98
|
+
// push(items...) -> splice at end
|
|
99
|
+
// Track aliasCount for drafts being inserted
|
|
100
|
+
for (const arg of args) {
|
|
101
|
+
incAliasCount(arg)
|
|
102
|
+
}
|
|
103
|
+
// Store raw values in array (preserving aliasing in the draft)
|
|
104
|
+
const result = arr.push(...args)
|
|
105
|
+
// Clone for the op (capture value at this moment)
|
|
106
|
+
pushOp(proxyDraft, {
|
|
107
|
+
kind: "splice",
|
|
108
|
+
path: getPathOrThrow(proxyDraft),
|
|
109
|
+
index: originalLength,
|
|
110
|
+
deleteCount: 0,
|
|
111
|
+
inserts: args.map(deepClone) as JSONValue[],
|
|
112
|
+
})
|
|
113
|
+
return result
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case "pop": {
|
|
117
|
+
if (originalLength === 0) {
|
|
118
|
+
return arr.pop()
|
|
119
|
+
}
|
|
120
|
+
// Get the element through the proxy to ensure it's a draft if draftable
|
|
121
|
+
const returnValue = proxy[originalLength - 1]
|
|
122
|
+
// Track aliasCount for draft being removed
|
|
123
|
+
decAliasCount(arr[originalLength - 1])
|
|
124
|
+
// Now perform the actual mutation
|
|
125
|
+
arr.pop()
|
|
126
|
+
pushOp(proxyDraft, {
|
|
127
|
+
kind: "splice",
|
|
128
|
+
path: getPathOrThrow(proxyDraft),
|
|
129
|
+
index: originalLength - 1,
|
|
130
|
+
deleteCount: 1,
|
|
131
|
+
inserts: [],
|
|
132
|
+
})
|
|
133
|
+
return returnValue
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case "shift": {
|
|
137
|
+
if (originalLength === 0) {
|
|
138
|
+
return arr.shift()
|
|
139
|
+
}
|
|
140
|
+
// Get the element through the proxy to ensure it's a draft if draftable
|
|
141
|
+
const returnValue = proxy[0]
|
|
142
|
+
// Track aliasCount for draft being removed
|
|
143
|
+
decAliasCount(arr[0])
|
|
144
|
+
// Now perform the actual mutation
|
|
145
|
+
arr.shift()
|
|
146
|
+
pushOp(proxyDraft, {
|
|
147
|
+
kind: "splice",
|
|
148
|
+
path: getPathOrThrow(proxyDraft),
|
|
149
|
+
index: 0,
|
|
150
|
+
deleteCount: 1,
|
|
151
|
+
inserts: [],
|
|
152
|
+
})
|
|
153
|
+
return returnValue
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case "unshift": {
|
|
157
|
+
// Track aliasCount for drafts being inserted
|
|
158
|
+
for (const arg of args) {
|
|
159
|
+
incAliasCount(arg)
|
|
160
|
+
}
|
|
161
|
+
// Store raw values in array (preserving aliasing in the draft)
|
|
162
|
+
const result = arr.unshift(...args)
|
|
163
|
+
// Clone for the op (capture value at this moment)
|
|
164
|
+
pushOp(proxyDraft, {
|
|
165
|
+
kind: "splice",
|
|
166
|
+
path: getPathOrThrow(proxyDraft),
|
|
167
|
+
index: 0,
|
|
168
|
+
deleteCount: 0,
|
|
169
|
+
inserts: args.map(deepClone) as JSONValue[],
|
|
170
|
+
})
|
|
171
|
+
return result
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case "splice": {
|
|
175
|
+
const start = args[0] as number | undefined
|
|
176
|
+
const deleteCountArg = args[1] as number | undefined
|
|
177
|
+
const inserts = args.slice(2)
|
|
178
|
+
|
|
179
|
+
// Normalize start index to get elements through proxy
|
|
180
|
+
const index =
|
|
181
|
+
start === undefined ? 0 : start < 0 ? Math.max(originalLength + start, 0) : start
|
|
182
|
+
const deleteCount = deleteCountArg ?? originalLength - index
|
|
183
|
+
|
|
184
|
+
// Get elements through proxy to ensure they're drafts if draftable
|
|
185
|
+
const returnValues: unknown[] = []
|
|
186
|
+
for (let i = 0; i < deleteCount && index + i < originalLength; i++) {
|
|
187
|
+
returnValues.push(proxy[index + i])
|
|
188
|
+
// Track aliasCount for drafts being removed
|
|
189
|
+
decAliasCount(arr[index + i])
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Track aliasCount for drafts being inserted
|
|
193
|
+
for (const insert of inserts) {
|
|
194
|
+
incAliasCount(insert)
|
|
195
|
+
}
|
|
196
|
+
// Store raw values in array (preserving aliasing in the draft)
|
|
197
|
+
;(arr.splice as (...args: unknown[]) => unknown[])(...args)
|
|
198
|
+
|
|
199
|
+
// Clone for the op (capture value at this moment)
|
|
200
|
+
pushOp(proxyDraft, {
|
|
201
|
+
kind: "splice",
|
|
202
|
+
path: getPathOrThrow(proxyDraft),
|
|
203
|
+
index: start ?? 0,
|
|
204
|
+
deleteCount: deleteCountArg ?? originalLength,
|
|
205
|
+
inserts: inserts.map(deepClone) as JSONValue[],
|
|
206
|
+
})
|
|
207
|
+
return returnValues
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case "fill": {
|
|
211
|
+
const fillValue = args[0]
|
|
212
|
+
const startArg = args[1] as number | undefined
|
|
213
|
+
const endArg = args[2] as number | undefined
|
|
214
|
+
|
|
215
|
+
const len = originalLength
|
|
216
|
+
const start = normalizeIndex(startArg, len, 0)
|
|
217
|
+
const end = normalizeIndex(endArg, len, len)
|
|
218
|
+
|
|
219
|
+
// Use proxy splice to replace the range (handles aliasCount and generates splice op)
|
|
220
|
+
const fillCount = end - start
|
|
221
|
+
if (fillCount > 0) {
|
|
222
|
+
const fillValues = Array(fillCount).fill(fillValue)
|
|
223
|
+
proxy.splice(start, fillCount, ...fillValues)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return proxy
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case "sort": {
|
|
230
|
+
const compareFn = args[0] as ((a: unknown, b: unknown) => number) | undefined
|
|
231
|
+
// Sort a copy to get the sorted order, then use proxy splice
|
|
232
|
+
const sorted = [...arr].sort(compareFn)
|
|
233
|
+
if (originalLength > 0) {
|
|
234
|
+
proxy.splice(0, originalLength, ...sorted)
|
|
235
|
+
}
|
|
236
|
+
return proxy
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case "reverse": {
|
|
240
|
+
// Reverse a copy to get the reversed order, then use proxy splice
|
|
241
|
+
const reversed = [...arr].reverse()
|
|
242
|
+
if (originalLength > 0) {
|
|
243
|
+
proxy.splice(0, originalLength, ...reversed)
|
|
244
|
+
}
|
|
245
|
+
return proxy
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case "copyWithin": {
|
|
249
|
+
const targetArg = args[0] as number
|
|
250
|
+
const startArg = args[1] as number | undefined
|
|
251
|
+
const endArg = args[2] as number | undefined
|
|
252
|
+
|
|
253
|
+
const len = originalLength
|
|
254
|
+
const targetIdx = normalizeIndex(targetArg, len, 0)
|
|
255
|
+
const startIdx = normalizeIndex(startArg, len, 0)
|
|
256
|
+
const endIdx = normalizeIndex(endArg, len, len)
|
|
257
|
+
|
|
258
|
+
// Calculate how many elements will be copied
|
|
259
|
+
const copyCount = Math.min(endIdx - startIdx, len - targetIdx)
|
|
260
|
+
|
|
261
|
+
if (copyCount > 0) {
|
|
262
|
+
// Get the elements that will be copied (from the source range)
|
|
263
|
+
const elementsToCopy = arr.slice(startIdx, startIdx + copyCount)
|
|
264
|
+
// Use proxy splice to replace just the affected range
|
|
265
|
+
proxy.splice(targetIdx, copyCount, ...elementsToCopy)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return proxy
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
default:
|
|
272
|
+
return (arr as unknown as Record<string, (...args: unknown[]) => unknown>)[method](...args)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Proxy handler for drafts
|
|
279
|
+
*/
|
|
280
|
+
const proxyHandler: ProxyHandler<ProxyDraft> = {
|
|
281
|
+
get(target: ProxyDraft, key: PropertyKey, receiver: unknown) {
|
|
282
|
+
// Return cached draft if available
|
|
283
|
+
const copy = target.copy?.[key as keyof typeof target.copy]
|
|
284
|
+
if (copy && target.finalities.draftsCache.has(copy as object)) {
|
|
285
|
+
return copy
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Return the ProxyDraft itself when accessing the symbol
|
|
289
|
+
if (key === PROXY_DRAFT) return target
|
|
290
|
+
|
|
291
|
+
const source = latest(target)
|
|
292
|
+
|
|
293
|
+
// Intercept mutating array methods for eager op logging
|
|
294
|
+
if (
|
|
295
|
+
target.type === DraftType.Array &&
|
|
296
|
+
typeof key === "string" &&
|
|
297
|
+
MUTATING_ARRAY_METHODS.has(key)
|
|
298
|
+
) {
|
|
299
|
+
const originalMethod = (source as unknown[])[key as keyof unknown[]]
|
|
300
|
+
if (typeof originalMethod === "function") {
|
|
301
|
+
return createArrayMethodWrapper(target, receiver, key)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Property doesn't exist - check prototype chain
|
|
306
|
+
if (!has(source, key)) {
|
|
307
|
+
const desc = getDescriptor(source, key)
|
|
308
|
+
return desc ? ("value" in desc ? desc.value : desc.get?.call(target.proxy)) : undefined
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const value = (source as Record<PropertyKey, unknown>)[key]
|
|
312
|
+
|
|
313
|
+
// Already finalized or not draftable - return as-is
|
|
314
|
+
if (target.finalized || !isDraftable(value)) {
|
|
315
|
+
return value
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// If value is same as original, create a nested draft
|
|
319
|
+
if (value === peek(target.original, key)) {
|
|
320
|
+
ensureShallowCopy(target)
|
|
321
|
+
const nestedKey = target.type === DraftType.Array ? Number(key) : key
|
|
322
|
+
;(target.copy as Record<PropertyKey, unknown>)[key] = createDraft({
|
|
323
|
+
original: (target.original as Record<PropertyKey, unknown>)[key] as object,
|
|
324
|
+
parentDraft: target,
|
|
325
|
+
key: nestedKey as string | number,
|
|
326
|
+
finalities: target.finalities,
|
|
327
|
+
})
|
|
328
|
+
return (target.copy as Record<PropertyKey, unknown>)[key]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Cache drafts that were assigned
|
|
332
|
+
if (isDraft(value)) {
|
|
333
|
+
target.finalities.draftsCache.add(value as object)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return value
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
set(target: ProxyDraft, key: PropertyKey, value: unknown) {
|
|
340
|
+
if (typeof key === "symbol") {
|
|
341
|
+
throw new Error(`Cannot set symbol properties on drafts`)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// For arrays, convert and validate the key
|
|
345
|
+
let opKey: string | number = key as string | number
|
|
346
|
+
if (target.type === DraftType.Array) {
|
|
347
|
+
if (key === "length") {
|
|
348
|
+
opKey = "length"
|
|
349
|
+
} else {
|
|
350
|
+
const numKey = typeof key === "number" ? key : parseArrayIndex(key as string)
|
|
351
|
+
if (numKey === null) {
|
|
352
|
+
throw new Error(`Only supports setting array indices and the 'length' property.`)
|
|
353
|
+
}
|
|
354
|
+
opKey = numKey
|
|
355
|
+
|
|
356
|
+
// Check for sparse array creation
|
|
357
|
+
const source = latest(target) as unknown[]
|
|
358
|
+
if (numKey > source.length) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`Cannot create sparse array. Index ${numKey} is out of bounds for array of length ${source.length}.`
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Handle setter from prototype
|
|
367
|
+
const desc = getDescriptor(latest(target), key)
|
|
368
|
+
if (desc?.set) {
|
|
369
|
+
desc.set.call(target.proxy, value)
|
|
370
|
+
return true
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const current = peek(latest(target), key)
|
|
374
|
+
const currentProxyDraft = getProxyDraft(current)
|
|
375
|
+
|
|
376
|
+
// If assigning original draftable value back to its draft, just mark as not assigned
|
|
377
|
+
if (currentProxyDraft && isEqual(currentProxyDraft.original, value)) {
|
|
378
|
+
;(target.copy as Record<PropertyKey, unknown>)[key] = value
|
|
379
|
+
target.assignedMap = target.assignedMap ?? new Map()
|
|
380
|
+
target.assignedMap.set(key, false)
|
|
381
|
+
return true
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// No change - skip
|
|
385
|
+
if (isEqual(value, current) && (value !== undefined || has(target.original, key))) {
|
|
386
|
+
return true
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
ensureShallowCopy(target)
|
|
390
|
+
markChanged(target)
|
|
391
|
+
|
|
392
|
+
// Track assignment (still needed for finalization to know what changed)
|
|
393
|
+
if (
|
|
394
|
+
has(target.original, key) &&
|
|
395
|
+
isEqual(value, (target.original as Record<PropertyKey, unknown>)[key])
|
|
396
|
+
) {
|
|
397
|
+
// Reverting to original value - still log the op since we're doing eager logging
|
|
398
|
+
target.assignedMap!.delete(key)
|
|
399
|
+
} else {
|
|
400
|
+
target.assignedMap!.set(key, true)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Track aliasCount and update copy
|
|
404
|
+
const copy = target.copy as Record<PropertyKey, unknown>
|
|
405
|
+
decAliasCount(copy[key])
|
|
406
|
+
incAliasCount(value)
|
|
407
|
+
copy[key] = value
|
|
408
|
+
|
|
409
|
+
// Eager op logging for set operations
|
|
410
|
+
if (target.type === DraftType.Array && opKey === "length") {
|
|
411
|
+
const oldLength = (target.original as unknown[]).length
|
|
412
|
+
const newLength = value as number
|
|
413
|
+
if (newLength !== oldLength) {
|
|
414
|
+
// Length change - always use set op to capture intent
|
|
415
|
+
pushOp(target, {
|
|
416
|
+
kind: "set",
|
|
417
|
+
path: getPathOrThrow(target),
|
|
418
|
+
key: "length",
|
|
419
|
+
value: newLength,
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
// Regular property set - use opKey (numeric for arrays)
|
|
424
|
+
pushOp(target, {
|
|
425
|
+
kind: "set",
|
|
426
|
+
path: getPathOrThrow(target),
|
|
427
|
+
key: opKey,
|
|
428
|
+
value: deepClone(value) as JSONValue,
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return true
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
has(target: ProxyDraft, key: PropertyKey) {
|
|
436
|
+
return key in latest(target)
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
ownKeys(target: ProxyDraft) {
|
|
440
|
+
return Reflect.ownKeys(latest(target))
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
getOwnPropertyDescriptor(target: ProxyDraft, key: PropertyKey) {
|
|
444
|
+
const source = latest(target)
|
|
445
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(source, key)
|
|
446
|
+
if (!descriptor) return descriptor
|
|
447
|
+
return {
|
|
448
|
+
writable: true,
|
|
449
|
+
configurable: target.type !== DraftType.Array || key !== "length",
|
|
450
|
+
enumerable: descriptor.enumerable,
|
|
451
|
+
value: (source as Record<PropertyKey, unknown>)[key],
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
getPrototypeOf(target: ProxyDraft) {
|
|
456
|
+
return Reflect.getPrototypeOf(target.original as object)
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
setPrototypeOf() {
|
|
460
|
+
throw new Error(`Cannot call 'setPrototypeOf()' on drafts`)
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
defineProperty() {
|
|
464
|
+
throw new Error(`Cannot call 'defineProperty()' on drafts`)
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
deleteProperty(target: ProxyDraft, key: PropertyKey) {
|
|
468
|
+
if (typeof key === "symbol") {
|
|
469
|
+
throw new Error(`Cannot delete symbol properties from drafts`)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (target.type === DraftType.Array) {
|
|
473
|
+
// For arrays, deleting a property sets it to undefined
|
|
474
|
+
return proxyHandler.set!.call(this, target, key as string | symbol, undefined, target.proxy)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Check if property exists and get its current value
|
|
478
|
+
const current = peek(latest(target), key)
|
|
479
|
+
const existed = current !== undefined || key in (target.original as object)
|
|
480
|
+
|
|
481
|
+
// For objects, track deletion
|
|
482
|
+
if (existed) {
|
|
483
|
+
ensureShallowCopy(target)
|
|
484
|
+
markChanged(target)
|
|
485
|
+
target.assignedMap!.set(key, false)
|
|
486
|
+
|
|
487
|
+
// Eager op logging for delete
|
|
488
|
+
pushOp(target, {
|
|
489
|
+
kind: "delete",
|
|
490
|
+
path: getPathOrThrow(target),
|
|
491
|
+
key: key,
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
// Track aliasCount and delete from copy
|
|
495
|
+
const copy = target.copy as Record<PropertyKey, unknown>
|
|
496
|
+
decAliasCount(copy[key])
|
|
497
|
+
delete copy[key]
|
|
498
|
+
} else {
|
|
499
|
+
target.assignedMap = target.assignedMap ?? new Map()
|
|
500
|
+
target.assignedMap.delete(key)
|
|
501
|
+
}
|
|
502
|
+
return true
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Create a draft proxy for a value
|
|
508
|
+
*/
|
|
509
|
+
export function createDraft<T extends object>(options: {
|
|
510
|
+
original: T
|
|
511
|
+
parentDraft?: ProxyDraft | null
|
|
512
|
+
key?: string | number
|
|
513
|
+
finalities: Finalities
|
|
514
|
+
}): T {
|
|
515
|
+
const { original, parentDraft, key, finalities } = options
|
|
516
|
+
const type = getType(original)
|
|
517
|
+
|
|
518
|
+
const proxyDraft: ProxyDraft<T> = {
|
|
519
|
+
type,
|
|
520
|
+
finalized: false,
|
|
521
|
+
parent: parentDraft ?? null,
|
|
522
|
+
original,
|
|
523
|
+
copy: null,
|
|
524
|
+
proxy: null,
|
|
525
|
+
finalities,
|
|
526
|
+
aliasCount: 1,
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Set key if provided
|
|
530
|
+
if (key !== undefined || "key" in options) {
|
|
531
|
+
proxyDraft.key = key
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Create revocable proxy
|
|
535
|
+
const { proxy, revoke } = Proxy.revocable<T>(
|
|
536
|
+
(type === DraftType.Array ? Object.assign([], proxyDraft) : proxyDraft) as T,
|
|
537
|
+
proxyHandler as ProxyHandler<T>
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
finalities.revoke.push(revoke)
|
|
541
|
+
proxyDraft.proxy = proxy
|
|
542
|
+
|
|
543
|
+
// Set up finalization callback to unwrap child drafts in parent copy
|
|
544
|
+
if (parentDraft) {
|
|
545
|
+
parentDraft.finalities.draft.push(() => {
|
|
546
|
+
const copy = parentDraft.copy
|
|
547
|
+
if (!copy) return
|
|
548
|
+
|
|
549
|
+
const draft = get(copy as object, key!)
|
|
550
|
+
const childProxyDraft = getProxyDraft(draft)
|
|
551
|
+
|
|
552
|
+
if (childProxyDraft) {
|
|
553
|
+
// Get the updated value
|
|
554
|
+
let updatedValue = childProxyDraft.original
|
|
555
|
+
if (childProxyDraft.operated) {
|
|
556
|
+
updatedValue = getValue(draft as object)
|
|
557
|
+
}
|
|
558
|
+
childProxyDraft.finalized = true
|
|
559
|
+
set(copy as object, key!, updatedValue)
|
|
560
|
+
}
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return proxy
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Finalize a draft and return the result with ops
|
|
569
|
+
*/
|
|
570
|
+
export function finalizeDraft<T>(result: T, returnedValue: [T] | []): [T, Op[]] {
|
|
571
|
+
const proxyDraft = getProxyDraft<T>(result)
|
|
572
|
+
const hasReturnedValue = returnedValue.length > 0
|
|
573
|
+
|
|
574
|
+
// Run finalization callbacks to unwrap child drafts
|
|
575
|
+
if (proxyDraft?.operated) {
|
|
576
|
+
while (proxyDraft.finalities.draft.length > 0) {
|
|
577
|
+
const finalize = proxyDraft.finalities.draft.pop()!
|
|
578
|
+
finalize()
|
|
579
|
+
}
|
|
580
|
+
proxyDraft.finalized = true
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Determine final state
|
|
584
|
+
const state = hasReturnedValue
|
|
585
|
+
? returnedValue[0]
|
|
586
|
+
: proxyDraft
|
|
587
|
+
? proxyDraft.operated
|
|
588
|
+
? (proxyDraft.copy as T)
|
|
589
|
+
: proxyDraft.original
|
|
590
|
+
: result
|
|
591
|
+
|
|
592
|
+
// Handle any remaining nested drafts in the state (e.g., from assignments like draft.a = draft.b)
|
|
593
|
+
if (proxyDraft && state && typeof state === "object") {
|
|
594
|
+
handleValue(state, proxyDraft.finalities.handledSet)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Get ops from finalities (eager logging)
|
|
598
|
+
const ops = proxyDraft?.finalities.ops ?? []
|
|
599
|
+
|
|
600
|
+
// Revoke all proxies
|
|
601
|
+
if (proxyDraft) {
|
|
602
|
+
revokeProxy(proxyDraft)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return [state as T, ops]
|
|
606
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a draft from a base state.
|
|
3
|
+
* Simplified from mutative - removed patches/freeze/mark support.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createDraft, finalizeDraft } from "./draft"
|
|
7
|
+
import type { Finalities, Op } from "./interface"
|
|
8
|
+
import { getProxyDraft, isDraftable } from "./utils"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a draft and return a finalize function
|
|
12
|
+
*/
|
|
13
|
+
export function draftify<T extends object>(
|
|
14
|
+
baseState: T
|
|
15
|
+
): [T, (returnedValue: [T] | []) => [T, Op[]]] {
|
|
16
|
+
const finalities: Finalities = {
|
|
17
|
+
draft: [],
|
|
18
|
+
revoke: [],
|
|
19
|
+
handledSet: new WeakSet<object>(),
|
|
20
|
+
draftsCache: new WeakSet<object>(),
|
|
21
|
+
ops: [],
|
|
22
|
+
rootDraft: null, // Will be set by createDraft
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check if state is draftable
|
|
26
|
+
if (!isDraftable(baseState)) {
|
|
27
|
+
throw new Error(`createOps() only supports plain objects and arrays.`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const draft = createDraft({
|
|
31
|
+
original: baseState,
|
|
32
|
+
parentDraft: null,
|
|
33
|
+
finalities,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Set the root draft for multi-path detection
|
|
37
|
+
finalities.rootDraft = getProxyDraft(draft)
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
draft,
|
|
41
|
+
(returnedValue: [T] | [] = []) => {
|
|
42
|
+
return finalizeDraft(draft, returnedValue)
|
|
43
|
+
},
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
|
|
8
|
+
// Main API
|
|
9
|
+
export { createOps } from "./createOps"
|
|
10
|
+
export { current } from "./current"
|
|
11
|
+
// Types
|
|
12
|
+
export type { CreateOpsResult, Draft, Immutable, Op, Path } from "./interface"
|
|
13
|
+
// Utilities
|
|
14
|
+
export { original } from "./original"
|
|
15
|
+
|
|
16
|
+
// Set-like helpers
|
|
17
|
+
export { addToSet, deleteFromSet } from "./setHelpers"
|
|
18
|
+
export { isDraft, isDraftable } from "./utils"
|