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/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
|
-
|
|
77
|
-
|
|
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)[
|
|
90
|
+
;(container as any)[key] = cloneValues ? deepClone(op.value) : op.value
|
|
81
91
|
break
|
|
92
|
+
}
|
|
82
93
|
|
|
83
|
-
case "delete":
|
|
84
|
-
|
|
85
|
-
|
|
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)[
|
|
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
|
+
}
|