@tanstack/db 0.0.1

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.
Files changed (154) hide show
  1. package/README.md +37 -0
  2. package/dist/cjs/SortedMap.cjs +140 -0
  3. package/dist/cjs/SortedMap.cjs.map +1 -0
  4. package/dist/cjs/SortedMap.d.cts +91 -0
  5. package/dist/cjs/collection.cjs +597 -0
  6. package/dist/cjs/collection.cjs.map +1 -0
  7. package/dist/cjs/collection.d.cts +176 -0
  8. package/dist/cjs/deferred.cjs +25 -0
  9. package/dist/cjs/deferred.cjs.map +1 -0
  10. package/dist/cjs/deferred.d.cts +20 -0
  11. package/dist/cjs/errors.cjs +10 -0
  12. package/dist/cjs/errors.cjs.map +1 -0
  13. package/dist/cjs/errors.d.cts +3 -0
  14. package/dist/cjs/index.cjs +33 -0
  15. package/dist/cjs/index.cjs.map +1 -0
  16. package/dist/cjs/index.d.cts +9 -0
  17. package/dist/cjs/proxy.cjs +654 -0
  18. package/dist/cjs/proxy.cjs.map +1 -0
  19. package/dist/cjs/proxy.d.cts +59 -0
  20. package/dist/cjs/query/compiled-query.cjs +162 -0
  21. package/dist/cjs/query/compiled-query.cjs.map +1 -0
  22. package/dist/cjs/query/compiled-query.d.cts +22 -0
  23. package/dist/cjs/query/evaluators.cjs +146 -0
  24. package/dist/cjs/query/evaluators.cjs.map +1 -0
  25. package/dist/cjs/query/evaluators.d.cts +9 -0
  26. package/dist/cjs/query/extractors.cjs +122 -0
  27. package/dist/cjs/query/extractors.cjs.map +1 -0
  28. package/dist/cjs/query/extractors.d.cts +22 -0
  29. package/dist/cjs/query/functions.cjs +152 -0
  30. package/dist/cjs/query/functions.cjs.map +1 -0
  31. package/dist/cjs/query/functions.d.cts +21 -0
  32. package/dist/cjs/query/group-by.cjs +91 -0
  33. package/dist/cjs/query/group-by.cjs.map +1 -0
  34. package/dist/cjs/query/group-by.d.cts +40 -0
  35. package/dist/cjs/query/index.d.cts +5 -0
  36. package/dist/cjs/query/joins.cjs +155 -0
  37. package/dist/cjs/query/joins.cjs.map +1 -0
  38. package/dist/cjs/query/joins.d.cts +14 -0
  39. package/dist/cjs/query/key-by.cjs +43 -0
  40. package/dist/cjs/query/key-by.cjs.map +1 -0
  41. package/dist/cjs/query/key-by.d.cts +3 -0
  42. package/dist/cjs/query/order-by.cjs +229 -0
  43. package/dist/cjs/query/order-by.cjs.map +1 -0
  44. package/dist/cjs/query/order-by.d.cts +3 -0
  45. package/dist/cjs/query/pipeline-compiler.cjs +94 -0
  46. package/dist/cjs/query/pipeline-compiler.cjs.map +1 -0
  47. package/dist/cjs/query/pipeline-compiler.d.cts +9 -0
  48. package/dist/cjs/query/query-builder.cjs +314 -0
  49. package/dist/cjs/query/query-builder.cjs.map +1 -0
  50. package/dist/cjs/query/query-builder.d.cts +219 -0
  51. package/dist/cjs/query/schema.d.cts +98 -0
  52. package/dist/cjs/query/select.cjs +107 -0
  53. package/dist/cjs/query/select.cjs.map +1 -0
  54. package/dist/cjs/query/select.d.cts +3 -0
  55. package/dist/cjs/query/types.d.cts +188 -0
  56. package/dist/cjs/query/utils.cjs +154 -0
  57. package/dist/cjs/query/utils.cjs.map +1 -0
  58. package/dist/cjs/query/utils.d.cts +37 -0
  59. package/dist/cjs/transactions.cjs +137 -0
  60. package/dist/cjs/transactions.cjs.map +1 -0
  61. package/dist/cjs/transactions.d.cts +27 -0
  62. package/dist/cjs/types.d.cts +94 -0
  63. package/dist/cjs/utils.cjs +17 -0
  64. package/dist/cjs/utils.cjs.map +1 -0
  65. package/dist/cjs/utils.d.cts +3 -0
  66. package/dist/esm/SortedMap.d.ts +91 -0
  67. package/dist/esm/SortedMap.js +140 -0
  68. package/dist/esm/SortedMap.js.map +1 -0
  69. package/dist/esm/collection.d.ts +176 -0
  70. package/dist/esm/collection.js +597 -0
  71. package/dist/esm/collection.js.map +1 -0
  72. package/dist/esm/deferred.d.ts +20 -0
  73. package/dist/esm/deferred.js +25 -0
  74. package/dist/esm/deferred.js.map +1 -0
  75. package/dist/esm/errors.d.ts +3 -0
  76. package/dist/esm/errors.js +10 -0
  77. package/dist/esm/errors.js.map +1 -0
  78. package/dist/esm/index.d.ts +9 -0
  79. package/dist/esm/index.js +33 -0
  80. package/dist/esm/index.js.map +1 -0
  81. package/dist/esm/proxy.d.ts +59 -0
  82. package/dist/esm/proxy.js +654 -0
  83. package/dist/esm/proxy.js.map +1 -0
  84. package/dist/esm/query/compiled-query.d.ts +22 -0
  85. package/dist/esm/query/compiled-query.js +162 -0
  86. package/dist/esm/query/compiled-query.js.map +1 -0
  87. package/dist/esm/query/evaluators.d.ts +9 -0
  88. package/dist/esm/query/evaluators.js +146 -0
  89. package/dist/esm/query/evaluators.js.map +1 -0
  90. package/dist/esm/query/extractors.d.ts +22 -0
  91. package/dist/esm/query/extractors.js +122 -0
  92. package/dist/esm/query/extractors.js.map +1 -0
  93. package/dist/esm/query/functions.d.ts +21 -0
  94. package/dist/esm/query/functions.js +152 -0
  95. package/dist/esm/query/functions.js.map +1 -0
  96. package/dist/esm/query/group-by.d.ts +40 -0
  97. package/dist/esm/query/group-by.js +91 -0
  98. package/dist/esm/query/group-by.js.map +1 -0
  99. package/dist/esm/query/index.d.ts +5 -0
  100. package/dist/esm/query/joins.d.ts +14 -0
  101. package/dist/esm/query/joins.js +155 -0
  102. package/dist/esm/query/joins.js.map +1 -0
  103. package/dist/esm/query/key-by.d.ts +3 -0
  104. package/dist/esm/query/key-by.js +43 -0
  105. package/dist/esm/query/key-by.js.map +1 -0
  106. package/dist/esm/query/order-by.d.ts +3 -0
  107. package/dist/esm/query/order-by.js +229 -0
  108. package/dist/esm/query/order-by.js.map +1 -0
  109. package/dist/esm/query/pipeline-compiler.d.ts +9 -0
  110. package/dist/esm/query/pipeline-compiler.js +94 -0
  111. package/dist/esm/query/pipeline-compiler.js.map +1 -0
  112. package/dist/esm/query/query-builder.d.ts +219 -0
  113. package/dist/esm/query/query-builder.js +314 -0
  114. package/dist/esm/query/query-builder.js.map +1 -0
  115. package/dist/esm/query/schema.d.ts +98 -0
  116. package/dist/esm/query/select.d.ts +3 -0
  117. package/dist/esm/query/select.js +107 -0
  118. package/dist/esm/query/select.js.map +1 -0
  119. package/dist/esm/query/types.d.ts +188 -0
  120. package/dist/esm/query/utils.d.ts +37 -0
  121. package/dist/esm/query/utils.js +154 -0
  122. package/dist/esm/query/utils.js.map +1 -0
  123. package/dist/esm/transactions.d.ts +27 -0
  124. package/dist/esm/transactions.js +137 -0
  125. package/dist/esm/transactions.js.map +1 -0
  126. package/dist/esm/types.d.ts +94 -0
  127. package/dist/esm/utils.d.ts +3 -0
  128. package/dist/esm/utils.js +17 -0
  129. package/dist/esm/utils.js.map +1 -0
  130. package/package.json +57 -0
  131. package/src/SortedMap.ts +163 -0
  132. package/src/collection.ts +919 -0
  133. package/src/deferred.ts +47 -0
  134. package/src/errors.ts +6 -0
  135. package/src/index.ts +12 -0
  136. package/src/proxy.ts +1104 -0
  137. package/src/query/compiled-query.ts +193 -0
  138. package/src/query/evaluators.ts +222 -0
  139. package/src/query/extractors.ts +211 -0
  140. package/src/query/functions.ts +297 -0
  141. package/src/query/group-by.ts +137 -0
  142. package/src/query/index.ts +5 -0
  143. package/src/query/joins.ts +247 -0
  144. package/src/query/key-by.ts +61 -0
  145. package/src/query/order-by.ts +312 -0
  146. package/src/query/pipeline-compiler.ts +152 -0
  147. package/src/query/query-builder.ts +898 -0
  148. package/src/query/schema.ts +255 -0
  149. package/src/query/select.ts +173 -0
  150. package/src/query/types.ts +417 -0
  151. package/src/query/utils.ts +245 -0
  152. package/src/transactions.ts +198 -0
  153. package/src/types.ts +125 -0
  154. package/src/utils.ts +15 -0
