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.
Files changed (38) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +368 -277
  3. package/dist/state-sync-log.esm.js +929 -136
  4. package/dist/state-sync-log.esm.mjs +929 -136
  5. package/dist/state-sync-log.umd.js +928 -135
  6. package/dist/types/createOps/constant.d.ts +6 -0
  7. package/dist/types/createOps/createOps.d.ts +25 -0
  8. package/dist/types/createOps/current.d.ts +13 -0
  9. package/dist/types/createOps/draft.d.ts +14 -0
  10. package/dist/types/createOps/draftify.d.ts +5 -0
  11. package/dist/types/createOps/index.d.ts +12 -0
  12. package/dist/types/createOps/interface.d.ts +74 -0
  13. package/dist/types/createOps/original.d.ts +15 -0
  14. package/dist/types/createOps/pushOp.d.ts +9 -0
  15. package/dist/types/createOps/setHelpers.d.ts +25 -0
  16. package/dist/types/createOps/utils.d.ts +95 -0
  17. package/dist/types/draft.d.ts +2 -2
  18. package/dist/types/index.d.ts +1 -0
  19. package/dist/types/json.d.ts +1 -1
  20. package/dist/types/operations.d.ts +2 -2
  21. package/dist/types/utils.d.ts +5 -0
  22. package/package.json +1 -1
  23. package/src/createOps/constant.ts +10 -0
  24. package/src/createOps/createOps.ts +97 -0
  25. package/src/createOps/current.ts +85 -0
  26. package/src/createOps/draft.ts +606 -0
  27. package/src/createOps/draftify.ts +45 -0
  28. package/src/createOps/index.ts +18 -0
  29. package/src/createOps/interface.ts +95 -0
  30. package/src/createOps/original.ts +24 -0
  31. package/src/createOps/pushOp.ts +42 -0
  32. package/src/createOps/setHelpers.ts +93 -0
  33. package/src/createOps/utils.ts +325 -0
  34. package/src/draft.ts +306 -288
  35. package/src/index.ts +1 -0
  36. package/src/json.ts +1 -1
  37. package/src/operations.ts +33 -11
  38. package/src/utils.ts +67 -55
package/src/operations.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { failure } from "./error"
2
2
  import { JSONObject, JSONValue, Path } from "./json"
3
- import { deepClone, deepEqual, isObject } from "./utils"
3
+ import { deepClone, deepEqual, isObject, parseArrayIndex } from "./utils"
4
4
 
5
5
  /**
6
6
  * Supported operations.
7
7
  * Applied sequentially within a tx.
8
8
  */
9
9
  export type Op =
10
- | { kind: "set"; path: Path; key: string; value: JSONValue }
11
- | { kind: "delete"; path: Path; key: string }
10
+ | { kind: "set"; path: Path; key: string | number; value: JSONValue }
11
+ | { kind: "delete"; path: Path; key: string | number }
12
12
  | { kind: "splice"; path: Path; index: number; deleteCount: number; inserts: JSONValue[] }
13
13
  | { kind: "addToSet"; path: Path; value: JSONValue }
14
14
  | { kind: "deleteFromSet"; path: Path; value: JSONValue }
