@jucie.io/state 1.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.
- package/LICENSE +61 -0
- package/core/README.md +635 -0
- package/dist/Plugin.js +36 -0
- package/dist/State.d.ts +44 -0
- package/dist/State.js +243 -0
- package/dist/admin/binary.js +90 -0
- package/dist/admin/buffer.js +174 -0
- package/dist/admin/pack.js +67 -0
- package/dist/admin/unpack.js +88 -0
- package/dist/lib/TOKENS.js +18 -0
- package/dist/lib/change.js +94 -0
- package/dist/lib/global.js +42 -0
- package/dist/lib/gsru.js +125 -0
- package/dist/lib/marker.js +233 -0
- package/dist/lib/pathEncoder.js +89 -0
- package/dist/lib/tree/mutate.js +193 -0
- package/dist/lib/tree/seek.js +66 -0
- package/dist/lib/tree/traverse.js +38 -0
- package/dist/main.js +5 -0
- package/dist/main.js.map +7 -0
- package/dist/plugins/history.js +2 -0
- package/dist/plugins/history.js.map +7 -0
- package/dist/plugins/matcher.js +2 -0
- package/dist/plugins/matcher.js.map +7 -0
- package/dist/plugins/on-change.js +2 -0
- package/dist/plugins/on-change.js.map +7 -0
- package/dist/utils/clone.js +7 -0
- package/dist/utils/convertStringToExpression.js +23 -0
- package/dist/utils/convertStringToFunction.js +17 -0
- package/dist/utils/defer.js +24 -0
- package/dist/utils/isAsync.js +4 -0
- package/dist/utils/isPrimitive.js +12 -0
- package/dist/utils/isPromise.js +1 -0
- package/dist/utils/nextIdleTick.js +23 -0
- package/package.json +81 -0
- package/plugins/history/README.md +320 -0
- package/plugins/matcher/README.md +402 -0
- package/plugins/on-change/README.md +444 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../core/src/Plugin.js", "../../core/src/lib/TOKENS.js", "../../core/src/lib/pathEncoder.js", "../../core/src/lib/marker.js", "../../plugins/matcher/src/Matcher.js"],
|
|
4
|
+
"sourcesContent": ["export class Plugin {\n\n static name = null;\n static options = {};\n\n static configure(options) {\n options = {...this.options, ...options};\n return {\n install: (state) => this.install(state, options),\n name: this.name,\n options\n }\n }\n\n static install(state, options) {\n options = {...this.options, ...options};\n const pluginInstance = new this(state, options);\n\n Object.defineProperty(pluginInstance, 'state', {\n value: state,\n writable: false,\n configurable: false\n });\n \n Object.defineProperty(pluginInstance, 'options', {\n value: options,\n writable: false,\n configurable: false\n });\n\n \n return pluginInstance;\n }\n}\n\nexport default Plugin;", "export const GLOBAL_TAG = '*';\nexport const STATE_CONTEXT = Symbol('STATE_CONTEXT');\nexport const MATCHER = Symbol('MATCHER');\nexport const CREATED = 'CREATED';\nexport const DELETED = 'DELETED';\nexport const UPDATED = 'UPDATED';\n\n// Marker type bitflags\nexport const MARKER_GLOBAL = 1; // 0b001\nexport const MARKER_SINGLE = 2; // 0b010\nexport const MARKER_MANY = 4; // 0b100\nexport const MARKER_EPHEMERAL = 8; // 0b1000\n\n// Comparison result constants\nexport const MATCH_EXACT = 0; // Markers are identical\nexport const MATCH_PARENT = 1; // controlMarker is child of comparedMarker (parent changed)\nexport const MATCH_CHILD = 2; // comparedMarker is child of controlMarker (child changed)\nexport const MATCH_NONE = -1; // No relationship", "import { GLOBAL_TAG } from './TOKENS.js';\n\n// Fast helpers\nfunction escapeStr(str) {\n // Order matters: escape ~ first, then .\n let out = \"\";\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n if (ch === \"~\") out += \"~~\";\n else if (ch === \".\") out += \"~d\";\n else out += ch;\n }\n // Represent empty strings as ~e to avoid trailing-dot filenames\n return out.length === 0 ? \"~e\" : out;\n}\n\nfunction unescapeStr(str) {\n let out = \"\";\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n if (ch === \"~\") {\n const next = str[++i];\n if (next === \"~\") out += \"~\";\n else if (next === \"d\") out += \".\";\n else if (next === \"e\") out += \"\"; // empty string marker\n else {\n // Unknown escape: treat as literal (robustness)\n out += \"~\" + (next ?? \"\");\n }\n } else {\n out += ch;\n }\n }\n return out;\n}\n\nexport function encodePath(segments) {\n // segments: array of strings or integers\n // Produces: filename/URL-safe string like \"sfoo.n0.sbaz\"\n const parts = new Array(segments.length);\n for (let i = 0; i < segments.length; i++) {\n const v = segments[i];\n if (typeof v === \"number\" && Number.isInteger(v)) {\n // 'n' tag\n parts[i] = \"n\" + String(v); // decimal; includes \"-\" if negative\n } else if (typeof v === \"string\") {\n // 's' tag\n parts[i] = \"s\" + escapeStr(v);\n } else {\n // If you need more types, add here (e.g., booleans 'b', floats 'f').\n throw new TypeError(`Unsupported segment type at index ${i}: ${v}`);\n }\n }\n // Use '.' as separator (safe in URLs and filenames; we avoid trailing '.')\n return parts.join(\".\");\n}\n\nexport function decodeAddress(address) {\n if (address === GLOBAL_TAG) return GLOBAL_TAG;\n if (address.length === 0) return [];\n const raw = address.split(\".\");\n const out = new Array(raw.length);\n for (let i = 0; i < raw.length; i++) {\n const token = raw[i];\n if (token.length === 0) {\n // Disallow empty tokens (would imply trailing or double dots)\n throw new Error(\"Invalid address: empty token\");\n }\n const tag = token[0];\n const body = token.slice(1);\n if (tag === \"n\") {\n // Fast parse (no regex)\n if (body.length === 0 || !/^[-]?\\d+$/.test(body)) {\n throw new Error(`Invalid numeric token: \"${token}\"`);\n }\n const num = Number(body);\n // Ensure it was an integer\n if (!Number.isInteger(num)) {\n throw new Error(`Non-integer numeric token: \"${token}\"`);\n }\n out[i] = num;\n } else if (tag === \"s\") {\n out[i] = unescapeStr(body);\n } else {\n throw new Error(`Unknown type tag \"${tag}\" in token \"${token}\"`);\n }\n }\n return out;\n}\n", "import { encodePath } from './pathEncoder.js';\nimport { GLOBAL_TAG } from './TOKENS.js';\nimport {\n MARKER_GLOBAL,\n MARKER_SINGLE,\n MARKER_MANY,\n MARKER_EPHEMERAL,\n MATCH_EXACT,\n MATCH_PARENT,\n MATCH_CHILD,\n MATCH_NONE \n} from './TOKENS.js';\n\n// Marker type bitflags\n\n// Helper functions to check marker type\nexport function isGlobalMarker(marker) {\n return (marker.type & MARKER_GLOBAL) === MARKER_GLOBAL;\n}\n\nexport function isPathMarker(marker) {\n return (marker.type & MARKER_SINGLE) === MARKER_SINGLE;\n}\n\nexport function isMarkers(marker) {\n return (marker.type & MARKER_MANY) === MARKER_MANY;\n}\n\nexport function isEphemeralMarker(marker) {\n return (marker.type & MARKER_EPHEMERAL) === MARKER_EPHEMERAL;\n}\n\nexport function isGlobalPath(path) {\n return !path || path.length === 0 || path === GLOBAL_TAG || path[0] === GLOBAL_TAG;\n}\n\nexport function normalizePaths(args = []) { \n const len = args.length;\n if (len === 0) return [0, [] ];\n if (len === 1 && args[0] === GLOBAL_TAG) return [0, [] ];\n \n if (Array.isArray(args[0])) {\n return len === 1 \n ? [1, [args[0]] ]\n : [len, args ];\n }\n \n return [1, [[...args]] ];\n}\n\nfunction pathHasEphemeral(path) {\n if (!Array.isArray(path)) {\n return false;\n }\n\n for (let i = 0; i < path.length; i++) {\n const segment = path[i];\n if (typeof segment === 'string' && segment.charCodeAt(0) === 46) { // '.'\n return true;\n }\n }\n\n return false;\n}\n\n// Marker factory\nexport function createMarker(paths = []) {\n if (isGlobalPath(paths)) {\n return {\n address: GLOBAL_TAG,\n isMarker: true,\n length: 0,\n path: [],\n children: null,\n type: MARKER_GLOBAL\n };\n }\n \n const [length, normalizedPaths] = normalizePaths(paths);\n const type = length === 1 ? MARKER_SINGLE : MARKER_MANY;\n const path = length === 1 ? normalizedPaths[0] : normalizedPaths;\n const children = length > 1 ? normalizedPaths.map(path => createMarker(path)) : null;\n const isEphemeral = type === MARKER_SINGLE\n ? pathHasEphemeral(path)\n : children.some(child => isEphemeralMarker(child));\n\n let markerType = type;\n if (isEphemeral) {\n markerType |= MARKER_EPHEMERAL;\n }\n\n return {\n address: type === MARKER_SINGLE ? encodePath(path) : null,\n isMarker: true,\n length,\n path,\n children,\n type: markerType\n };\n}\n\nexport function createChildMarker(parentMarker, childPaths) {\n if (childPaths.length === 0) {\n return parentMarker;\n }\n // Normalize the child paths\n const [childLength, normalizedChildPaths] = normalizePaths(childPaths);\n \n // Handle global marker - just return marker with child paths\n if (isGlobalMarker(parentMarker)) {\n return createMarker(normalizedChildPaths, parentMarker.state);\n }\n \n // Handle single path marker\n if (isPathMarker(parentMarker)) {\n if (childLength === 0) {\n // No child paths, return parent as-is\n return parentMarker;\n }\n \n if (childLength === 1) {\n // Single child path - append to parent path\n const newPath = [...parentMarker.path, ...normalizedChildPaths[0]];\n return createMarker(newPath, parentMarker.state);\n } else {\n // Multiple child paths - create many markers\n const newPaths = normalizedChildPaths.map(childPath => \n [...parentMarker.path, ...childPath]\n );\n return createMarker(newPaths, parentMarker.state);\n }\n }\n \n // Handle many markers - recursively create child markers for each\n if (isMarkers(parentMarker)) {\n const newMarkers = new Array(parentMarker.length);\n let i = 0;\n while (i < parentMarker.length) {\n newMarkers[i] = createChildMarker(parentMarker.children[i], childPaths);\n i++;\n }\n \n // Collect all paths from the new markers\n const allPaths = [];\n i = 0;\n while (i < newMarkers.length) {\n const marker = newMarkers[i];\n if (isPathMarker(marker)) {\n allPaths.push(marker.path);\n } else if (isMarkers(marker)) {\n let j = 0;\n while (j < marker.length) {\n allPaths.push(marker.children[j].path);\n j++;\n }\n }\n i++;\n }\n \n return createMarker(allPaths, parentMarker.state);\n }\n \n // Fallback - shouldn't reach here\n return parentMarker;\n}\n\nexport function dispatch(marker, { global, path, ephemeral, error }) {\n try {\n if (!marker.isMarker) return undefined;\n if (isGlobalMarker(marker)) return global ? global(marker) : undefined;\n if (isEphemeralMarker(marker)) return ephemeral ? ephemeral(marker) : path ? path(marker) : undefined;\n if (isPathMarker(marker)) return path ? path(marker) : undefined;\n if (isMarkers(marker)) {\n const results = new Array(marker.length);\n let i = 0;\n while (i < marker.length) {\n const nestedMarker = marker.children[i];\n results[i] = dispatch(nestedMarker, { global, path, ephemeral, error });\n i++;\n }\n return results;\n }\n \n return undefined;\n } catch (err) {\n return error ? error(err.message) : undefined;\n }\n}\n\nexport function compareMarkers(controlMarker, comparedMarker) {\n // Both are global markers or exact address match\n if (isGlobalMarker(controlMarker) && isGlobalMarker(comparedMarker)) {\n return MATCH_EXACT;\n }\n\n // If comparedMarker is global, it's always a parent\n if (isGlobalMarker(comparedMarker)) {\n return MATCH_PARENT;\n }\n \n // Need addresses to compare\n if (!controlMarker.address || !comparedMarker.address) {\n return MATCH_NONE;\n }\n \n // Exact match\n if (controlMarker.address === comparedMarker.address) {\n return MATCH_EXACT;\n }\n \n // controlMarker is more nested (child) - parent changed\n if (controlMarker.address.startsWith(comparedMarker.address + '.')) {\n return MATCH_PARENT;\n }\n \n // comparedMarker is more nested (child) - child changed\n if (comparedMarker.address.startsWith(controlMarker.address + '.')) {\n return MATCH_CHILD;\n }\n \n return MATCH_NONE;\n}\n\nexport const Marker = {\n compare: compareMarkers,\n create: createMarker,\n createChild: createChildMarker,\n dispatch: dispatch,\n isGlobal: isGlobalMarker,\n isSingle: isPathMarker,\n isMarkers: isMarkers,\n isEphemeral: isEphemeralMarker\n};", "import { Plugin } from '@jucie-state/core/Plugin';\nimport { compareMarkers, createMarker, dispatch } from '@jucie-state/core/lib/marker.js';\nimport { MATCHER } from '@jucie-state/core/lib/TOKENS.js';\n\nimport { MATCH_CHILD, MATCH_PARENT, MATCH_EXACT, MATCH_NONE } from '@jucie-state/core/lib/TOKENS.js';\n\nexport class Matcher extends Plugin {\n static name = 'matcher';\n static options = {\n matchers: [],\n };\n \n #matchers = new Set();\n \n initialize(state, config) {\n if (config.matchers && config.matchers.length > 0) {\n for (const matcher of config.matchers) {\n if (typeof matcher === 'function' && matcher._isMatcher === MATCHER) {\n this.#assignMatcherState(matcher);\n this.#addMatcher(matcher);\n } else {\n if (!matcher.path || !matcher.handler) {\n throw new Error('Matcher path and handler are required');\n }\n this.#addMatcher(createMatcher(matcher.path, matcher.handler));\n }\n }\n }\n }\n\n actions () {\n return {\n createMatcher: (path, handler) => {\n const matcher = createMatcher(path, handler);\n this.#assignMatcherState(matcher);\n this.#addMatcher(matcher);\n return () => this.#removeMatcher(matcher);\n },\n addMatcher: (matcher) => this.#addMatcher(matcher),\n removeMatcher: (matcher) => this.#removeMatcher(matcher),\n }\n }\n\n #assignMatcherState(matcher) {\n Object.defineProperty(matcher, '_state', {\n value: this.state,\n writable: false,\n configurable: false\n });\n }\n\n #addMatcher(matcher) {\n this.#assignMatcherState(matcher);\n this.#matchers.add(matcher);\n return () => this.#removeMatcher(matcher);\n }\n\n #removeMatcher(matcher) {\n this.#matchers.delete(matcher);\n }\n\n onStateChange(marker, change) {\n this.#matchers.forEach(matcher => matcher(marker, change));\n }\n}\n\nexport function createMatcher(matchPath, fn) {\n if (!Array.isArray(matchPath)) {\n throw new Error('matchPath must be an array');\n }\n\n if (matchPath.length === 0) {\n throw new Error('matchPath must be a non-empty array');\n }\n\n if (fn === undefined || typeof fn !== 'function') {\n throw new Error('Matcher function is required');\n }\n\n const changeQueue = new Map();\n let isFlushing = false;\n\n const matchMarker = createMarker(matchPath);\n const matcher = (marker) => {\n if (compareMarkers(matchMarker, marker) >= 0) {\n queueChange(marker);\n };\n }\n\n function flushChanges() {\n if (isFlushing) return;\n isFlushing = true;\n setTimeout(() => {\n const markers = Array.from(changeQueue.values());\n let changes = {};\n let hasChanges = false;\n for (const marker of markers) {\n dispatch(marker, {\n global: () => {\n changes = matcher._state.get(matchPath);\n hasChanges = true;\n },\n path: () => {\n const comp = compareMarkers(matchMarker, marker);\n \n switch (comp) {\n case MATCH_PARENT:\n case MATCH_EXACT:\n changes = matcher._state.get(matchPath);\n hasChanges = true;\n break;\n case MATCH_CHILD:\n const childPath = marker.path.slice(0, matchPath.length + 1);\n const key = childPath[childPath.length - 1];\n changes = typeof changes === 'object' && changes !== null ? changes : {};\n changes[key] = matcher._state.get(childPath);\n hasChanges = true;\n break;\n }\n },\n });\n }\n\n if (hasChanges) {\n fn(changes);\n }\n changeQueue.clear();\n isFlushing = false;\n }, 0);\n }\n\n function queueChange(marker) {\n if (changeQueue.has(marker.address)) {\n changeQueue.delete(marker.address);\n }\n changeQueue.set(marker.address, marker);\n flushChanges();\n }\n\n Object.defineProperty(matcher, '_isMatcher', {\n value: MATCHER,\n writable: false,\n enumerable: false,\n configurable: true\n });\n\n\n return matcher;\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["Plugin", "options", "state", "pluginInstance", "GLOBAL_TAG", "STATE_CONTEXT", "MATCHER", "MARKER_GLOBAL", "MARKER_SINGLE", "MARKER_MANY", "MARKER_EPHEMERAL", "MATCH_EXACT", "MATCH_PARENT", "MATCH_CHILD", "MATCH_NONE", "escapeStr", "str", "out", "i", "ch", "encodePath", "segments", "parts", "i", "v", "escapeStr", "isGlobalMarker", "marker", "MARKER_GLOBAL", "isPathMarker", "MARKER_SINGLE", "isMarkers", "MARKER_MANY", "isEphemeralMarker", "MARKER_EPHEMERAL", "isGlobalPath", "path", "GLOBAL_TAG", "normalizePaths", "args", "len", "pathHasEphemeral", "i", "segment", "createMarker", "paths", "length", "normalizedPaths", "type", "children", "isEphemeral", "child", "markerType", "encodePath", "dispatch", "marker", "global", "path", "ephemeral", "error", "isGlobalMarker", "isEphemeralMarker", "isPathMarker", "isMarkers", "results", "i", "nestedMarker", "err", "compareMarkers", "controlMarker", "comparedMarker", "MATCH_EXACT", "MATCH_PARENT", "MATCH_NONE", "MATCH_CHILD", "Matcher", "Plugin", "#matchers", "state", "config", "matcher", "MATCHER", "#assignMatcherState", "#addMatcher", "createMatcher", "path", "handler", "#removeMatcher", "marker", "change", "matchPath", "fn", "changeQueue", "isFlushing", "matchMarker", "createMarker", "compareMarkers", "queueChange", "flushChanges", "markers", "changes", "hasChanges", "dispatch", "MATCH_PARENT", "MATCH_EXACT", "MATCH_CHILD", "childPath", "key"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var r=class{static name=null;static options={};static configure(e){return e={...this.options,...e},{install:t=>this.install(t,e),name:this.name,options:e}}static install(e,t){t={...this.options,...t};let s=new this(e,t);return Object.defineProperty(s,"state",{value:e,writable:!1,configurable:!1}),Object.defineProperty(s,"options",{value:t,writable:!1,configurable:!1}),s}};var i=class extends r{static name="onChange";#t=new Set;#e=new Map;#r=!1;#s=!1;#n=0;constructor(e={}){super(e),this.#n=e.debounce??0}actions(){return{addListener:e=>{if(typeof e=="function")return this.#a(e)},subscribe:e=>{if(typeof e=="function")return this.#a(e)},unsubscribe:e=>{if(typeof e=="function")return this.#i(e)},removeListener:e=>this.#i(e),clear:()=>this.#o()}}onStateChange(e,t){!this.#s||t.method==="apply"||this.#h(e,t)}#h(e,t){this.#e.has(e.address)&&this.#e.delete(e.address),this.#e.set(e.address,t),this.#u()}#u(){if(!this.#r){this.#r=!0;try{setTimeout(()=>{let e=Array.from(this.#e.values());this.#e.clear();for(let t of this.#t)try{t(e)}catch(s){console.error("Error in onChange listener:",s)}this.#r=!1},this.#n)}catch(e){console.error(e)}}}#a(e){return this.#t.add(e),this.#s=!0,()=>this.#i(e)}#i(e){typeof e=="function"&&(this.#t.delete(e),this.#t.size===0&&(this.#s=!1))}#o(){this.#t.clear(),this.#e.clear(),this.#s=!1}};export{i as OnChange};
|
|
2
|
+
//# sourceMappingURL=on-change.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../core/src/Plugin.js", "../../plugins/on-change/src/OnChange.js"],
|
|
4
|
+
"sourcesContent": ["export class Plugin {\n\n static name = null;\n static options = {};\n\n static configure(options) {\n options = {...this.options, ...options};\n return {\n install: (state) => this.install(state, options),\n name: this.name,\n options\n }\n }\n\n static install(state, options) {\n options = {...this.options, ...options};\n const pluginInstance = new this(state, options);\n\n Object.defineProperty(pluginInstance, 'state', {\n value: state,\n writable: false,\n configurable: false\n });\n \n Object.defineProperty(pluginInstance, 'options', {\n value: options,\n writable: false,\n configurable: false\n });\n\n \n return pluginInstance;\n }\n}\n\nexport default Plugin;", "import { Plugin } from '@jucie-state/core/Plugin';\n\nexport class OnChange extends Plugin {\n static name = 'onChange';\n\n #listeners = new Set();\n #changeQueue = new Map();\n #flushing = false;\n #hasListeners = false;\n #debounceMs = 0;\n\n constructor(options = {}) {\n super(options);\n this.#debounceMs = options.debounce ?? 0;\n }\n\n actions() {\n return {\n addListener: (callback) => {\n if (typeof callback !== 'function') {\n return;\n }\n return this.#addListener(callback);\n },\n subscribe: (callback) => {\n if (typeof callback !== 'function') {\n return;\n }\n return this.#addListener(callback);\n },\n unsubscribe: (callback) => {\n if (typeof callback !== 'function') {\n return;\n }\n return this.#removeListener(callback);\n },\n removeListener: (callback) => {\n return this.#removeListener(callback);\n },\n clear: () => this.#clear()\n }\n }\n\n onStateChange(marker, change) {\n if (!this.#hasListeners || change.method === 'apply') {\n return;\n }\n\n this.#queueChange(marker, change);\n }\n\n #queueChange(marker, change) {\n if (this.#changeQueue.has(marker.address)) {\n this.#changeQueue.delete(marker.address);\n }\n this.#changeQueue.set(marker.address, change);\n this.#flush();\n }\n\n #flush() {\n if (this.#flushing) {\n return;\n }\n this.#flushing = true;\n try {\n setTimeout(() => {\n const changes = Array.from(this.#changeQueue.values());\n this.#changeQueue.clear();\n for (const callback of this.#listeners) {\n try {\n callback(changes);\n } catch (error) {\n console.error('Error in onChange listener:', error);\n }\n }\n this.#flushing = false;\n }, this.#debounceMs);\n } catch (error) {\n console.error(error);\n }\n }\n\n #addListener(callback) {\n this.#listeners.add(callback);\n this.#hasListeners = true;\n return () => this.#removeListener(callback);\n }\n\n #removeListener(callback) {\n if (typeof callback !== 'function') {\n return;\n }\n this.#listeners.delete(callback);\n if (this.#listeners.size === 0) {\n this.#hasListeners = false;\n }\n }\n\n #clear() {\n this.#listeners.clear();\n this.#changeQueue.clear();\n this.#hasListeners = false;\n }\n}"],
|
|
5
|
+
"mappings": "AAAO,IAAMA,EAAN,KAAa,CAElB,OAAO,KAAO,KACd,OAAO,QAAU,CAAC,EAElB,OAAO,UAAUC,EAAS,CACxB,OAAAA,EAAU,CAAC,GAAG,KAAK,QAAS,GAAGA,CAAO,EAC/B,CACL,QAAUC,GAAU,KAAK,QAAQA,EAAOD,CAAO,EAC/C,KAAM,KAAK,KACX,QAAAA,CACF,CACF,CAEA,OAAO,QAAQC,EAAOD,EAAS,CAC7BA,EAAU,CAAC,GAAG,KAAK,QAAS,GAAGA,CAAO,EACtC,IAAME,EAAiB,IAAI,KAAKD,EAAOD,CAAO,EAE9C,cAAO,eAAeE,EAAgB,QAAS,CAC7C,MAAOD,EACP,SAAU,GACV,aAAc,EAChB,CAAC,EAED,OAAO,eAAeC,EAAgB,UAAW,CAC/C,MAAOF,EACP,SAAU,GACV,aAAc,EAChB,CAAC,EAGME,CACT,CACF,EC/BO,IAAMC,EAAN,cAAuBC,CAAO,CACnC,OAAO,KAAO,WAEdC,GAAa,IAAI,IACjBC,GAAe,IAAI,IACnBC,GAAY,GACZC,GAAgB,GAChBC,GAAc,EAEd,YAAYC,EAAU,CAAC,EAAG,CACxB,MAAMA,CAAO,EACb,KAAKD,GAAcC,EAAQ,UAAY,CACzC,CAEA,SAAU,CACR,MAAO,CACL,YAAcC,GAAa,CACzB,GAAI,OAAOA,GAAa,WAGxB,OAAO,KAAKC,GAAaD,CAAQ,CACnC,EACA,UAAYA,GAAa,CACvB,GAAI,OAAOA,GAAa,WAGxB,OAAO,KAAKC,GAAaD,CAAQ,CACnC,EACA,YAAcA,GAAa,CACzB,GAAI,OAAOA,GAAa,WAGxB,OAAO,KAAKE,GAAgBF,CAAQ,CACtC,EACA,eAAiBA,GACR,KAAKE,GAAgBF,CAAQ,EAEtC,MAAO,IAAM,KAAKG,GAAO,CAC3B,CACF,CAEA,cAAcC,EAAQC,EAAQ,CACxB,CAAC,KAAKR,IAAiBQ,EAAO,SAAW,SAI7C,KAAKC,GAAaF,EAAQC,CAAM,CAClC,CAEAC,GAAaF,EAAQC,EAAQ,CACvB,KAAKV,GAAa,IAAIS,EAAO,OAAO,GACtC,KAAKT,GAAa,OAAOS,EAAO,OAAO,EAEzC,KAAKT,GAAa,IAAIS,EAAO,QAASC,CAAM,EAC5C,KAAKE,GAAO,CACd,CAEAA,IAAS,CACP,GAAI,MAAKX,GAGT,MAAKA,GAAY,GACjB,GAAI,CACF,WAAW,IAAM,CACf,IAAMY,EAAU,MAAM,KAAK,KAAKb,GAAa,OAAO,CAAC,EACrD,KAAKA,GAAa,MAAM,EACxB,QAAWK,KAAY,KAAKN,GAC1B,GAAI,CACFM,EAASQ,CAAO,CAClB,OAASC,EAAO,CACd,QAAQ,MAAM,8BAA+BA,CAAK,CACpD,CAEF,KAAKb,GAAY,EACnB,EAAG,KAAKE,EAAW,CACrB,OAASW,EAAO,CACd,QAAQ,MAAMA,CAAK,CACrB,EACF,CAEAR,GAAaD,EAAU,CACrB,YAAKN,GAAW,IAAIM,CAAQ,EAC5B,KAAKH,GAAgB,GACd,IAAM,KAAKK,GAAgBF,CAAQ,CAC5C,CAEAE,GAAgBF,EAAU,CACpB,OAAOA,GAAa,aAGxB,KAAKN,GAAW,OAAOM,CAAQ,EAC3B,KAAKN,GAAW,OAAS,IAC3B,KAAKG,GAAgB,IAEzB,CAEAM,IAAS,CACP,KAAKT,GAAW,MAAM,EACtB,KAAKC,GAAa,MAAM,EACxB,KAAKE,GAAgB,EACvB,CACF",
|
|
6
|
+
"names": ["Plugin", "options", "state", "pluginInstance", "OnChange", "Plugin", "#listeners", "#changeQueue", "#flushing", "#hasListeners", "#debounceMs", "options", "callback", "#addListener", "#removeListener", "#clear", "marker", "change", "#queueChange", "#flush", "changes", "error"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const expressions = new Map();
|
|
2
|
+
|
|
3
|
+
export function convertStringToExpression(exprString) {
|
|
4
|
+
if (!exprString) return null;
|
|
5
|
+
if (expressions.has(exprString)) return expressions.get(exprString);
|
|
6
|
+
|
|
7
|
+
// Wrap the expression in a try/catch and return the result
|
|
8
|
+
const fnBody = `
|
|
9
|
+
try {
|
|
10
|
+
return (${exprString});
|
|
11
|
+
} catch (e) {
|
|
12
|
+
// Optionally log the error
|
|
13
|
+
// console.error('Expression error:', e);
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
// Only 'scope' is passed in, but you could add more context if needed
|
|
19
|
+
const fn = new Function('scope', fnBody);
|
|
20
|
+
expressions.set(exprString, fn);
|
|
21
|
+
|
|
22
|
+
return fn;
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const functions = new Map();
|
|
2
|
+
|
|
3
|
+
export function convertStringToFunction (fnString) {
|
|
4
|
+
if (!fnString) return null;
|
|
5
|
+
if (functions.has(fnString)) return functions.get(fnString);
|
|
6
|
+
|
|
7
|
+
const match = fnString.match(/^function\s*\(([^)]*)\)\s*{([\s\S]*)}$/);
|
|
8
|
+
if (!match) throw new Error("Invalid function string");
|
|
9
|
+
|
|
10
|
+
const args = match[1].split(',').map(arg => arg.trim()).filter(Boolean);
|
|
11
|
+
const body = match[2];
|
|
12
|
+
|
|
13
|
+
const fn = new Function(...args, body);
|
|
14
|
+
functions.set(fnString, fn);
|
|
15
|
+
|
|
16
|
+
return fn;
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function defer(arg1, arg2) {
|
|
2
|
+
let callback = null;
|
|
3
|
+
let delay = 0;
|
|
4
|
+
|
|
5
|
+
if (typeof arg1 === 'function') {
|
|
6
|
+
callback = arg1;
|
|
7
|
+
if (typeof arg2 === 'number') {
|
|
8
|
+
delay = arg2;
|
|
9
|
+
}
|
|
10
|
+
} else if (typeof arg1 === 'number') {
|
|
11
|
+
delay = arg1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (...args) => new Promise((resolve, reject) => {
|
|
15
|
+
setTimeout(() => {
|
|
16
|
+
try {
|
|
17
|
+
const result = callback ? callback(...args) : undefined;
|
|
18
|
+
resolve(result);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
reject(error);
|
|
21
|
+
}
|
|
22
|
+
}, delay);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function isPrimitive(value) {
|
|
2
|
+
// Fast type check for primitives
|
|
3
|
+
const type = typeof value;
|
|
4
|
+
return value === null ||
|
|
5
|
+
value === undefined ||
|
|
6
|
+
type === 'string' ||
|
|
7
|
+
type === 'number' ||
|
|
8
|
+
type === 'boolean' ||
|
|
9
|
+
type === 'bigint' ||
|
|
10
|
+
type === 'symbol' ||
|
|
11
|
+
type === 'function';
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isPromise = v => (typeof v === 'object' && v !== null) && Promise.resolve(v) === v;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function nextIdleTick(callback) {
|
|
2
|
+
if (typeof window !== 'undefined' && window.requestIdleCallback) {
|
|
3
|
+
return window.requestIdleCallback(callback, { timeout: 100 });
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Fallback for browsers without requestIdleCallback (Safari/WebKit)
|
|
7
|
+
if (typeof window !== 'undefined') {
|
|
8
|
+
// Use requestAnimationFrame + setTimeout for better idle approximation
|
|
9
|
+
if (window.requestAnimationFrame) {
|
|
10
|
+
return window.requestAnimationFrame(() => {
|
|
11
|
+
setTimeout(callback, 0);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
// Final fallback to setTimeout
|
|
15
|
+
return setTimeout(callback, 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Server-side or no window object - execute immediately
|
|
19
|
+
if (typeof callback === 'function') {
|
|
20
|
+
callback();
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jucie.io/state",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Modular state management system with path-based access, history, and reactive plugins",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/main.js",
|
|
7
|
+
"types": "./dist/State.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/State.d.ts",
|
|
11
|
+
"import": "./dist/main.js"
|
|
12
|
+
},
|
|
13
|
+
"./Plugin": {
|
|
14
|
+
"types": "./dist/Plugin.d.ts",
|
|
15
|
+
"import": "./dist/Plugin.js"
|
|
16
|
+
},
|
|
17
|
+
"./State": {
|
|
18
|
+
"types": "./dist/State.d.ts",
|
|
19
|
+
"import": "./dist/State.js"
|
|
20
|
+
},
|
|
21
|
+
"./history": {
|
|
22
|
+
"import": "./dist/plugins/history.js"
|
|
23
|
+
},
|
|
24
|
+
"./matcher": {
|
|
25
|
+
"import": "./dist/plugins/matcher.js"
|
|
26
|
+
},
|
|
27
|
+
"./on-change": {
|
|
28
|
+
"import": "./dist/plugins/on-change.js"
|
|
29
|
+
},
|
|
30
|
+
"./lib/*": "./dist/lib/*",
|
|
31
|
+
"./utils/*": "./dist/utils/*",
|
|
32
|
+
"./admin/*": "./dist/admin/*"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist/",
|
|
36
|
+
"core/README.md",
|
|
37
|
+
"plugins/history/README.md",
|
|
38
|
+
"plugins/matcher/README.md",
|
|
39
|
+
"plugins/on-change/README.md",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "node scripts/build-monolith.js",
|
|
45
|
+
"test": "vitest",
|
|
46
|
+
"bench": "NODE_OPTIONS='--max-old-space-size=4096' vitest bench 2>&1 | grep -v 'faster than' | grep -v 'BENCH Summary' | grep -v '^ [a-z]'",
|
|
47
|
+
"bench:raw": "NODE_OPTIONS='--max-old-space-size=4096' vitest bench",
|
|
48
|
+
"clean": "rm -rf dist",
|
|
49
|
+
"prepublishOnly": "npm run build"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/adrianjonmiller/state"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"state",
|
|
57
|
+
"state-management",
|
|
58
|
+
"reactive",
|
|
59
|
+
"history",
|
|
60
|
+
"undo",
|
|
61
|
+
"redo",
|
|
62
|
+
"serialization",
|
|
63
|
+
"path-based"
|
|
64
|
+
],
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=18.0.0"
|
|
67
|
+
},
|
|
68
|
+
"author": "Adrian Miller",
|
|
69
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"cbor-x": "^1.6.0"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"esbuild": "^0.24.2",
|
|
78
|
+
"nodemon": "^3.1.10",
|
|
79
|
+
"vitest": "^1.6.1"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# @jucio.io/state/history
|
|
2
|
+
|
|
3
|
+
History management plugin for @jucio.io/state that provides undo/redo functionality with markers, batching, and commit listeners.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ⏪ **Undo/Redo**: Full undo and redo support with automatic change tracking
|
|
8
|
+
- 🏷️ **Markers**: Add descriptive markers to create logical undo/redo boundaries
|
|
9
|
+
- 📦 **Batching**: Group multiple changes into a single undo/redo step
|
|
10
|
+
- 🔔 **Commit Listeners**: React to history commits
|
|
11
|
+
- 🎯 **Smart Change Consolidation**: Automatically merges changes to the same path
|
|
12
|
+
- 📏 **Configurable History Size**: Limit the number of stored changes
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @jucio.io/state
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Note:** History plugin is included in the main package.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
import { createState } from '@jucio.io/state';
|
|
26
|
+
import { HistoryManager } from '@jucio.io/state/history';
|
|
27
|
+
|
|
28
|
+
// Create state and install history plugin
|
|
29
|
+
const state = createState({
|
|
30
|
+
data: { count: 0 }
|
|
31
|
+
});
|
|
32
|
+
state.install(HistoryManager);
|
|
33
|
+
|
|
34
|
+
// Make some changes
|
|
35
|
+
state.set(['data', 'count'], 1);
|
|
36
|
+
state.set(['data', 'count'], 2);
|
|
37
|
+
state.set(['data', 'count'], 3);
|
|
38
|
+
|
|
39
|
+
console.log(state.get(['data', 'count'])); // 3
|
|
40
|
+
|
|
41
|
+
// Undo the changes
|
|
42
|
+
state.history.undo();
|
|
43
|
+
console.log(state.get(['data', 'count'])); // 2
|
|
44
|
+
|
|
45
|
+
state.history.undo();
|
|
46
|
+
console.log(state.get(['data', 'count'])); // 1
|
|
47
|
+
|
|
48
|
+
// Redo
|
|
49
|
+
state.history.redo();
|
|
50
|
+
console.log(state.get(['data', 'count'])); // 2
|
|
51
|
+
|
|
52
|
+
// Check if undo/redo is available
|
|
53
|
+
console.log(state.history.canUndo()); // true
|
|
54
|
+
console.log(state.history.canRedo()); // true
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### Actions
|
|
60
|
+
|
|
61
|
+
#### `undo(callback?)`
|
|
62
|
+
|
|
63
|
+
Undo the last committed change(s).
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
state.history.undo(() => {
|
|
67
|
+
console.log('Undo completed');
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Returns:** `boolean` - `true` if undo was successful, `false` if no changes to undo
|
|
72
|
+
|
|
73
|
+
#### `redo(callback?)`
|
|
74
|
+
|
|
75
|
+
Redo the next change(s).
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
state.history.redo(() => {
|
|
79
|
+
console.log('Redo completed');
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Returns:** `boolean` - `true` if redo was successful, `false` if no changes to redo
|
|
84
|
+
|
|
85
|
+
#### `canUndo()`
|
|
86
|
+
|
|
87
|
+
Check if undo is available.
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
if (state.history.canUndo()) {
|
|
91
|
+
state.history.undo();
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Returns:** `boolean`
|
|
96
|
+
|
|
97
|
+
#### `canRedo()`
|
|
98
|
+
|
|
99
|
+
Check if redo is available.
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
if (state.history.canRedo()) {
|
|
103
|
+
state.history.redo();
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Returns:** `boolean`
|
|
108
|
+
|
|
109
|
+
### Batching
|
|
110
|
+
|
|
111
|
+
#### `batch()`
|
|
112
|
+
|
|
113
|
+
Start a batch to group multiple changes into a single undo/redo step.
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const endBatch = state.history.batch();
|
|
117
|
+
|
|
118
|
+
state.set(['user', 'name'], 'Alice');
|
|
119
|
+
state.set(['user', 'age'], 30);
|
|
120
|
+
state.set(['user', 'email'], 'alice@example.com');
|
|
121
|
+
|
|
122
|
+
endBatch(); // All three changes are now a single undo/redo step
|
|
123
|
+
|
|
124
|
+
state.history.undo(); // Undoes all three changes at once
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Returns:** `Function` - Call the returned function to end the batch
|
|
128
|
+
|
|
129
|
+
#### `commit()`
|
|
130
|
+
|
|
131
|
+
Manually commit pending changes and end the current batch.
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
state.history.batch();
|
|
135
|
+
state.set(['data', 'value'], 1);
|
|
136
|
+
state.history.commit(); // Forces commit
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Returns:** `HistoryManager` instance (for chaining)
|
|
140
|
+
|
|
141
|
+
### Markers
|
|
142
|
+
|
|
143
|
+
#### `addMarker(description)`
|
|
144
|
+
|
|
145
|
+
Add a descriptive marker to create a logical undo/redo boundary.
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
state.set(['user', 'name'], 'Alice');
|
|
149
|
+
state.history.addMarker('Set user name');
|
|
150
|
+
|
|
151
|
+
state.set(['user', 'age'], 30);
|
|
152
|
+
state.history.addMarker('Set user age');
|
|
153
|
+
|
|
154
|
+
// Now each undo will stop at the marker
|
|
155
|
+
state.history.undo(); // Undoes age change
|
|
156
|
+
state.history.undo(); // Undoes name change
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Parameters:**
|
|
160
|
+
- `description` (string): Optional description for the marker
|
|
161
|
+
|
|
162
|
+
### Commit Listeners
|
|
163
|
+
|
|
164
|
+
#### `onCommit(callback)`
|
|
165
|
+
|
|
166
|
+
Listen for history commits.
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
const unsubscribe = state.history.onCommit((changes) => {
|
|
170
|
+
console.log('Changes committed:', changes);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
state.set(['data', 'value'], 1);
|
|
174
|
+
// Console: "Changes committed: [...]"
|
|
175
|
+
|
|
176
|
+
// Remove listener when done
|
|
177
|
+
unsubscribe();
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Parameters:**
|
|
181
|
+
- `callback` (Function): Called with an array of changes when committed
|
|
182
|
+
|
|
183
|
+
**Returns:** `Function` - Call to remove the listener
|
|
184
|
+
|
|
185
|
+
### Info
|
|
186
|
+
|
|
187
|
+
#### `size()`
|
|
188
|
+
|
|
189
|
+
Get the current number of items in the history.
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
const historySize = state.history.size();
|
|
193
|
+
console.log(`History contains ${historySize} items`);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Returns:** `number`
|
|
197
|
+
|
|
198
|
+
## Configuration
|
|
199
|
+
|
|
200
|
+
Configure the history plugin using the `configure()` method:
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
import { createState } from '@jucio.io/state';
|
|
204
|
+
import { HistoryManager } from '@jucio.io/state/history';
|
|
205
|
+
|
|
206
|
+
const state = createState({
|
|
207
|
+
data: { count: 0 }
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Install with custom configuration
|
|
211
|
+
state.install(HistoryManager.configure({
|
|
212
|
+
maxSize: 200 // Limit to 200 history items (default: 100)
|
|
213
|
+
}));
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Options
|
|
217
|
+
|
|
218
|
+
- **`maxSize`** (number): Maximum number of history items to keep. Default: `100`
|
|
219
|
+
|
|
220
|
+
## Advanced Usage
|
|
221
|
+
|
|
222
|
+
### Complex Batching with Markers
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
// Start a complex operation
|
|
226
|
+
state.history.batch();
|
|
227
|
+
state.history.addMarker('Start user registration');
|
|
228
|
+
|
|
229
|
+
state.set(['user', 'name'], 'Alice');
|
|
230
|
+
state.set(['user', 'email'], 'alice@example.com');
|
|
231
|
+
state.set(['user', 'preferences'], { theme: 'dark' });
|
|
232
|
+
|
|
233
|
+
state.history.commit();
|
|
234
|
+
|
|
235
|
+
// All changes are now a single undo step with a descriptive marker
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Pause and Resume Recording
|
|
239
|
+
|
|
240
|
+
```javascript
|
|
241
|
+
// Temporarily pause history recording (internal API)
|
|
242
|
+
state.plugins.history.pause();
|
|
243
|
+
|
|
244
|
+
state.set(['temp', 'data'], 'not recorded');
|
|
245
|
+
|
|
246
|
+
state.plugins.history.resume();
|
|
247
|
+
|
|
248
|
+
state.set(['tracked', 'data'], 'recorded'); // This will be recorded
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Reset History
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
// Clear all history (internal API)
|
|
255
|
+
state.plugins.history.reset();
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## How It Works
|
|
259
|
+
|
|
260
|
+
1. **Change Tracking**: The plugin automatically tracks all state changes
|
|
261
|
+
2. **Consolidation**: Multiple changes to the same path are consolidated
|
|
262
|
+
3. **Deferred Commits**: Changes are committed asynchronously for performance
|
|
263
|
+
4. **Markers**: Markers create logical boundaries for undo/redo operations
|
|
264
|
+
5. **Inversion**: Changes are inverted for undo operations
|
|
265
|
+
|
|
266
|
+
## Common Patterns
|
|
267
|
+
|
|
268
|
+
### Form Editing with Undo/Redo
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
// Track form edits
|
|
272
|
+
function handleFieldChange(field, value) {
|
|
273
|
+
const endBatch = state.history.batch();
|
|
274
|
+
state.set(['form', field], value);
|
|
275
|
+
state.history.addMarker(`Update ${field}`);
|
|
276
|
+
endBatch();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Implement undo/redo buttons
|
|
280
|
+
function handleUndo() {
|
|
281
|
+
if (state.history.canUndo()) {
|
|
282
|
+
state.history.undo(() => {
|
|
283
|
+
updateUI();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Multi-Step Operations
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
function performComplexOperation() {
|
|
293
|
+
const endBatch = state.history.batch();
|
|
294
|
+
|
|
295
|
+
// Step 1: Update user
|
|
296
|
+
state.set(['user', 'status'], 'processing');
|
|
297
|
+
|
|
298
|
+
// Step 2: Create records
|
|
299
|
+
state.set(['records'], [{ id: 1, status: 'new' }]);
|
|
300
|
+
|
|
301
|
+
// Step 3: Update timestamp
|
|
302
|
+
state.set(['lastUpdate'], Date.now());
|
|
303
|
+
|
|
304
|
+
endBatch();
|
|
305
|
+
state.history.addMarker('Complex operation completed');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// All steps undo/redo as one operation
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## License
|
|
312
|
+
|
|
313
|
+
See the root [LICENSE](../../LICENSE) file for license information.
|
|
314
|
+
|
|
315
|
+
## Related Packages
|
|
316
|
+
|
|
317
|
+
- [@jucio.io/state](../../core) - Core state management system
|
|
318
|
+
- [@jucio.io/state/matcher](../matcher) - Path pattern matching
|
|
319
|
+
- [@jucio.io/state/on-change](../on-change) - Change listeners
|
|
320
|
+
|