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
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Constants for createOps.
3
+ * Simplified from original source - removed dataTypes (mark feature).
4
+ */
5
+ export declare const PROXY_DRAFT: unique symbol;
6
+ export declare const iteratorSymbol: typeof Symbol.iterator;
@@ -0,0 +1,25 @@
1
+ import { CreateOpsResult, Draft } from './interface';
2
+ /**
3
+ * Create operations from mutable-style mutations.
4
+ *
5
+ * @param base - The base state (will not be mutated)
6
+ * @param mutate - A function that mutates the draft
7
+ * @returns An object containing the next state and the operations performed
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const state = { list: [{ text: 'Learn', done: false }] };
12
+ *
13
+ * const { nextState, ops } = createOps(state, (draft) => {
14
+ * draft.list[0].done = true;
15
+ * draft.list.push({ text: 'Practice', done: false });
16
+ * });
17
+ *
18
+ * // ops contains the operations that were performed:
19
+ * // [
20
+ * // { kind: 'set', path: ['list', 0], key: 'done', value: true },
21
+ * // { kind: 'splice', path: ['list'], index: 1, deleteCount: 0, inserts: [{ text: 'Practice', done: false }] }
22
+ * // ]
23
+ * ```
24
+ */
25
+ export declare function createOps<T extends object>(base: T, mutate: (draft: Draft<T>) => void): CreateOpsResult<T>;
@@ -0,0 +1,13 @@
1
+ import { Draft } from './interface';
2
+ /**
3
+ * `current(draft)` to get current state in the draft mutation function.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const { nextState, ops } = createOps(baseState, (draft) => {
8
+ * draft.foo.bar = 'new value';
9
+ * console.log(current(draft.foo)); // { bar: 'new value' }
10
+ * });
11
+ * ```
12
+ */
13
+ export declare function current<T extends object>(target: Draft<T>): T;
@@ -0,0 +1,14 @@
1
+ import { Finalities, Op, ProxyDraft } from './interface';
2
+ /**
3
+ * Create a draft proxy for a value
4
+ */
5
+ export declare function createDraft<T extends object>(options: {
6
+ original: T;
7
+ parentDraft?: ProxyDraft | null;
8
+ key?: string | number;
9
+ finalities: Finalities;
10
+ }): T;
11
+ /**
12
+ * Finalize a draft and return the result with ops
13
+ */
14
+ export declare function finalizeDraft<T>(result: T, returnedValue: [T] | []): [T, Op[]];
@@ -0,0 +1,5 @@
1
+ import { Op } from './interface';
2
+ /**
3
+ * Create a draft and return a finalize function
4
+ */
5
+ export declare function draftify<T extends object>(baseState: T): [T, (returnedValue: [T] | []) => [T, Op[]]];
@@ -0,0 +1,12 @@
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
+ export { createOps } from './createOps';
8
+ export { current } from './current';
9
+ export type { CreateOpsResult, Draft, Immutable, Op, Path } from './interface';
10
+ export { original } from './original';
11
+ export { addToSet, deleteFromSet } from './setHelpers';
12
+ export { isDraft, isDraftable } from './utils';
@@ -0,0 +1,74 @@
1
+ import { JSONPrimitive, JSONValue, Path } from '../json';
2
+ import { Op } from '../operations';
3
+ export type { Op, Path, JSONValue };
4
+ export declare enum DraftType {
5
+ Object = 0,
6
+ Array = 1
7
+ }
8
+ /**
9
+ * Finalities - shared state for the draft tree
10
+ */
11
+ export interface Finalities {
12
+ /** Finalization callbacks (for unwrapping child drafts) */
13
+ draft: (() => void)[];
14
+ /** Revoke functions for all proxies */
15
+ revoke: (() => void)[];
16
+ /** Set of handled objects (for cycle detection) */
17
+ handledSet: WeakSet<object>;
18
+ /** Cache of created drafts */
19
+ draftsCache: WeakSet<object>;
20
+ /** List of operations performed in this draft session (eager logging) */
21
+ ops: Op[];
22
+ /** Root draft of the tree (set when creating the root draft) */
23
+ rootDraft: ProxyDraft | null;
24
+ }
25
+ /**
26
+ * Internal proxy draft state
27
+ */
28
+ export interface ProxyDraft<T = any> {
29
+ /** Type of the draft (Object or Array) */
30
+ type: DraftType;
31
+ /** Whether this draft has been mutated */
32
+ operated?: boolean;
33
+ /** Whether finalization has been completed */
34
+ finalized: boolean;
35
+ /** The original (unmodified) value */
36
+ original: T;
37
+ /** The shallow copy (created on first mutation) */
38
+ copy: T | null;
39
+ /** The proxy instance */
40
+ proxy: T | null;
41
+ /** Finalities container (shared across draft tree) */
42
+ finalities: Finalities;
43
+ /** Parent draft (for path tracking) */
44
+ parent?: ProxyDraft | null;
45
+ /** Key in parent */
46
+ key?: string | number;
47
+ /** Track which keys have been assigned (key -> true=assigned, false=deleted) */
48
+ assignedMap?: Map<PropertyKey, boolean>;
49
+ /** Count of positions this draft exists at (for aliasing optimization) */
50
+ aliasCount: number;
51
+ }
52
+ /**
53
+ * Result of createOps
54
+ */
55
+ export interface CreateOpsResult<T> {
56
+ /** The new immutable state */
57
+ nextState: T;
58
+ /** The operations that were performed */
59
+ ops: Op[];
60
+ }
61
+ /** Primitive types that don't need drafting */
62
+ type Primitive = JSONPrimitive;
63
+ /**
64
+ * Draft type - makes all properties mutable for editing
65
+ */
66
+ export type Draft<T> = T extends Primitive ? T : T extends object ? {
67
+ -readonly [K in keyof T]: Draft<T[K]>;
68
+ } : T;
69
+ /**
70
+ * Immutable type - makes all properties readonly
71
+ */
72
+ export type Immutable<T> = T extends Primitive ? T : T extends object ? {
73
+ readonly [K in keyof T]: Immutable<T[K]>;
74
+ } : T;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Get the original value from a draft.
3
+ */
4
+ /**
5
+ * `original(draft)` to get original state in the draft mutation function.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const { nextState, ops } = createOps(baseState, (draft) => {
10
+ * draft.foo.bar = 'new value';
11
+ * console.log(original(draft.foo)); // { bar: 'old value' }
12
+ * });
13
+ * ```
14
+ */
15
+ export declare function original<T>(target: T): T;
@@ -0,0 +1,9 @@
1
+ import { Op, ProxyDraft } from './interface';
2
+ /**
3
+ * Push an operation to the ops log.
4
+ * Values should already be cloned by the caller to avoid aliasing issues.
5
+ *
6
+ * When the target draft exists at multiple positions (due to aliasing),
7
+ * this function emits ops for all positions to maintain consistency.
8
+ */
9
+ export declare function pushOp(proxyDraft: ProxyDraft, op: Op): void;
@@ -0,0 +1,25 @@
1
+ import { JSONValue } from '../json';
2
+ /**
3
+ * Add a value to an array if it doesn't already exist (set semantics).
4
+ * Generates an `addToSet` operation.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * createOps(state, (draft) => {
9
+ * addToSet(draft.tags, 'newTag'); // Only adds if not present
10
+ * });
11
+ * ```
12
+ */
13
+ export declare function addToSet<T extends JSONValue>(draft: T[], value: T): void;
14
+ /**
15
+ * Remove a value from an array (set semantics).
16
+ * Generates a `deleteFromSet` operation.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * createOps(state, (draft) => {
21
+ * deleteFromSet(draft.tags, 'oldTag'); // Removes all matching items
22
+ * });
23
+ * ```
24
+ */
25
+ export declare function deleteFromSet<T extends JSONValue>(draft: T[], value: T): void;
@@ -0,0 +1,95 @@
1
+ import { DraftType, ProxyDraft } from './interface';
2
+ /**
3
+ * Get the latest value (copy if exists, otherwise original)
4
+ */
5
+ export declare function latest<T>(proxyDraft: ProxyDraft<T>): T;
6
+ /**
7
+ * Check if the value is a draft
8
+ */
9
+ export declare function isDraft(target: unknown): boolean;
10
+ /**
11
+ * Get the ProxyDraft from a draft value
12
+ */
13
+ export declare function getProxyDraft<T>(value: unknown): ProxyDraft<T> | null;
14
+ /**
15
+ * Get the actual value from a draft (copy or original)
16
+ */
17
+ export declare function getValue<T extends object>(value: T): T;
18
+ /**
19
+ * Check if a value is draftable (plain object or array)
20
+ * We only support plain objects and arrays - no Map, Set, Date, etc.
21
+ */
22
+ export declare function isDraftable(value: unknown): value is object;
23
+ /**
24
+ * Get the draft type
25
+ */
26
+ export declare function getType(target: unknown): DraftType;
27
+ /**
28
+ * Get a value by key
29
+ */
30
+ export declare function get(target: object, key: PropertyKey): unknown;
31
+ /**
32
+ * Set a value by key
33
+ */
34
+ export declare function set(target: object, key: PropertyKey, value: unknown): void;
35
+ /**
36
+ * Check if a key exists (own property)
37
+ */
38
+ export declare function has(target: object, key: PropertyKey): boolean;
39
+ /**
40
+ * Peek at a value (through drafts)
41
+ */
42
+ export declare function peek(target: object, key: PropertyKey): unknown;
43
+ /**
44
+ * SameValue comparison (handles -0 and NaN)
45
+ */
46
+ export declare function isEqual(x: unknown, y: unknown): boolean;
47
+ /**
48
+ * Revoke all proxies in a draft tree
49
+ */
50
+ export declare function revokeProxy(proxyDraft: ProxyDraft | null): void;
51
+ /**
52
+ * Get the path from root to this draft
53
+ */
54
+ export declare function getPath(target: ProxyDraft, path?: (string | number)[]): (string | number)[] | null;
55
+ /**
56
+ * Get the path from root to this draft, or throw if not available
57
+ */
58
+ export declare function getPathOrThrow(target: ProxyDraft): (string | number)[];
59
+ /**
60
+ * Find all paths to a given draft by searching the tree.
61
+ * This handles aliasing where the same draft exists at multiple positions.
62
+ */
63
+ export declare function getAllPathsForDraft(rootDraft: ProxyDraft, targetDraft: ProxyDraft): (string | number)[][];
64
+ /**
65
+ * Get a property descriptor from the prototype chain
66
+ */
67
+ export declare function getDescriptor(target: object, key: PropertyKey): PropertyDescriptor | undefined;
68
+ /**
69
+ * Create a shallow copy of an object or array
70
+ */
71
+ export declare function shallowCopy<T>(original: T): T;
72
+ /**
73
+ * Ensure a draft has a shallow copy
74
+ */
75
+ export declare function ensureShallowCopy(target: ProxyDraft): void;
76
+ /**
77
+ * Deep clone a value, unwrapping any drafts
78
+ */
79
+ export declare function deepClone<T>(target: T): T;
80
+ /**
81
+ * Clone if the value is a draft, otherwise return as-is
82
+ */
83
+ export declare function cloneIfNeeded<T>(target: T): T;
84
+ /**
85
+ * Mark a draft as changed (operated)
86
+ */
87
+ export declare function markChanged(target: ProxyDraft): void;
88
+ /**
89
+ * Iterate over object/array entries
90
+ */
91
+ export declare function forEach<T extends object>(target: T, callback: (key: PropertyKey, value: unknown, target: T) => void): void;
92
+ /**
93
+ * Handle nested values during finalization
94
+ */
95
+ export declare function handleValue(target: unknown, handledSet: WeakSet<object>): void;
@@ -32,11 +32,11 @@ export declare function isDraftModified<T extends JSONObject>(ctx: DraftContext<
32
32
  /**
33
33
  * Applies a single "set" operation to the draft with copy-on-write.
34
34
  */
35
- export declare function draftSet<T extends JSONObject>(ctx: DraftContext<T>, path: Path, key: string, value: JSONValue): void;
35
+ export declare function draftSet<T extends JSONObject>(ctx: DraftContext<T>, path: Path, key: string | number, value: JSONValue): void;
36
36
  /**
37
37
  * Applies a single "delete" operation to the draft with copy-on-write.
38
38
  */
39
- export declare function draftDelete<T extends JSONObject>(ctx: DraftContext<T>, path: Path, key: string): void;
39
+ export declare function draftDelete<T extends JSONObject>(ctx: DraftContext<T>, path: Path, key: string | number): void;
40
40
  /**
41
41
  * Applies a single "splice" operation to the draft with copy-on-write.
42
42
  */
@@ -1,4 +1,5 @@
1
1
  export type { CheckpointKey, CheckpointRecord } from './checkpoints';
2
+ export * from './createOps';
2
3
  export { createStateSyncLog, type StateSyncLogController, type StateSyncLogOptions, } from './createStateSyncLog';
3
4
  export type { JSONObject, JSONValue, Path } from './json';
4
5
  export { type ApplyOpsOptions, applyOps, type Op, type ValidateFn } from './operations';
@@ -6,7 +6,7 @@ export type Path = readonly (string | number)[];
6
6
  /**
7
7
  * A JSON primitive.
8
8
  */
9
- export type JSONPrimitive = null | boolean | number | string;
9
+ export type JSONPrimitive = undefined | null | boolean | number | string;
10
10
  /**
11
11
  * A JSON record.
12
12
  */
@@ -6,12 +6,12 @@ import { JSONObject, JSONValue, Path } from './json';
6
6
  export type Op = {
7
7
  kind: "set";
8
8
  path: Path;
9
- key: string;
9
+ key: string | number;
10
10
  value: JSONValue;
11
11
  } | {
12
12
  kind: "delete";
13
13
  path: Path;
14
- key: string;
14
+ key: string | number;
15
15
  } | {
16
16
  kind: "splice";
17
17
  path: Path;
@@ -21,3 +21,8 @@ export declare function deepClone<T>(value: T): T;
21
21
  * Creates a lazy memoized getter.
22
22
  */
23
23
  export declare function lazy<T>(fn: () => T): () => T;
24
+ /**
25
+ * Checks if a string is a valid non-negative integer array index.
26
+ * Returns the numeric value if valid, or null if invalid.
27
+ */
28
+ export declare function parseArrayIndex(key: string): number | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "state-sync-log",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Validated Replicated State Machine built on Yjs. Combine CRDT offline capabilities with strict business logic validation, state reconciliation, and audit trails.",
5
5
  "keywords": [
6
6
  "state-sync",
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Constants for createOps.
3
+ * Simplified from original source - removed dataTypes (mark feature).
4
+ */
5
+
6
+ // Symbol to identify proxy drafts - accessible for 3rd party
7
+ export const PROXY_DRAFT = Symbol.for("__CREATEOPS_PROXY_DRAFT__")
8
+
9
+ // Symbol iterator
10
+ export const iteratorSymbol: typeof Symbol.iterator = Symbol.iterator
@@ -0,0 +1,97 @@
1
+ /**
2
+ * createOps - Main API for generating operations from mutable-style mutations.
3
+ *
4
+ * Forked from mutative (https://github.com/unadlib/mutative)
5
+ * MIT License
6
+ */
7
+
8
+ import { current } from "./current"
9
+ import { draftify } from "./draftify"
10
+ import type { CreateOpsResult, Draft } from "./interface"
11
+ import { getProxyDraft, isDraft, isDraftable, isEqual, revokeProxy } from "./utils"
12
+
13
+ /**
14
+ * Create operations from mutable-style mutations.
15
+ *
16
+ * @param base - The base state (will not be mutated)
17
+ * @param mutate - A function that mutates the draft
18
+ * @returns An object containing the next state and the operations performed
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const state = { list: [{ text: 'Learn', done: false }] };
23
+ *
24
+ * const { nextState, ops } = createOps(state, (draft) => {
25
+ * draft.list[0].done = true;
26
+ * draft.list.push({ text: 'Practice', done: false });
27
+ * });
28
+ *
29
+ * // ops contains the operations that were performed:
30
+ * // [
31
+ * // { kind: 'set', path: ['list', 0], key: 'done', value: true },
32
+ * // { kind: 'splice', path: ['list'], index: 1, deleteCount: 0, inserts: [{ text: 'Practice', done: false }] }
33
+ * // ]
34
+ * ```
35
+ */
36
+ export function createOps<T extends object>(
37
+ base: T,
38
+ mutate: (draft: Draft<T>) => void
39
+ ): CreateOpsResult<T> {
40
+ // Handle case where base is already a draft
41
+ const state = isDraft(base) ? current(base as Draft<T>) : base
42
+
43
+ // Validate that state is draftable
44
+ if (!isDraftable(state)) {
45
+ throw new Error(`createOps() only supports plain objects and arrays.`)
46
+ }
47
+
48
+ // Create draft
49
+ const [draft, finalize] = draftify(state)
50
+
51
+ // Run mutation
52
+ let result: unknown
53
+ try {
54
+ result = mutate(draft as Draft<T>)
55
+ } catch (error) {
56
+ revokeProxy(getProxyDraft(draft))
57
+ throw error
58
+ }
59
+
60
+ // Handle return value
61
+ const proxyDraft = getProxyDraft(draft)!
62
+
63
+ // Check for invalid return values
64
+ if (result !== undefined && !isDraft(result)) {
65
+ if (!isEqual(result, draft) && proxyDraft.operated) {
66
+ throw new Error(
67
+ `Either the value is returned as a new non-draft value, or only the draft is modified without returning any value.`
68
+ )
69
+ }
70
+ // User returned a new value - use it as the next state
71
+ // Note: We don't support rawReturn, so returning a non-draft value replaces the state
72
+ // but we can't generate meaningful ops for this case
73
+ if (result !== undefined) {
74
+ const [, ops] = finalize([])
75
+ return { nextState: result as T, ops }
76
+ }
77
+ }
78
+
79
+ // Standard flow - finalize the draft
80
+ if (result === draft || result === undefined) {
81
+ const [nextState, ops] = finalize([])
82
+ return { nextState, ops }
83
+ }
84
+
85
+ // Returned a different draft (child)
86
+ const returnedProxyDraft = getProxyDraft(result)
87
+ if (returnedProxyDraft) {
88
+ if (returnedProxyDraft.operated) {
89
+ throw new Error(`Cannot return a modified child draft.`)
90
+ }
91
+ const [, ops] = finalize([])
92
+ return { nextState: current(result as object) as T, ops }
93
+ }
94
+
95
+ const [nextState, ops] = finalize([])
96
+ return { nextState, ops }
97
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Get the current value from a draft (snapshot).
3
+ * Adapted from mutative - simplified for plain objects/arrays only.
4
+ */
5
+
6
+ import type { Draft } from "./interface"
7
+ import {
8
+ forEach,
9
+ get,
10
+ getProxyDraft,
11
+ isDraft,
12
+ isDraftable,
13
+ isEqual,
14
+ set,
15
+ shallowCopy,
16
+ } from "./utils"
17
+
18
+ /**
19
+ * Get current state from a value (handles nested drafts)
20
+ */
21
+ function getCurrent<T>(target: T): T {
22
+ const proxyDraft = getProxyDraft(target)
23
+
24
+ // Not draftable - return as-is
25
+ if (!isDraftable(target)) return target
26
+
27
+ // Draft that hasn't been modified - return original
28
+ if (proxyDraft && !proxyDraft.operated) {
29
+ return proxyDraft.original as T
30
+ }
31
+
32
+ let currentValue: T | undefined
33
+
34
+ function ensureShallowCopyLocal() {
35
+ currentValue = shallowCopy(target)
36
+ }
37
+
38
+ if (proxyDraft) {
39
+ // It's a draft - create a shallow copy eagerly
40
+ proxyDraft.finalized = true
41
+ try {
42
+ ensureShallowCopyLocal()
43
+ } finally {
44
+ proxyDraft.finalized = false
45
+ }
46
+ } else {
47
+ // Not a draft - use target directly, copy lazily if needed
48
+ currentValue = target
49
+ }
50
+
51
+ // Recursively process children
52
+ forEach(currentValue as object, (key, value) => {
53
+ if (proxyDraft && isEqual(get(proxyDraft.original as object, key), value)) {
54
+ return
55
+ }
56
+ const newValue = getCurrent(value)
57
+ if (newValue !== value) {
58
+ if (currentValue === target) {
59
+ ensureShallowCopyLocal()
60
+ }
61
+ set(currentValue as object, key, newValue)
62
+ }
63
+ })
64
+
65
+ return currentValue as T
66
+ }
67
+
68
+ /**
69
+ * `current(draft)` to get current state in the draft mutation function.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const { nextState, ops } = createOps(baseState, (draft) => {
74
+ * draft.foo.bar = 'new value';
75
+ * console.log(current(draft.foo)); // { bar: 'new value' }
76
+ * });
77
+ * ```
78
+ */
79
+ export function current<T extends object>(target: Draft<T>): T
80
+ export function current<T extends object>(target: T | Draft<T>): T {
81
+ if (!isDraft(target)) {
82
+ throw new Error(`current() is only used for Draft, parameter: ${target}`)
83
+ }
84
+ return getCurrent(target) as T
85
+ }