@@ -72,21 +72,43 @@ function applyOp(state: JSONObject, op: Op, cloneValues: boolean): void {
72
72
  const container = resolvePath(state, op.path)
73
73
 
74
74
  switch (op.kind) {
75
- case "set":
76
- if (!isObject(container) || Array.isArray(container)) {
77
- failure("set requires object container")
75
+ case "set": {
76
+ // Allow object or array container
77
+ if (!isObject(container)) {
78
+ failure("set requires object or array container")
79
+ }
80
+ let key: string | number = op.key
81
+ // For arrays, convert string keys to numbers (except "length")
82
+ if (Array.isArray(container) && typeof key === "string" && key !== "length") {
83
+ const numKey = parseArrayIndex(key)
84
+ if (numKey === null) {
85
+ failure(`Cannot set non-numeric property "${key}" on array`)
86
+ }
87
+ key = numKey
78
88
  }
79
89
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
- ;(container as any)[op.key] = cloneValues ? deepClone(op.value) : op.value
90
+ ;(container as any)[key] = cloneValues ? deepClone(op.value) : op.value
81
91
  break
92
+ }
82
93
 
83
- case "delete":
84
- if (!isObject(container) || Array.isArray(container)) {
85
- failure("delete requires object container")
94
+ case "delete": {
95
+ // Allow object or array (sparse delete?)
96
+ if (!isObject(container)) {
97
+ failure("delete requires object or array container")
98
+ }
99
+ let key: string | number = op.key
100
+ // For arrays, convert string keys to numbers
101
+ if (Array.isArray(container) && typeof key === "string") {
102
+ const numKey = parseArrayIndex(key)
103
+ if (numKey === null) {
104
+ failure(`Cannot delete non-numeric property "${key}" from array`)
105
+ }
106
+ key = numKey
86
107
  }
87
108
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
- delete (container as any)[op.key]
109
+ delete (container as any)[key]
89
110
  break
111
+ }
90
112
 
91
113
  case "splice": {
92
114
  if (!Array.isArray(container)) {
package/src/utils.ts CHANGED
@@ -1,55 +1,67 @@
1
- import equal from "fast-deep-equal"
2
- import { nanoid } from "nanoid"
3
- import rfdc from "rfdc"
4
- import type { JSONValue } from "./json"
5
-
6
- const clone = rfdc({ proto: true })
7
-
8
- /**
9
- * Deep equality check for JSONValues.
10
- * Used for addToSet / deleteFromSet operations.
11
- */
12
- export function deepEqual(a: JSONValue, b: JSONValue): boolean {
13
- return equal(a, b)
14
- }
15
-
16
- /**
17
- * Generates a unique ID using nanoid.
18
- */
19
- export function generateID(): string {
20
- return nanoid()
21
- }
22
-
23
- /**
24
- * Checks if a value is an object (typeof === "object" && !== null).
25
- */
26
- export function isObject(value: unknown): value is object {
27
- return value !== null && typeof value === "object"
28
- }
29
-
30
- /**
31
- * Deep clones a JSON-serializable value.
32
- * Optimized: primitives are returned as-is.
33
- */
34
- export function deepClone<T>(value: T): T {
35
- // Primitives don't need cloning
36
- if (value === null || typeof value !== "object") {
37
- return value
38
- }
39
- return clone(value)
40
- }
41
-
42
- /**
43
- * Creates a lazy memoized getter.
44
- */
45
- export function lazy<T>(fn: () => T): () => T {
46
- let computed = false
47
- let value: T
48
- return () => {
49
- if (!computed) {
50
- value = fn()
51
- computed = true
52
- }
53
- return value
54
- }
55
- }
1
+ import equal from "fast-deep-equal"
2
+ import { nanoid } from "nanoid"
3
+ import rfdc from "rfdc"
4
+ import type { JSONValue } from "./json"
5
+
6
+ const clone = rfdc({ proto: true })
7
+
8
+ /**
9
+ * Deep equality check for JSONValues.
10
+ * Used for addToSet / deleteFromSet operations.
11
+ */
12
+ export function deepEqual(a: JSONValue, b: JSONValue): boolean {
13
+ return equal(a, b)
14
+ }
15
+
16
+ /**
17
+ * Generates a unique ID using nanoid.
18
+ */
19
+ export function generateID(): string {
20
+ return nanoid()
21
+ }
22
+
23
+ /**
24
+ * Checks if a value is an object (typeof === "object" && !== null).
25
+ */
26
+ export function isObject(value: unknown): value is object {
27
+ return value !== null && typeof value === "object"
28
+ }
29
+
30
+ /**
31
+ * Deep clones a JSON-serializable value.
32
+ * Optimized: primitives are returned as-is.
33
+ */
34
+ export function deepClone<T>(value: T): T {
35
+ // Primitives don't need cloning
36
+ if (value === null || typeof value !== "object") {
37
+ return value
38
+ }
39
+ return clone(value)
40
+ }
41
+
42
+ /**
43
+ * Creates a lazy memoized getter.
44
+ */
45
+ export function lazy<T>(fn: () => T): () => T {
46
+ let computed = false
47
+ let value: T
48
+ return () => {
49
+ if (!computed) {
50
+ value = fn()
51
+ computed = true
52
+ }
53
+ return value
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Checks if a string is a valid non-negative integer array index.
59
+ * Returns the numeric value if valid, or null if invalid.
60
+ */
61
+ export function parseArrayIndex(key: string): number | null {
62
+ const n = Number(key)
63
+ if (!Number.isInteger(n) || n < 0) {
64
+ return null
65
+ }
66
+ return n
67
+ }