package/src/proxy.ts ADDED
@@ -0,0 +1,1104 @@
1
+ /**
2
+ * A utility for creating a proxy that captures changes to an object
3
+ * and provides a way to retrieve those changes.
4
+ */
5
+
6
+ /**
7
+ * Simple debug utility that only logs when debug mode is enabled
8
+ * Set DEBUG to true in localStorage to enable debug logging
9
+ */
10
+ function debugLog(...args: Array<unknown>): void {
11
+ // Check if we're in a browser environment
12
+ const isBrowser =
13
+ typeof window !== `undefined` && typeof localStorage !== `undefined`
14
+
15
+ // In browser, check localStorage for debug flag
16
+ if (isBrowser && localStorage.getItem(`DEBUG`) === `true`) {
17
+ console.log(`[proxy]`, ...args)
18
+ }
19
+ // In Node.js environment, check for environment variable (though this is primarily for browser)
20
+ else if (
21
+ !isBrowser &&
22
+ typeof process !== `undefined` &&
23
+ process.env.DEBUG === `true`
24
+ ) {
25
+ console.log(`[proxy]`, ...args)
26
+ }
27
+ }
28
+
29
+ // Add TypedArray interface with proper type
30
+ interface TypedArray {
31
+ length: number
32
+ [index: number]: number
33
+ }
34
+
35
+ // Update type for ChangeTracker
36
+ interface ChangeTracker<T extends object> {
37
+ changes: Record<string | symbol, unknown>
38
+ originalObject: T
39
+ modified: boolean
40
+ copy_?: T
41
+ assigned_: Record<string | symbol, boolean>
42
+ parent?: {
43
+ tracker: ChangeTracker<object>
44
+ prop: string | symbol
45
+ }
46
+ target: T
47
+ }
48
+
49
+ /**
50
+ * Deep clones an object while preserving special types like Date and RegExp
51
+ */
52
+
53
+ function deepClone<T extends unknown>(
54
+ obj: T,
55
+ visited = new WeakMap<object, unknown>()
56
+ ): T {
57
+ // Handle null and undefined
58
+ if (obj === null || obj === undefined) {
59
+ return obj
60
+ }
61
+
62
+ // Handle primitive types
63
+ if (typeof obj !== `object`) {
64
+ return obj
65
+ }
66
+
67
+ // If we've already cloned this object, return the cached clone
68
+ if (visited.has(obj as object)) {
69
+ return visited.get(obj as object) as T
70
+ }
71
+
72
+ if (obj instanceof Date) {
73
+ return new Date(obj.getTime()) as unknown as T
74
+ }
75
+
76
+ if (obj instanceof RegExp) {
77
+ return new RegExp(obj.source, obj.flags) as unknown as T
78
+ }
79
+
80
+ if (Array.isArray(obj)) {
81
+ const arrayClone = [] as Array<unknown>
82
+ visited.set(obj as object, arrayClone)
83
+ obj.forEach((item, index) => {
84
+ arrayClone[index] = deepClone(item, visited)
85
+ })
86
+ return arrayClone as unknown as T
87
+ }
88
+
89
+ // Handle TypedArrays
90
+ if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
91
+ // Get the constructor to create a new instance of the same type
92
+ const TypedArrayConstructor = Object.getPrototypeOf(obj).constructor
93
+ const clone = new TypedArrayConstructor(
94
+ (obj as unknown as TypedArray).length
95
+ ) as unknown as TypedArray
96
+ visited.set(obj as object, clone)
97
+
98
+ // Copy the values
99
+ for (let i = 0; i < (obj as unknown as TypedArray).length; i++) {
100
+ clone[i] = (obj as unknown as TypedArray)[i]!
101
+ }
102
+
103
+ return clone as unknown as T
104
+ }
105
+
106
+ if (obj instanceof Map) {
107
+ const clone = new Map() as Map<unknown, unknown>
108
+ visited.set(obj as object, clone)
109
+ obj.forEach((value, key) => {
110
+ clone.set(key, deepClone(value, visited))
111
+ })
112
+ return clone as unknown as T
113
+ }
114
+
115
+ if (obj instanceof Set) {
116
+ const clone = new Set()
117
+ visited.set(obj as object, clone)
118
+ obj.forEach((value) => {
119
+ clone.add(deepClone(value, visited))
120
+ })
121
+ return clone as unknown as T
122
+ }
123
+
124
+ const clone = {} as Record<string | symbol, unknown>
125
+ visited.set(obj as object, clone)
126
+
127
+ for (const key in obj) {
128
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
129
+ clone[key] = deepClone(
130
+ (obj as Record<string | symbol, unknown>)[key],
131
+ visited
132
+ )
133
+ }
134
+ }
135
+
136
+ const symbolProps = Object.getOwnPropertySymbols(obj)
137
+ for (const sym of symbolProps) {
138
+ clone[sym] = deepClone(
139
+ (obj as Record<string | symbol, unknown>)[sym],
140
+ visited
141
+ )
142
+ }
143
+
144
+ return clone as T
145
+ }
146
+
147
+ /**
148
+ * Deep equality check that handles special types like Date, RegExp, Map, and Set
149
+ */
150
+ function deepEqual<T>(a: T, b: T): boolean {
151
+ // Handle primitive types
152
+ if (a === b) return true
153
+
154
+ // If either is null or not an object, they're not equal
155
+ if (
156
+ a === null ||
157
+ b === null ||
158
+ typeof a !== `object` ||
159
+ typeof b !== `object`
160
+ ) {
161
+ return false
162
+ }
163
+
164
+ // Handle Date objects
165
+ if (a instanceof Date && b instanceof Date) {
166
+ return a.getTime() === b.getTime()
167
+ }
168
+
169
+ // Handle RegExp objects
170
+ if (a instanceof RegExp && b instanceof RegExp) {
171
+ return a.source === b.source && a.flags === b.flags
172
+ }
173
+
174
+ // Handle Map objects
175
+ if (a instanceof Map && b instanceof Map) {
176
+ if (a.size !== b.size) return false
177
+
178
+ const entries = Array.from(a.entries())
179
+ for (const [key, val] of entries) {
180
+ if (!b.has(key) || !deepEqual(val, b.get(key))) {
181
+ return false
182
+ }
183
+ }
184
+
185
+ return true
186
+ }
187
+
188
+ // Handle Set objects
189
+ if (a instanceof Set && b instanceof Set) {
190
+ if (a.size !== b.size) return false
191
+
192
+ // Convert to arrays for comparison
193
+ const aValues = Array.from(a)
194
+ const bValues = Array.from(b)
195
+
196
+ // Simple comparison for primitive values
197
+ if (aValues.every((val) => typeof val !== `object`)) {
198
+ return aValues.every((val) => b.has(val))
199
+ }
200
+
201
+ // For objects in sets, we need to do a more complex comparison
202
+ // This is a simplified approach and may not work for all cases
203
+ return aValues.length === bValues.length
204
+ }
205
+
206
+ // Handle arrays
207
+ if (Array.isArray(a) && Array.isArray(b)) {
208
+ if (a.length !== b.length) return false
209
+
210
+ for (let i = 0; i < a.length; i++) {
211
+ if (!deepEqual(a[i], b[i])) return false
212
+ }
213
+
214
+ return true
215
+ }
216
+
217
+ // Handle TypedArrays
218
+ if (
219
+ ArrayBuffer.isView(a) &&
220
+ ArrayBuffer.isView(b) &&
221
+ !(a instanceof DataView) &&
222
+ !(b instanceof DataView)
223
+ ) {
224
+ const typedA = a as unknown as TypedArray
225
+ const typedB = b as unknown as TypedArray
226
+ if (typedA.length !== typedB.length) return false
227
+
228
+ for (let i = 0; i < typedA.length; i++) {
229
+ if (typedA[i] !== typedB[i]) return false
230
+ }
231
+
232
+ return true
233
+ }
234
+
235
+ // Handle plain objects
236
+ const keysA = Object.keys(a as object)
237
+ const keysB = Object.keys(b as object)
238
+
239
+ if (keysA.length !== keysB.length) return false
240
+
241
+ return keysA.every(
242
+ (key) =>
243
+ Object.prototype.hasOwnProperty.call(b, key) &&
244
+ deepEqual((a as any)[key], (b as any)[key])
245
+ )
246
+ }
247
+
248
+ /**
249
+ * Creates a proxy that tracks changes to the target object
250
+ *
251
+ * @param target The object to proxy
252
+ * @param parent Optional parent information
253
+ * @returns An object containing the proxy and a function to get the changes
254
+ */
255
+ export function createChangeProxy<T extends object>(
256
+ target: T,
257
+ parent?: { tracker: ChangeTracker<object>; prop: string | symbol }
258
+ ): {
259
+ proxy: T
260
+
261
+ getChanges: () => Record<string | symbol, any>
262
+ } {
263
+ // Create a WeakMap to cache proxies for nested objects
264
+ // This prevents creating multiple proxies for the same object
265
+ // and handles circular references
266
+ const proxyCache = new WeakMap<object, object>()
267
+
268
+ // Create a change tracker to track changes to the object
269
+ const changeTracker: ChangeTracker<T> = {
270
+ changes: {},
271
+ originalObject: deepClone(target), // Create a deep clone to preserve the original state
272
+ modified: false,
273
+ assigned_: {},
274
+ parent,
275
+ target, // Store reference to the target object
276
+ }
277
+
278
+ // Mark this object and all its ancestors as modified
279
+ function markChanged(state: ChangeTracker<object>) {
280
+ if (!state.modified) {
281
+ state.modified = true
282
+
283
+ // Propagate the change up the parent chain
284
+ if (state.parent) {
285
+ markChanged(state.parent.tracker)
286
+ }
287
+ }
288
+ }
289
+
290
+ // Check if all properties in the current state have reverted to original values
291
+ function checkIfReverted(state: ChangeTracker<object>): boolean {
292
+ debugLog(
293
+ `checkIfReverted called with assigned keys:`,
294
+ Object.keys(state.assigned_)
295
+ )
296
+
297
+ // If there are no assigned properties, object is unchanged
298
+ if (
299
+ Object.keys(state.assigned_).length === 0 &&
300
+ Object.getOwnPropertySymbols(state.assigned_).length === 0
301
+ ) {
302
+ debugLog(`No assigned properties, returning true`)
303
+ return true
304
+ }
305
+
306
+ // Check each assigned regular property
307
+ for (const prop in state.assigned_) {
308
+ // If this property is marked as assigned
309
+ if (state.assigned_[prop] === true) {
310
+ const currentValue = state.copy_ ? (state.copy_ as any)[prop] : null
311
+ const originalValue = (state.originalObject as any)[prop]
312
+
313
+ debugLog(
314
+ `Checking property ${String(prop)}, current:`,
315
+ currentValue,
316
+ `original:`,
317
+ originalValue
318
+ )
319
+
320
+ // If the value is not equal to original, something is still changed
321
+ if (!deepEqual(currentValue, originalValue)) {
322
+ debugLog(`Property ${String(prop)} is different, returning false`)
323
+ return false
324
+ }
325
+ } else if (state.assigned_[prop] === false) {
326
+ // Property was deleted, so it's different from original
327
+ debugLog(`Property ${String(prop)} was deleted, returning false`)
328
+ return false
329
+ }
330
+ }
331
+
332
+ // Check each assigned symbol property
333
+ const symbolProps = Object.getOwnPropertySymbols(state.assigned_)
334
+ for (const sym of symbolProps) {
335
+ if (state.assigned_[sym.toString()] === true) {
336
+ const currentValue = state.copy_ ? (state.copy_ as any)[sym] : null
337
+ const originalValue = (state.originalObject as any)[sym]
338
+
339
+ // If the value is not equal to original, something is still changed
340
+ if (!deepEqual(currentValue, originalValue)) {
341
+ debugLog(`Symbol property is different, returning false`)
342
+ return false
343
+ }
344
+ } else if (state.assigned_[sym.toString()] === false) {
345
+ // Property was deleted, so it's different from original
346
+ debugLog(`Symbol property was deleted, returning false`)
347
+ return false
348
+ }
349
+ }
350
+
351
+ debugLog(`All properties match original values, returning true`)
352
+ // All assigned properties match their original values
353
+ return true
354
+ }
355
+
356
+ // Recursively check and update modified status based on child objects
357
+ function updateModifiedStatus(state: ChangeTracker<object>): boolean {
358
+ debugLog(
359
+ `updateModifiedStatus called, assigned keys:`,
360
+ Object.keys(state.assigned_)
361
+ )
362
+
363
+ // Only check for reverts if we actually have changes
364
+ if (
365
+ Object.keys(state.assigned_).length === 0 &&
366
+ Object.getOwnPropertySymbols(state.assigned_).length === 0
367
+ ) {
368
+ debugLog(`No assigned properties, returning false`)
369
+ return false
370
+ }
371
+
372
+ // If this object has direct changes that aren't reverted, it's modified
373
+ const isReverted = checkIfReverted(state)
374
+ debugLog(`checkIfReverted returned:`, isReverted)
375
+
376
+ if (!isReverted) {
377
+ debugLog(`Object has changes that aren't reverted, returning true`)
378
+ return true
379
+ }
380
+
381
+ debugLog(`All changes reverted, clearing tracking`)
382
+ // All changes have been reverted, clear the tracking
383
+ state.modified = false
384
+ state.changes = {}
385
+ state.assigned_ = {}
386
+
387
+ // If we have a parent, update its status too
388
+ if (state.parent) {
389
+ debugLog(`Checking parent status for prop:`, state.parent.prop)
390
+ // Tell the parent this child has reverted
391
+ checkParentStatus(state.parent.tracker, state.parent.prop)
392
+ }
393
+
394
+ return false
395
+ }
396
+
397
+ // Update parent status based on child changes
398
+ function checkParentStatus(
399
+ parentState: ChangeTracker<object>,
400
+ childProp: string | symbol
401
+ ) {
402
+ debugLog(`checkParentStatus called for child prop:`, childProp)
403
+
404
+ // Check if all properties of the parent are reverted
405
+ const isReverted = checkIfReverted(parentState)
406
+ debugLog(`Parent checkIfReverted returned:`, isReverted)
407
+
408
+ if (isReverted) {
409
+ debugLog(`Parent is fully reverted, clearing tracking`)
410
+ // If everything is reverted, clear the tracking
411
+ parentState.modified = false
412
+ parentState.changes = {}
413
+ parentState.assigned_ = {}
414
+
415
+ // Continue up the chain
416
+ if (parentState.parent) {
417
+ debugLog(`Continuing up the parent chain`)
418
+ checkParentStatus(parentState.parent.tracker, parentState.parent.prop)
419
+ }
420
+ }
421
+ }
422
+
423
+ // Create a proxy for the target object
424
+ function createObjectProxy<TObj extends object>(obj: TObj): TObj {
425
+ // If we've already created a proxy for this object, return it
426
+ if (proxyCache.has(obj)) {
427
+ return proxyCache.get(obj) as TObj
428
+ }
429
+
430
+ // Create a proxy for the object
431
+ const proxy = new Proxy(obj, {
432
+ get(ptarget, prop) {
433
+ const value = ptarget[prop as keyof TObj]
434
+
435
+ // If it's a getter, return the value directly
436
+ const desc = Object.getOwnPropertyDescriptor(ptarget, prop)
437
+ if (desc?.get) {
438
+ return value
439
+ }
440
+
441
+ // If the value is a function, bind it to the ptarget
442
+ if (typeof value === `function`) {
443
+ // For Map and Set methods that modify the collection
444
+ if (ptarget instanceof Map || ptarget instanceof Set) {
445
+ const methodName = prop.toString()
446
+ const modifyingMethods = new Set([
447
+ `set`,
448
+ `delete`,
449
+ `clear`,
450
+ `add`,
451
+ `pop`,
452
+ `push`,
453
+ `shift`,
454
+ `unshift`,
455
+ `splice`,
456
+ `sort`,
457
+ `reverse`,
458
+ ])
459
+
460
+ if (modifyingMethods.has(methodName)) {
461
+ return function (...args: Array<unknown>) {
462
+ const result = value.apply(ptarget, args)
463
+ markChanged(changeTracker)
464
+ return result
465
+ }
466
+ }
467
+
468
+ // Handle iterator methods for Map and Set
469
+ const iteratorMethods = new Set([
470
+ `entries`,
471
+ `keys`,
472
+ `values`,
473
+ `forEach`,
474
+ Symbol.iterator,
475
+ ])
476
+
477
+ if (iteratorMethods.has(methodName) || prop === Symbol.iterator) {
478
+ return function (this: unknown, ...args: Array<unknown>) {
479
+ const result = value.apply(ptarget, args)
480
+
481
+ // For forEach, we need to wrap the callback to track changes
482
+ if (methodName === `forEach`) {
483
+ const callback = args[0]
484
+ if (typeof callback === `function`) {
485
+ // Replace the original callback with our wrapped version
486
+ const wrappedCallback = function (
487
+ // eslint-disable-next-line
488
+ this: unknown,
489
+ // eslint-disable-next-line
490
+ value: unknown,
491
+ key: unknown,
492
+ collection: unknown
493
+ ) {
494
+ // Call the original callback
495
+ const cbresult = callback.call(
496
+ this,
497
+ value,
498
+ key,
499
+ collection
500
+ )
501
+ // Mark as changed since the callback might have modified the value
502
+ markChanged(changeTracker)
503
+ return cbresult
504
+ }
505
+ // Call forEach with our wrapped callback
506
+ return value.apply(ptarget, [
507
+ wrappedCallback,
508
+ ...args.slice(1),
509
+ ])
510
+ }
511
+ }
512
+
513
+ // For iterators (entries, keys, values, Symbol.iterator)
514
+ if (
515
+ methodName === `entries` ||
516
+ methodName === `values` ||
517
+ methodName === Symbol.iterator.toString() ||
518
+ prop === Symbol.iterator
519
+ ) {
520
+ // If it's an iterator, we need to wrap the returned iterator
521
+ // to track changes when the values are accessed and potentially modified
522
+ const originalIterator = result
523
+
524
+ // Create a proxy for the iterator that will mark changes when next() is called
525
+ return {
526
+ next() {
527
+ const nextResult = originalIterator.next()
528
+
529
+ // If we have a value and it's an object, we need to track it
530
+ if (
531
+ !nextResult.done &&
532
+ nextResult.value &&
533
+ typeof nextResult.value === `object`
534
+ ) {
535
+ // For entries, the value is a [key, value] pair
536
+ if (
537
+ methodName === `entries` &&
538
+ Array.isArray(nextResult.value) &&
539
+ nextResult.value.length === 2
540
+ ) {
541
+ // The value is at index 1 in the [key, value] pair
542
+ if (
543
+ nextResult.value[1] &&
544
+ typeof nextResult.value[1] === `object`
545
+ ) {
546
+ // Create a proxy for the value and replace it in the result
547
+ const { proxy: valueProxy } = createChangeProxy(
548
+ nextResult.value[1],
549
+ {
550
+ tracker: changeTracker,
551
+ prop:
552
+ typeof nextResult.value[0] === `symbol`
553
+ ? nextResult.value[0]
554
+ : String(nextResult.value[0]),
555
+ }
556
+ )
557
+ nextResult.value[1] = valueProxy
558
+ }
559
+ } else if (
560
+ methodName === `values` ||
561
+ methodName === Symbol.iterator.toString() ||
562
+ prop === Symbol.iterator
563
+ ) {
564
+ // If the value is an object, create a proxy for it
565
+ if (
566
+ typeof nextResult.value === `object` &&
567
+ nextResult.value !== null
568
+ ) {
569
+ // For Set, we need to track the whole object
570
+ // For Map, we would need the key, but we don't have it here
571
+ // So we'll use a symbol as a placeholder
572
+ const tempKey = Symbol(`iterator-value`)
573
+ const { proxy: valueProxy } = createChangeProxy(
574
+ nextResult.value,
575
+ {
576
+ tracker: changeTracker,
577
+ prop: tempKey,
578
+ }
579
+ )
580
+ nextResult.value = valueProxy
581
+ }
582
+ }
583
+ }
584
+
585
+ return nextResult
586
+ },
587
+ [Symbol.iterator]() {
588
+ return this
589
+ },
590
+ }
591
+ }
592
+
593
+ return result
594
+ }
595
+ }
596
+ }
597
+ return value.bind(ptarget)
598
+ }
599
+
600
+ // If the value is an object, create a proxy for it
601
+ if (
602
+ value &&
603
+ typeof value === `object` &&
604
+ !(value instanceof Date) &&
605
+ !(value instanceof RegExp)
606
+ ) {
607
+ // Create a parent reference for the nested object
608
+ const nestedParent = {
609
+ tracker: changeTracker,
610
+ prop: String(prop),
611
+ }
612
+
613
+ // Create a proxy for the nested object
614
+ const { proxy: nestedProxy } = createChangeProxy(value, nestedParent)
615
+
616
+ // Cache the proxy
617
+ proxyCache.set(value, nestedProxy)
618
+
619
+ return nestedProxy
620
+ }
621
+
622
+ return value
623
+ },
624
+
625
+ set(sobj, prop, value) {
626
+ const currentValue = sobj[prop as keyof TObj]
627
+ debugLog(
628
+ `set called for property ${String(prop)}, current:`,
629
+ currentValue,
630
+ `new:`,
631
+ value
632
+ )
633
+
634
+ // Special handling for array length changes
635
+ if (Array.isArray(sobj) && prop === `length`) {
636
+ const newLength = Number(value)
637
+ const oldLength = sobj.length
638
+
639
+ // Create a new array with the desired length
640
+ const newArray = Array.from({ length: newLength }, (_, i) =>
641
+ i < oldLength ? sobj[i] : undefined
642
+ )
643
+
644
+ // Track the change in the parent object since 'arr' is the property name
645
+ if (parent) {
646
+ parent.tracker.changes[parent.prop] = newArray
647
+ parent.tracker.assigned_[parent.prop] = true
648
+ markChanged(parent.tracker)
649
+ }
650
+
651
+ // Update the original array
652
+ sobj.length = newLength
653
+ return true
654
+ }
655
+
656
+ // Only track the change if the value is actually different
657
+ if (!deepEqual(currentValue, value)) {
658
+ // Check if the new value is equal to the original value
659
+ // Important: Use the originalObject to get the true original value
660
+ const originalValue = changeTracker.originalObject[prop as keyof T]
661
+ const isRevertToOriginal = deepEqual(value, originalValue)
662
+ debugLog(
663
+ `Value different, original:`,
664
+ originalValue,
665
+ `isRevertToOriginal:`,
666
+ isRevertToOriginal
667
+ )
668
+
669
+ if (isRevertToOriginal) {
670
+ debugLog(`Reverting property ${String(prop)} to original value`)
671
+ // If the value is reverted to its original state, remove it from changes
672
+ delete changeTracker.changes[prop.toString()]
673
+ delete changeTracker.assigned_[prop.toString()]
674
+
675
+ // Make sure the copy is updated with the original value
676
+ if (changeTracker.copy_) {
677
+ debugLog(`Updating copy with original value for ${String(prop)}`)
678
+ changeTracker.copy_[prop as keyof T] = deepClone(originalValue)
679
+ }
680
+
681
+ // Check if all properties in this object have been reverted
682
+ debugLog(`Checking if all properties reverted`)
683
+ const allReverted = checkIfReverted(changeTracker)
684
+ debugLog(`All reverted:`, allReverted)
685
+
686
+ if (allReverted) {
687
+ debugLog(`All properties reverted, clearing tracking`)
688
+ // If all have been reverted, clear tracking
689
+ changeTracker.modified = false
690
+ changeTracker.changes = {}
691
+ changeTracker.assigned_ = {}
692
+
693
+ // If we're a nested object, check if the parent needs updating
694
+ if (parent) {
695
+ debugLog(`Updating parent for property:`, parent.prop)
696
+ checkParentStatus(parent.tracker, parent.prop)
697
+ }
698
+ } else {
699
+ // Some properties are still changed
700
+ debugLog(`Some properties still changed, keeping modified flag`)
701
+ changeTracker.modified = true
702
+ }
703
+ } else {
704
+ debugLog(`Setting new value for property ${String(prop)}`)
705
+ // Create a copy of the object if it doesn't exist
706
+ prepareCopy(changeTracker)
707
+
708
+ // Set the value on the copy
709
+ if (changeTracker.copy_) {
710
+ changeTracker.copy_[prop as keyof T] = value
711
+ }
712
+
713
+ // Set the value on the original object
714
+ obj[prop as keyof TObj] = value
715
+
716
+ // Track that this property was assigned - store using the actual property (symbol or string)
717
+ changeTracker.assigned_[prop.toString()] = true
718
+
719
+ // Track the change directly with the property as the key
720
+ changeTracker.changes[prop.toString()] = deepClone(value)
721
+
722
+ // Mark this object and its ancestors as modified
723
+ debugLog(`Marking object and ancestors as modified`)
724
+ markChanged(changeTracker)
725
+ }
726
+ } else {
727
+ debugLog(`Value unchanged, not tracking`)
728
+ }
729
+
730
+ return true
731
+ },
732
+
733
+ defineProperty(ptarget, prop, descriptor) {
734
+ const result = Reflect.defineProperty(ptarget, prop, descriptor)
735
+ if (result) {
736
+ // Track the change if the property has a value
737
+ if (`value` in descriptor) {
738
+ changeTracker.changes[prop.toString()] = deepClone(descriptor.value)
739
+ changeTracker.assigned_[prop.toString()] = true
740
+ markChanged(changeTracker)
741
+ }
742
+ }
743
+ return result
744
+ },
745
+
746
+ setPrototypeOf(ptarget, proto) {
747
+ // Allow setting prototype but don't track it as a change
748
+ return Object.setPrototypeOf(ptarget, proto)
749
+ },
750
+
751
+ deleteProperty(dobj, prop) {
752
+ const stringProp = typeof prop === `symbol` ? prop.toString() : prop
753
+
754
+ if (stringProp in dobj) {
755
+ // Check if the property exists in the original object
756
+ const hadPropertyInOriginal =
757
+ stringProp in changeTracker.originalObject
758
+
759
+ // Create a copy of the object if it doesn't exist
760
+ prepareCopy(changeTracker)
761
+
762
+ // Delete the property from the copy
763
+ if (changeTracker.copy_) {
764
+ // Use type assertion to tell TypeScript this is allowed
765
+ delete (changeTracker.copy_ as Record<string | symbol, unknown>)[
766
+ prop
767
+ ]
768
+ }
769
+
770
+ // Delete the property from the original object
771
+ // Use type assertion to tell TypeScript this is allowed
772
+ delete (dobj as Record<string | symbol, unknown>)[prop]
773
+
774
+ // If the property didn't exist in the original object, removing it
775
+ // should revert to the original state
776
+ if (!hadPropertyInOriginal) {
777
+ delete changeTracker.changes[stringProp]
778
+ delete changeTracker.assigned_[stringProp]
779
+
780
+ // If this is the last change and we're not a nested object,
781
+ // mark the object as unmodified
782
+ if (
783
+ Object.keys(changeTracker.assigned_).length === 0 &&
784
+ Object.getOwnPropertySymbols(changeTracker.assigned_).length === 0
785
+ ) {
786
+ changeTracker.modified = false
787
+ } else {
788
+ // We still have changes, keep as modified
789
+ changeTracker.modified = true
790
+ }
791
+ } else {
792
+ // Mark this property as deleted
793
+ changeTracker.assigned_[stringProp] = false
794
+ changeTracker.changes[stringProp] = undefined
795
+ markChanged(changeTracker)
796
+ }
797
+ }
798
+
799
+ return true
800
+ },
801
+ })
802
+
803
+ // Cache the proxy
804
+ proxyCache.set(obj, proxy)
805
+
806
+ return proxy
807
+ }
808
+
809
+ // Create a proxy for the target object
810
+ const proxy = createObjectProxy(target)
811
+
812
+ // Return the proxy and a function to get the changes
813
+ return {
814
+ proxy,
815
+ getChanges: () => {
816
+ debugLog(
817
+ `getChanges called, modified:`,
818
+ changeTracker.modified,
819
+ `assigned keys:`,
820
+ Object.keys(changeTracker.assigned_)
821
+ )
822
+
823
+ // First, check if the object is still considered modified
824
+ if (!changeTracker.modified) {
825
+ debugLog(`Object not modified, returning empty object`)
826
+ return {}
827
+ }
828
+
829
+ // For deeply nested changes, we need to verify explicitly
830
+ if (
831
+ Object.keys(changeTracker.assigned_).length === 0 &&
832
+ Object.getOwnPropertySymbols(changeTracker.assigned_).length === 0
833
+ ) {
834
+ debugLog(`No assigned properties, checking deep equality`)
835
+
836
+ // If there are no assigned properties but the object is still marked as modified,
837
+ // we should check deep equality with the original object
838
+ if (changeTracker.copy_) {
839
+ debugLog(`Comparing copy with original`)
840
+ if (deepEqual(changeTracker.copy_, changeTracker.originalObject)) {
841
+ debugLog(`Copy equals original, returning empty object`)
842
+ changeTracker.modified = false
843
+ return {}
844
+ }
845
+ } else if (deepEqual(target, changeTracker.originalObject)) {
846
+ debugLog(`Target equals original, returning empty object`)
847
+ changeTracker.modified = false
848
+ changeTracker.changes = {}
849
+ changeTracker.assigned_ = {}
850
+ return {}
851
+ }
852
+ }
853
+
854
+ debugLog(`Forcing full check for reverted state`)
855
+ // Force a full check for reverted state, which will update the modified flag accordingly
856
+ updateModifiedStatus(changeTracker)
857
+
858
+ // If we're no longer modified after the check, return empty changes
859
+ // eslint-disable-next-line
860
+ if (!changeTracker.modified) {
861
+ debugLog(`No longer modified after check, returning empty object`)
862
+ return {}
863
+ }
864
+
865
+ // Handle optimization case - if the object is marked modified but actually is equal to original
866
+ // eslint-disable-next-line
867
+ if (changeTracker.modified) {
868
+ const objToCheck = changeTracker.copy_ || target
869
+ debugLog(
870
+ `Checking if object is equal to original:`,
871
+ objToCheck,
872
+ changeTracker.originalObject
873
+ )
874
+ if (deepEqual(objToCheck, changeTracker.originalObject)) {
875
+ debugLog(`Object equals original, returning empty object`)
876
+ changeTracker.modified = false
877
+ changeTracker.changes = {}
878
+ changeTracker.assigned_ = {}
879
+ return {}
880
+ }
881
+ }
882
+
883
+ // If there are assigned properties, return the changes
884
+ if (
885
+ Object.keys(changeTracker.assigned_).length > 0 ||
886
+ Object.getOwnPropertySymbols(changeTracker.assigned_).length > 0
887
+ ) {
888
+ // If we have a copy, use it to construct the changes
889
+ if (changeTracker.copy_) {
890
+ const changes: Record<string | symbol, unknown> = {}
891
+
892
+ // Add all assigned properties
893
+ for (const key in changeTracker.assigned_) {
894
+ if (changeTracker.assigned_[key] === true) {
895
+ // Property was assigned
896
+ changes[key] = deepClone(changeTracker.copy_[key as keyof T])
897
+ } else if (changeTracker.assigned_[key] === false) {
898
+ // Property was deleted
899
+ changes[key] = undefined
900
+ }
901
+ }
902
+
903
+ // Handle symbol properties - this needs special handling
904
+ const symbolProps = Object.getOwnPropertySymbols(
905
+ changeTracker.assigned_
906
+ )
907
+ for (const sym of symbolProps) {
908
+ if (changeTracker.assigned_[sym.toString()] === true) {
909
+ // Use the symbol directly instead of its string representation
910
+
911
+ const value = (changeTracker.copy_ as any)[sym]
912
+
913
+ changes[sym.toString()] = deepClone(value)
914
+ }
915
+ }
916
+
917
+ return changes
918
+ }
919
+
920
+ // Fall back to the existing changes object if no copy exists
921
+ return changeTracker.changes
922
+ }
923
+
924
+ // If the object is modified but has no direct changes (nested changes),
925
+ // but we're the root object, recursively check if unknown changes exist
926
+ // eslint-disable-next-line
927
+ if (changeTracker.modified && !parent) {
928
+ debugLog(`Root object with nested changes, checking deep equality`)
929
+
930
+ const currentState = changeTracker.copy_ || (target as any)
931
+
932
+ debugLog(
933
+ `Comparing current state with original:`,
934
+ currentState,
935
+ changeTracker.originalObject
936
+ )
937
+ if (deepEqual(currentState, changeTracker.originalObject)) {
938
+ // The entire object has been reverted to its original state
939
+ debugLog(`Current state equals original, returning empty object`)
940
+ changeTracker.modified = false
941
+ return {}
942
+ }
943
+
944
+ // One more deep check - compare the actual values
945
+ // This is needed for the case where nested properties are modified and then reverted
946
+ debugLog(
947
+ `Comparing target with original:`,
948
+ target,
949
+ changeTracker.originalObject
950
+ )
951
+ if (deepEqual(target, changeTracker.originalObject)) {
952
+ debugLog(`Target equals original, returning empty object`)
953
+ changeTracker.modified = false
954
+ changeTracker.changes = {}
955
+ changeTracker.assigned_ = {}
956
+ return {}
957
+ }
958
+
959
+ // Special case for nested object reverts
960
+ // If we're here, we need to check if the nested objects have been reverted
961
+ // even if the parent object still shows as modified
962
+ // eslint-disable-next-line
963
+ if (typeof target === `object` && target !== null) {
964
+ let allNestedReverted = true
965
+
966
+ // Check each property to see if it's been reverted to original
967
+ for (const key in target) {
968
+ if (Object.prototype.hasOwnProperty.call(target, key)) {
969
+ const currentValue = target[key]
970
+ const originalValue = changeTracker.originalObject[key as keyof T]
971
+
972
+ // If this property is different from original, not all are reverted
973
+ if (!deepEqual(currentValue, originalValue)) {
974
+ allNestedReverted = false
975
+ break
976
+ }
977
+ }
978
+ }
979
+
980
+ // If all nested properties match original values, return empty changes
981
+ if (allNestedReverted) {
982
+ debugLog(
983
+ `All nested properties match original values, returning empty object`
984
+ )
985
+ changeTracker.modified = false
986
+ changeTracker.changes = {}
987
+ changeTracker.assigned_ = {}
988
+ return {}
989
+ }
990
+ }
991
+
992
+ debugLog(
993
+ `Changes detected, returning full object:`,
994
+ changeTracker.copy_ || target
995
+ )
996
+ // Convert the copy or target to a Record type before returning
997
+ const result = changeTracker.copy_ || target
998
+ return result as unknown as Record<string | symbol, unknown>
999
+ }
1000
+
1001
+ // No changes
1002
+ debugLog(`No changes detected, returning empty object`)
1003
+ return {}
1004
+ },
1005
+ }
1006
+ }
1007
+
1008
+ /**
1009
+ * Creates proxies for an array of objects and tracks changes to each
1010
+ *
1011
+ * @param targets Array of objects to proxy
1012
+ * @returns An object containing the array of proxies and a function to get all changes
1013
+ */
1014
+ export function createArrayChangeProxy<T extends object>(
1015
+ targets: Array<T>
1016
+ ): {
1017
+ proxies: Array<T>
1018
+ getChanges: () => Array<Record<string | symbol, unknown>>
1019
+ } {
1020
+ const proxiesWithChanges = targets.map((target) => createChangeProxy(target))
1021
+
1022
+ return {
1023
+ proxies: proxiesWithChanges.map((p) => p.proxy),
1024
+ getChanges: () => proxiesWithChanges.map((p) => p.getChanges()),
1025
+ }
1026
+ }
1027
+
1028
+ /**
1029
+ * Creates a proxy for an object, passes it to a callback function,
1030
+ * and returns the changes made by the callback
1031
+ *
1032
+ * @param target The object to proxy
1033
+ * @param callback Function that receives the proxy and can make changes to it
1034
+ * @returns The changes made to the object
1035
+ */
1036
+ export function withChangeTracking<T extends object>(
1037
+ target: T,
1038
+ callback: (proxy: T) => void
1039
+ ): Record<string | symbol, unknown> {
1040
+ const { proxy, getChanges } = createChangeProxy(target)
1041
+
1042
+ callback(proxy)
1043
+
1044
+ return getChanges()
1045
+ }
1046
+
1047
+ /**
1048
+ * Creates proxies for an array of objects, passes them to a callback function,
1049
+ * and returns the changes made by the callback for each object
1050
+ *
1051
+ * @param targets Array of objects to proxy
1052
+ * @param callback Function that receives the proxies and can make changes to them
1053
+ * @returns Array of changes made to each object
1054
+ */
1055
+ export function withArrayChangeTracking<T extends object>(
1056
+ targets: Array<T>,
1057
+ callback: (proxies: Array<T>) => void
1058
+ ): Array<Record<string | symbol, unknown>> {
1059
+ const { proxies, getChanges } = createArrayChangeProxy(targets)
1060
+
1061
+ callback(proxies)
1062
+
1063
+ return getChanges()
1064
+ }
1065
+
1066
+ /**
1067
+ * Creates a shallow copy of the target object if it doesn't already exist
1068
+ */
1069
+ function prepareCopy<T extends object>(state: ChangeTracker<T>) {
1070
+ if (!state.copy_) {
1071
+ state.copy_ = shallowCopy(state.originalObject)
1072
+ }
1073
+ }
1074
+
1075
+ /**
1076
+ * Creates a shallow copy of an object
1077
+ */
1078
+ function shallowCopy<T>(obj: T): T {
1079
+ if (Array.isArray(obj)) {
1080
+ return [...obj] as unknown as T
1081
+ }
1082
+
1083
+ if (obj instanceof Map) {
1084
+ return new Map(obj) as unknown as T
1085
+ }
1086
+
1087
+ if (obj instanceof Set) {
1088
+ return new Set(obj) as unknown as T
1089
+ }
1090
+
1091
+ if (obj instanceof Date) {
1092
+ return new Date(obj.getTime()) as unknown as T
1093
+ }
1094
+
1095
+ if (obj instanceof RegExp) {
1096
+ return new RegExp(obj.source, obj.flags) as unknown as T
1097
+ }
1098
+
1099
+ if (obj !== null && typeof obj === `object`) {
1100
+ return { ...obj } as T
1101
+ }
1102
+
1103
+ return obj
1104
+ }