statewire 0.12.0 → 0.12.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"draft.d.ts","names":[],"sources":["../../src/host/draft.ts"],"mappings":";;;KAMY;EACV;EACA,SAAS,IAAI,UAAU,IAAI,aAAa;;KAG9B,WAAW;EACrB,OAAO;;EAEP;;EAEA;;cA6BW,mBAAoB,GAAG,MAAM,cAAY,WAAW;;;;;;;cAsQpD,SAAU,8BACrB,oBAAoB,KAAK,kBACzB,KAAK,MACL"}
1
+ {"version":3,"file":"draft.d.ts","names":[],"sources":["../../src/host/draft.ts"],"mappings":";;;KAMY;EACV;EACA,SAAS,IAAI,UAAU,IAAI,aAAa;;KAG9B,WAAW;EACrB,OAAO;;EAEP;;EAEA;;cAuCW,mBAAoB,GAAG,MAAM,cAAY,WAAW;;;;;;;cA0QpD,SAAU,8BACrB,oBAAoB,KAAK,kBACzB,KAAK,MACL"}
@@ -5,6 +5,14 @@ import { sanitizeValue } from "../wire/ops.js";
5
5
  const NODE = Symbol("statewire.draftNode");
6
6
  const isIndex = (key) => /^(0|[1-9]\d*)$/.test(key);
7
7
  const invalid = (path, message) => /* @__PURE__ */ new Error(`statewire: ${message} at path: ${path.join(".")}`);
8
+ const codePointLength = (text) => {
9
+ let count = 0;
10
+ for (let index = 0; index < text.length; index += 1) {
11
+ if (text.codePointAt(index) > 65535) index += 1;
12
+ count += 1;
13
+ }
14
+ return count;
15
+ };
8
16
  const createStateDraft = (host) => {
9
17
  let gen = 0;
10
18
  const dirty = [];
@@ -45,7 +53,7 @@ const createStateDraft = (host) => {
45
53
  path: [
46
54
  ...node.path,
47
55
  key,
48
- existing.length
56
+ codePointLength(existing)
49
57
  ],
50
58
  value: suffix
51
59
  }, changedPathFor(node, key));
@@ -1 +1 @@
1
- {"version":3,"file":"draft.js","names":["Statewire","StatewirePath","sanitizeValue","isUnsafeKey","sharedDraftTraps","DraftHost","snapshot","record","op","Op","changedPath","StateDraft","proxy","T","flush","invalidate","NODE","Symbol","NodeHandle","append","key","suffix","isIndex","test","invalid","path","message","Error","join","Container","Record","Node","parent","gen","base","copy","children","Map","isArray","handle","createStateDraft","host","dirty","latest","node","resolve","value","prepareForWrite","Array","slice","push","changedPathFor","applyAppend","rawKey","container","Number","existing","Object","hasOwn","undefined","length","createNode","seed","childKey","current","childProxy","prop","child","get","set","assign","old","isObjectContainer","Proxy","_target","c","next","isInteger","String","i","splice","index","deleteProperty","has","Reflect","root","TKey"],"sources":["../../src/host/draft.ts"],"sourcesContent":["import type { Statewire } from \"../types\";\nimport type { StatewirePath } from \"../wire/ops\";\nimport { sanitizeValue } from \"../wire/ops\";\nimport { isUnsafeKey } from \"../util/statePath\";\nimport { sharedDraftTraps } from \"../util/draftTraps\";\n\nexport type DraftHost = {\n snapshot: () => unknown;\n record: (op: Statewire.Op, changedPath: StatewirePath) => void;\n};\n\nexport type StateDraft<T> = {\n proxy: T;\n /** Seal pending writes and return the committed state. */\n flush: () => unknown;\n /** Committed state was replaced externally; cached nodes re-resolve. */\n invalidate: () => void;\n};\n\nconst NODE = Symbol(\"statewire.draftNode\");\n\ntype NodeHandle = {\n append: (key: string | number, suffix: string) => void;\n};\n\nconst isIndex = (key: string) => /^(0|[1-9]\\d*)$/.test(key);\n\nconst invalid = (path: StatewirePath, message: string) =>\n new Error(`statewire: ${message} at path: ${path.join(\".\")}`);\n\ntype Container = Record<string | number, unknown>;\n\ntype Node = {\n parent: Node | null;\n key: string | number;\n path: StatewirePath;\n gen: number;\n base: unknown;\n copy: Container | null;\n children: Map<string, Node>;\n isArray: boolean;\n proxy: object;\n handle: NodeHandle;\n};\n\nexport const createStateDraft = <T>(host: DraftHost): StateDraft<T> => {\n let gen = 0;\n const dirty: Node[] = [];\n\n const latest = (node: Node): unknown => node.copy ?? node.base;\n\n const resolve = (node: Node): Node => {\n if (node.gen === gen) return node;\n node.gen = gen;\n if (node.parent === null) {\n if (node.copy === null) node.base = host.snapshot();\n return node;\n }\n const value = (latest(resolve(node.parent)) as Container)[node.key];\n if (value !== latest(node)) {\n node.base = value;\n node.copy = null;\n }\n return node;\n };\n\n const prepareForWrite = (node: Node): Container => {\n resolve(node);\n if (node.copy !== null) return node.copy;\n const base = node.base;\n node.copy = Array.isArray(base)\n ? (base.slice() as unknown as Container)\n : { ...(base as Container) };\n dirty.push(node);\n if (node.parent !== null) {\n prepareForWrite(node.parent)[node.key] = node.copy;\n }\n return node.copy;\n };\n\n const changedPathFor = (node: Node, key: string | number): StatewirePath =>\n Array.isArray(latest(node)) ? node.path : [...node.path, key];\n\n const applyAppend = (node: Node, rawKey: string | number, suffix: string) => {\n const container = latest(resolve(node)) as Container;\n const key =\n Array.isArray(container) && typeof rawKey === \"string\" && isIndex(rawKey)\n ? Number(rawKey)\n : rawKey;\n const existing = Object.hasOwn(container as object, key)\n ? container[key]\n : undefined;\n if (typeof existing !== \"string\") {\n throw invalid([...node.path, key], \"cannot append to a non-string\");\n }\n if (suffix === \"\") return;\n prepareForWrite(node)[key] = existing + suffix;\n host.record(\n { op: \"add\", path: [...node.path, key, existing.length], value: suffix },\n changedPathFor(node, key),\n );\n };\n\n const createNode = (parent: Node | null, key: string | number): Node => {\n const seed =\n parent === null ? host.snapshot() : (latest(parent) as Container)[key];\n const node: Node = {\n parent,\n key,\n path: parent === null ? [] : [...parent.path, key],\n gen,\n base: seed,\n copy: null,\n children: new Map(),\n isArray: Array.isArray(seed),\n proxy: null!,\n handle: null!,\n };\n node.handle = {\n append: (childKey, suffix) => applyAppend(node, childKey, suffix),\n };\n\n const current = () => latest(resolve(node));\n\n const childProxy = (\n prop: string,\n childKey: string | number,\n value: object,\n ) => {\n let child = node.children.get(prop);\n if (child === undefined || child.isArray !== Array.isArray(value)) {\n child = createNode(node, childKey);\n node.children.set(prop, child);\n }\n return child.proxy;\n };\n\n const assign = (\n container: Container,\n key: string | number,\n value: unknown,\n ) => {\n const old = Object.hasOwn(container as object, key)\n ? container[key]\n : undefined;\n if (old === value) return;\n const isObjectContainer = !Array.isArray(container);\n if (\n isObjectContainer &&\n isUnsafeKey(key, node.path[node.path.length - 1])\n ) {\n throw invalid([...node.path, key], `cannot set unsafe key \"${key}\"`);\n }\n if (value === undefined) {\n if (!isObjectContainer) {\n throw invalid([...node.path, key], \"cannot serialize undefined\");\n }\n delete prepareForWrite(node)[key];\n host.record({ op: \"remove\", path: [...node.path, key] }, [\n ...node.path,\n key,\n ]);\n gen++;\n return;\n }\n prepareForWrite(node)[key] = isObjectContainer\n ? sanitizeValue(value, key as string)\n : sanitizeValue(value);\n host.record(\n { op: \"replace\", path: [...node.path, key], value },\n changedPathFor(node, key),\n );\n if (\n (typeof old === \"object\" && old !== null) ||\n (typeof value === \"object\" && value !== null)\n ) {\n gen++;\n }\n };\n\n node.proxy = new Proxy(seed as object, {\n get: (_target, prop) => {\n if (prop === NODE) return node.handle;\n const container = current();\n if (typeof prop === \"symbol\") {\n return (container as Record<symbol, unknown>)[prop];\n }\n const c = container as Container;\n const key = Array.isArray(c) && isIndex(prop) ? Number(prop) : prop;\n const value = c[key];\n if (typeof value === \"object\" && value !== null) {\n return childProxy(prop, key, value);\n }\n return value;\n },\n set: (_target, prop, value) => {\n if (typeof prop === \"symbol\") return false;\n const container = current();\n if (Array.isArray(container)) {\n if (prop === \"length\") {\n const next = Number(value);\n if (\n !Number.isInteger(next) ||\n next < 0 ||\n next > container.length\n ) {\n throw invalid(\n node.path,\n `cannot set list length to ${String(value)}`,\n );\n }\n if (next === container.length) return true;\n const copy = prepareForWrite(node) as unknown as unknown[];\n for (let i = container.length - 1; i >= next; i--) {\n copy.splice(i, 1);\n host.record({ op: \"remove\", path: [...node.path, i] }, node.path);\n }\n gen++;\n return true;\n }\n if (!isIndex(prop)) {\n throw invalid(node.path, `cannot set key \"${prop}\" on a list`);\n }\n const index = Number(prop);\n if (index > container.length) {\n throw invalid(node.path, `list index ${index} out of range`);\n }\n if (index === container.length) {\n if (value === undefined) {\n throw invalid(\n [...node.path, index],\n \"cannot serialize undefined\",\n );\n }\n prepareForWrite(node)[index] = sanitizeValue(value);\n host.record(\n { op: \"add\", path: [...node.path, index], value },\n node.path,\n );\n gen++;\n return true;\n }\n assign(container as unknown as Container, index, value);\n return true;\n }\n assign(container as Container, prop, value);\n return true;\n },\n deleteProperty: (_target, prop) => {\n if (typeof prop === \"symbol\") return false;\n const container = current();\n if (Array.isArray(container)) {\n if (!isIndex(prop) || Number(prop) >= container.length) return true;\n const index = Number(prop);\n (prepareForWrite(node) as unknown as unknown[]).splice(index, 1);\n host.record({ op: \"remove\", path: [...node.path, index] }, node.path);\n gen++;\n return true;\n }\n if (!Object.hasOwn(container as object, prop)) return true;\n if (isUnsafeKey(prop, node.path[node.path.length - 1])) {\n throw invalid(\n [...node.path, prop],\n `cannot remove unsafe key \"${prop}\"`,\n );\n }\n delete prepareForWrite(node)[prop];\n host.record({ op: \"remove\", path: [...node.path, prop] }, [\n ...node.path,\n prop,\n ]);\n gen++;\n return true;\n },\n has: (_target, prop) => Reflect.has(current() as object, prop),\n ...sharedDraftTraps(() => current() as object, \"on a state draft\"),\n });\n return node;\n };\n\n const root = createNode(null, \"\");\n\n return {\n proxy: root.proxy as T,\n flush: () => {\n if (dirty.length === 0) return host.snapshot();\n for (const node of dirty) {\n if (node.copy !== null) {\n node.base = node.copy;\n node.copy = null;\n }\n }\n dirty.length = 0;\n return root.base;\n },\n invalidate: () => {\n gen++;\n },\n };\n};\n\n/**\n * Append `suffix` to the string at `parent[key]` on a statewire state draft,\n * recording a single end-offset text-insert `add` op instead of a full\n * `replace` — use it on token-streaming hot paths. Throws when the current\n * value is not a string.\n */\nexport const append = <TKey extends string | number>(\n parent: { readonly [P in TKey]: string },\n key: TKey,\n suffix: string,\n): void => {\n const handle = (parent as { [NODE]?: NodeHandle })[NODE];\n if (handle === undefined) {\n throw new Error(\"statewire: append expects a state draft object\");\n }\n if (typeof suffix !== \"string\") {\n throw new Error(\"statewire: append suffix must be a string\");\n }\n handle.append(key, suffix);\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"draft.js","names":["Statewire","StatewirePath","sanitizeValue","isUnsafeKey","sharedDraftTraps","DraftHost","snapshot","record","op","Op","changedPath","StateDraft","proxy","T","flush","invalidate","NODE","Symbol","NodeHandle","append","key","suffix","isIndex","test","invalid","path","message","Error","join","Container","Record","codePointLength","text","count","index","length","codePointAt","Node","parent","gen","base","copy","children","Map","isArray","handle","createStateDraft","host","dirty","latest","node","resolve","value","prepareForWrite","Array","slice","push","changedPathFor","applyAppend","rawKey","container","Number","existing","Object","hasOwn","undefined","createNode","seed","childKey","current","childProxy","prop","child","get","set","assign","old","isObjectContainer","Proxy","_target","c","next","isInteger","String","i","splice","deleteProperty","has","Reflect","root","TKey"],"sources":["../../src/host/draft.ts"],"sourcesContent":["import type { Statewire } from \"../types\";\nimport type { StatewirePath } from \"../wire/ops\";\nimport { sanitizeValue } from \"../wire/ops\";\nimport { isUnsafeKey } from \"../util/statePath\";\nimport { sharedDraftTraps } from \"../util/draftTraps\";\n\nexport type DraftHost = {\n snapshot: () => unknown;\n record: (op: Statewire.Op, changedPath: StatewirePath) => void;\n};\n\nexport type StateDraft<T> = {\n proxy: T;\n /** Seal pending writes and return the committed state. */\n flush: () => unknown;\n /** Committed state was replaced externally; cached nodes re-resolve. */\n invalidate: () => void;\n};\n\nconst NODE = Symbol(\"statewire.draftNode\");\n\ntype NodeHandle = {\n append: (key: string | number, suffix: string) => void;\n};\n\nconst isIndex = (key: string) => /^(0|[1-9]\\d*)$/.test(key);\n\nconst invalid = (path: StatewirePath, message: string) =>\n new Error(`statewire: ${message} at path: ${path.join(\".\")}`);\n\ntype Container = Record<string | number, unknown>;\n\n// Text-insert offsets are Unicode code points on the wire, not UTF-16 units.\nconst codePointLength = (text: string): number => {\n let count = 0;\n for (let index = 0; index < text.length; index += 1) {\n if (text.codePointAt(index)! > 0xffff) index += 1;\n count += 1;\n }\n return count;\n};\n\ntype Node = {\n parent: Node | null;\n key: string | number;\n path: StatewirePath;\n gen: number;\n base: unknown;\n copy: Container | null;\n children: Map<string, Node>;\n isArray: boolean;\n proxy: object;\n handle: NodeHandle;\n};\n\nexport const createStateDraft = <T>(host: DraftHost): StateDraft<T> => {\n let gen = 0;\n const dirty: Node[] = [];\n\n const latest = (node: Node): unknown => node.copy ?? node.base;\n\n const resolve = (node: Node): Node => {\n if (node.gen === gen) return node;\n node.gen = gen;\n if (node.parent === null) {\n if (node.copy === null) node.base = host.snapshot();\n return node;\n }\n const value = (latest(resolve(node.parent)) as Container)[node.key];\n if (value !== latest(node)) {\n node.base = value;\n node.copy = null;\n }\n return node;\n };\n\n const prepareForWrite = (node: Node): Container => {\n resolve(node);\n if (node.copy !== null) return node.copy;\n const base = node.base;\n node.copy = Array.isArray(base)\n ? (base.slice() as unknown as Container)\n : { ...(base as Container) };\n dirty.push(node);\n if (node.parent !== null) {\n prepareForWrite(node.parent)[node.key] = node.copy;\n }\n return node.copy;\n };\n\n const changedPathFor = (node: Node, key: string | number): StatewirePath =>\n Array.isArray(latest(node)) ? node.path : [...node.path, key];\n\n const applyAppend = (node: Node, rawKey: string | number, suffix: string) => {\n const container = latest(resolve(node)) as Container;\n const key =\n Array.isArray(container) && typeof rawKey === \"string\" && isIndex(rawKey)\n ? Number(rawKey)\n : rawKey;\n const existing = Object.hasOwn(container as object, key)\n ? container[key]\n : undefined;\n if (typeof existing !== \"string\") {\n throw invalid([...node.path, key], \"cannot append to a non-string\");\n }\n if (suffix === \"\") return;\n prepareForWrite(node)[key] = existing + suffix;\n host.record(\n {\n op: \"add\",\n path: [...node.path, key, codePointLength(existing)],\n value: suffix,\n },\n changedPathFor(node, key),\n );\n };\n\n const createNode = (parent: Node | null, key: string | number): Node => {\n const seed =\n parent === null ? host.snapshot() : (latest(parent) as Container)[key];\n const node: Node = {\n parent,\n key,\n path: parent === null ? [] : [...parent.path, key],\n gen,\n base: seed,\n copy: null,\n children: new Map(),\n isArray: Array.isArray(seed),\n proxy: null!,\n handle: null!,\n };\n node.handle = {\n append: (childKey, suffix) => applyAppend(node, childKey, suffix),\n };\n\n const current = () => latest(resolve(node));\n\n const childProxy = (\n prop: string,\n childKey: string | number,\n value: object,\n ) => {\n let child = node.children.get(prop);\n if (child === undefined || child.isArray !== Array.isArray(value)) {\n child = createNode(node, childKey);\n node.children.set(prop, child);\n }\n return child.proxy;\n };\n\n const assign = (\n container: Container,\n key: string | number,\n value: unknown,\n ) => {\n const old = Object.hasOwn(container as object, key)\n ? container[key]\n : undefined;\n if (old === value) return;\n const isObjectContainer = !Array.isArray(container);\n if (\n isObjectContainer &&\n isUnsafeKey(key, node.path[node.path.length - 1])\n ) {\n throw invalid([...node.path, key], `cannot set unsafe key \"${key}\"`);\n }\n if (value === undefined) {\n if (!isObjectContainer) {\n throw invalid([...node.path, key], \"cannot serialize undefined\");\n }\n delete prepareForWrite(node)[key];\n host.record({ op: \"remove\", path: [...node.path, key] }, [\n ...node.path,\n key,\n ]);\n gen++;\n return;\n }\n prepareForWrite(node)[key] = isObjectContainer\n ? sanitizeValue(value, key as string)\n : sanitizeValue(value);\n host.record(\n { op: \"replace\", path: [...node.path, key], value },\n changedPathFor(node, key),\n );\n if (\n (typeof old === \"object\" && old !== null) ||\n (typeof value === \"object\" && value !== null)\n ) {\n gen++;\n }\n };\n\n node.proxy = new Proxy(seed as object, {\n get: (_target, prop) => {\n if (prop === NODE) return node.handle;\n const container = current();\n if (typeof prop === \"symbol\") {\n return (container as Record<symbol, unknown>)[prop];\n }\n const c = container as Container;\n const key = Array.isArray(c) && isIndex(prop) ? Number(prop) : prop;\n const value = c[key];\n if (typeof value === \"object\" && value !== null) {\n return childProxy(prop, key, value);\n }\n return value;\n },\n set: (_target, prop, value) => {\n if (typeof prop === \"symbol\") return false;\n const container = current();\n if (Array.isArray(container)) {\n if (prop === \"length\") {\n const next = Number(value);\n if (\n !Number.isInteger(next) ||\n next < 0 ||\n next > container.length\n ) {\n throw invalid(\n node.path,\n `cannot set list length to ${String(value)}`,\n );\n }\n if (next === container.length) return true;\n const copy = prepareForWrite(node) as unknown as unknown[];\n for (let i = container.length - 1; i >= next; i--) {\n copy.splice(i, 1);\n host.record({ op: \"remove\", path: [...node.path, i] }, node.path);\n }\n gen++;\n return true;\n }\n if (!isIndex(prop)) {\n throw invalid(node.path, `cannot set key \"${prop}\" on a list`);\n }\n const index = Number(prop);\n if (index > container.length) {\n throw invalid(node.path, `list index ${index} out of range`);\n }\n if (index === container.length) {\n if (value === undefined) {\n throw invalid(\n [...node.path, index],\n \"cannot serialize undefined\",\n );\n }\n prepareForWrite(node)[index] = sanitizeValue(value);\n host.record(\n { op: \"add\", path: [...node.path, index], value },\n node.path,\n );\n gen++;\n return true;\n }\n assign(container as unknown as Container, index, value);\n return true;\n }\n assign(container as Container, prop, value);\n return true;\n },\n deleteProperty: (_target, prop) => {\n if (typeof prop === \"symbol\") return false;\n const container = current();\n if (Array.isArray(container)) {\n if (!isIndex(prop) || Number(prop) >= container.length) return true;\n const index = Number(prop);\n (prepareForWrite(node) as unknown as unknown[]).splice(index, 1);\n host.record({ op: \"remove\", path: [...node.path, index] }, node.path);\n gen++;\n return true;\n }\n if (!Object.hasOwn(container as object, prop)) return true;\n if (isUnsafeKey(prop, node.path[node.path.length - 1])) {\n throw invalid(\n [...node.path, prop],\n `cannot remove unsafe key \"${prop}\"`,\n );\n }\n delete prepareForWrite(node)[prop];\n host.record({ op: \"remove\", path: [...node.path, prop] }, [\n ...node.path,\n prop,\n ]);\n gen++;\n return true;\n },\n has: (_target, prop) => Reflect.has(current() as object, prop),\n ...sharedDraftTraps(() => current() as object, \"on a state draft\"),\n });\n return node;\n };\n\n const root = createNode(null, \"\");\n\n return {\n proxy: root.proxy as T,\n flush: () => {\n if (dirty.length === 0) return host.snapshot();\n for (const node of dirty) {\n if (node.copy !== null) {\n node.base = node.copy;\n node.copy = null;\n }\n }\n dirty.length = 0;\n return root.base;\n },\n invalidate: () => {\n gen++;\n },\n };\n};\n\n/**\n * Append `suffix` to the string at `parent[key]` on a statewire state draft,\n * recording a single end-offset text-insert `add` op instead of a full\n * `replace` — use it on token-streaming hot paths. Throws when the current\n * value is not a string.\n */\nexport const append = <TKey extends string | number>(\n parent: { readonly [P in TKey]: string },\n key: TKey,\n suffix: string,\n): void => {\n const handle = (parent as { [NODE]?: NodeHandle })[NODE];\n if (handle === undefined) {\n throw new Error(\"statewire: append expects a state draft object\");\n }\n if (typeof suffix !== \"string\") {\n throw new Error(\"statewire: append suffix must be a string\");\n }\n handle.append(key, suffix);\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ops.d.ts","names":[],"sources":["../../src/wire/ops.ts"],"mappings":";;KAIY;cAwDC,gBAAiB,gBAAgB;cAsGjC,UACX,eACA,IAAI,UAAU;EACX;EAAgB,cAAc;;cAUtB,WACX,gBACA;EAEA;EACA,cAAc"}
1
+ {"version":3,"file":"ops.d.ts","names":[],"sources":["../../src/wire/ops.ts"],"mappings":";;KAIY;cAwDC,gBAAiB,gBAAgB;cAiIjC,UACX,eACA,IAAI,UAAU;EACX;EAAgB,cAAc;;cAUtB,WACX,gBACA;EAEA;EACA,cAAc"}
package/dist/wire/ops.js CHANGED
@@ -31,9 +31,33 @@ const sanitizeAt = (value, parent, path) => {
31
31
  };
32
32
  const sanitizeValue = (value, parent) => sanitizeAt(value, parent, parent === void 0 ? [] : [parent]);
33
33
  const invalid = (op) => /* @__PURE__ */ new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(".")}`);
34
+ const utf16Index = (text, offset) => {
35
+ let index = 0;
36
+ for (let remaining = offset; remaining > 0; remaining -= 1) {
37
+ if (index >= text.length) return -1;
38
+ index += text.codePointAt(index) > 65535 ? 2 : 1;
39
+ }
40
+ return index;
41
+ };
42
+ const countCodePoints = (text) => {
43
+ let count = 0;
44
+ for (let index = 0; index < text.length; index += 1) {
45
+ if (text.codePointAt(index) > 65535) index += 1;
46
+ count += 1;
47
+ }
48
+ return count;
49
+ };
50
+ let last = null;
34
51
  const insertedText = (current, op, offset) => {
35
- if (typeof op.value !== "string" || offset > current.length) throw invalid(op);
36
- return current.slice(0, offset) + op.value + current.slice(offset);
52
+ if (typeof op.value !== "string") throw invalid(op);
53
+ const index = last !== null && last.str === current && last.count === offset ? current.length : utf16Index(current, offset);
54
+ if (index < 0) throw invalid(op);
55
+ const next = current.slice(0, index) + op.value + current.slice(index);
56
+ if (index === current.length) last = {
57
+ str: next,
58
+ count: offset + countCodePoints(op.value)
59
+ };
60
+ return next;
37
61
  };
38
62
  const applyAdd = (box, op) => {
39
63
  const offset = op.path[op.path.length - 1];
@@ -1 +1 @@
1
- {"version":3,"file":"ops.js","names":["recordMutations","isUnsafeKey","walkStatePath","Statewire","StatewirePath","warnStripped","key","parent","process","env","console","warn","undefined","unserializable","kind","path","Error","length","join","sanitizeAt","value","Date","getTime","Array","isArray","map","item","index","out","Record","Object","keys","sanitizeValue","invalid","op","Op","insertedText","current","Extract","offset","slice","applyAdd","box","state","walked","Number","isInteger","target","leaf","splice","next","applyInList","list","applyInObject","obj","hasOwn","applyOpToDraft","isMountPlaneOp","doc","warnedOps","Set","warnUnknownOp","type","has","add","JSON","stringify","unboxPaths","paths","applyOp","root","changedPaths","recorded","draft","applyOps","ops","mutable","push"],"sources":["../../src/wire/ops.ts"],"sourcesContent":["import { recordMutations } from \"../util/recordMutations\";\nimport { isUnsafeKey, walkStatePath } from \"../util/statePath\";\nimport type { Statewire } from \"../types\";\n\nexport type StatewirePath = readonly (string | number)[];\n\nconst warnStripped = (key: string, parent: string | undefined) => {\n if (\n typeof process === \"undefined\" ||\n process.env[\"NODE_ENV\"] !== \"production\"\n ) {\n console.warn(\n `statewire: stripped unsafe key \"${key}\"` +\n (parent === undefined ? \"\" : ` under \"${parent}\"`),\n );\n }\n};\n\nconst unserializable = (\n kind: \"function\" | \"undefined\",\n path: readonly (string | number)[],\n): never => {\n throw new Error(\n `statewire: cannot serialize ${kind}` +\n (path.length === 0 ? \"\" : ` at ${path.join(\".\")}`),\n );\n};\n\nconst sanitizeAt = (\n value: unknown,\n parent: string | undefined,\n path: readonly (string | number)[],\n): unknown => {\n if (value instanceof Date) return new Date(value.getTime());\n if (Array.isArray(value)) {\n return value.map((item, index) => {\n if (item === undefined) unserializable(\"undefined\", [...path, index]);\n return sanitizeAt(item, undefined, [...path, index]);\n });\n }\n if (typeof value === \"object\" && value !== null) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n if (isUnsafeKey(key, parent)) {\n warnStripped(key, parent);\n continue;\n }\n const item = (value as Record<string, unknown>)[key];\n if (item === undefined) continue;\n out[key] = sanitizeAt(item, key, [...path, key]);\n }\n return out;\n }\n if (typeof value === \"function\") unserializable(\"function\", path);\n return value;\n};\n\n// One clone pass: strips unsafe keys (with a warning); explicit-undefined\n// object entries are omitted (delete semantics), functions and undefined\n// list items throw.\nexport const sanitizeValue = (value: unknown, parent?: string): unknown =>\n sanitizeAt(value, parent, parent === undefined ? [] : [parent]);\n\nconst invalid = (op: Statewire.Op) =>\n new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(\".\")}`);\n\nconst insertedText = (\n current: string,\n op: Extract<Statewire.Op, { op: \"add\" }>,\n offset: number,\n): string => {\n if (typeof op.value !== \"string\" || offset > current.length) {\n throw invalid(op);\n }\n return current.slice(0, offset) + op.value + current.slice(offset);\n};\n\nconst applyAdd = (\n box: { state: unknown },\n op: Extract<Statewire.Op, { op: \"add\" }>,\n) => {\n const offset = op.path[op.path.length - 1];\n const walked = walkStatePath(box.state, op.path.slice(0, -1), \"resolve\");\n if (\n walked === undefined ||\n typeof offset !== \"number\" ||\n !Number.isInteger(offset) ||\n offset < 0\n ) {\n throw invalid(op);\n }\n const target = walked.leaf;\n if (Array.isArray(target)) {\n if (offset > target.length) throw invalid(op);\n target.splice(offset, 0, sanitizeValue(op.value));\n } else if (typeof target === \"string\") {\n const next = insertedText(target, op, offset);\n if (op.path.length === 1) {\n box.state = next;\n } else {\n (walked.parent as Record<string | number, unknown>)[walked.key] = next;\n }\n } else {\n throw invalid(op);\n }\n};\n\nconst applyInList = (list: unknown[], index: number, op: Statewire.Op) => {\n if (index >= list.length) throw invalid(op);\n if (op.op === \"replace\") {\n list[index] = sanitizeValue(op.value);\n } else {\n list.splice(index, 1);\n }\n};\n\nconst applyInObject = (\n obj: Record<string, unknown>,\n key: string,\n op: Statewire.Op,\n) => {\n if (op.op === \"replace\") {\n obj[key] = sanitizeValue(op.value, key);\n } else {\n if (!Object.hasOwn(obj, key)) throw invalid(op);\n delete obj[key];\n }\n};\n\nconst applyOpToDraft = (box: { state: unknown }, op: Statewire.Op): void => {\n if (op.op === \"add\") {\n if (op.path.length === 0) throw invalid(op);\n applyAdd(box, op);\n return;\n }\n if (op.path.length === 0) {\n if (op.op !== \"replace\") throw invalid(op);\n box.state = sanitizeValue(op.value);\n return;\n }\n const walked = walkStatePath(box.state, op.path, \"parent\");\n if (walked === undefined) throw invalid(op);\n if (typeof walked.key === \"number\") {\n applyInList(walked.parent as unknown[], walked.key, op);\n } else {\n applyInObject(walked.parent as Record<string, unknown>, walked.key, op);\n }\n};\n\nconst isMountPlaneOp = (op: { op?: unknown; doc?: unknown }) =>\n \"doc\" in op || op.op === \"mount\" || op.op === \"unmount\";\n\nconst warnedOps = new Set<unknown>();\nconst warnUnknownOp = (type: unknown) => {\n if (warnedOps.has(type)) return;\n warnedOps.add(type);\n console.warn(`statewire: ignoring unknown op type ${JSON.stringify(type)}`);\n};\n\nconst unboxPaths = (paths: readonly StatewirePath[]): StatewirePath[] =>\n paths.map((path) => path.slice(1));\n\nexport const applyOp = (\n root: unknown,\n op: Statewire.Op,\n): { state: unknown; changedPaths: StatewirePath[] } => {\n const recorded = recordMutations({ state: root }, (draft) =>\n applyOpToDraft(draft, op),\n );\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n\nexport const applyOps = (\n state: unknown,\n ops: readonly unknown[],\n): {\n state: unknown;\n changedPaths: StatewirePath[];\n} => {\n const mutable: Statewire.Op[] = [];\n for (const op of ops as Statewire.Op[]) {\n if (isMountPlaneOp(op)) continue;\n if (op.op === \"replace\" || op.op === \"add\" || op.op === \"remove\") {\n mutable.push(op);\n } else {\n warnUnknownOp((op as { op: unknown }).op);\n }\n }\n const recorded = recordMutations({ state }, (draft) => {\n for (const op of mutable) applyOpToDraft(draft, op);\n });\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"ops.js","names":["recordMutations","isUnsafeKey","walkStatePath","Statewire","StatewirePath","warnStripped","key","parent","process","env","console","warn","undefined","unserializable","kind","path","Error","length","join","sanitizeAt","value","Date","getTime","Array","isArray","map","item","index","out","Record","Object","keys","sanitizeValue","invalid","op","Op","utf16Index","text","offset","remaining","codePointAt","countCodePoints","count","last","str","insertedText","current","Extract","memoized","next","slice","applyAdd","box","state","walked","Number","isInteger","target","leaf","splice","applyInList","list","applyInObject","obj","hasOwn","applyOpToDraft","isMountPlaneOp","doc","warnedOps","Set","warnUnknownOp","type","has","add","JSON","stringify","unboxPaths","paths","applyOp","root","changedPaths","recorded","draft","applyOps","ops","mutable","push"],"sources":["../../src/wire/ops.ts"],"sourcesContent":["import { recordMutations } from \"../util/recordMutations\";\nimport { isUnsafeKey, walkStatePath } from \"../util/statePath\";\nimport type { Statewire } from \"../types\";\n\nexport type StatewirePath = readonly (string | number)[];\n\nconst warnStripped = (key: string, parent: string | undefined) => {\n if (\n typeof process === \"undefined\" ||\n process.env[\"NODE_ENV\"] !== \"production\"\n ) {\n console.warn(\n `statewire: stripped unsafe key \"${key}\"` +\n (parent === undefined ? \"\" : ` under \"${parent}\"`),\n );\n }\n};\n\nconst unserializable = (\n kind: \"function\" | \"undefined\",\n path: readonly (string | number)[],\n): never => {\n throw new Error(\n `statewire: cannot serialize ${kind}` +\n (path.length === 0 ? \"\" : ` at ${path.join(\".\")}`),\n );\n};\n\nconst sanitizeAt = (\n value: unknown,\n parent: string | undefined,\n path: readonly (string | number)[],\n): unknown => {\n if (value instanceof Date) return new Date(value.getTime());\n if (Array.isArray(value)) {\n return value.map((item, index) => {\n if (item === undefined) unserializable(\"undefined\", [...path, index]);\n return sanitizeAt(item, undefined, [...path, index]);\n });\n }\n if (typeof value === \"object\" && value !== null) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n if (isUnsafeKey(key, parent)) {\n warnStripped(key, parent);\n continue;\n }\n const item = (value as Record<string, unknown>)[key];\n if (item === undefined) continue;\n out[key] = sanitizeAt(item, key, [...path, key]);\n }\n return out;\n }\n if (typeof value === \"function\") unserializable(\"function\", path);\n return value;\n};\n\n// One clone pass: strips unsafe keys (with a warning); explicit-undefined\n// object entries are omitted (delete semantics), functions and undefined\n// list items throw.\nexport const sanitizeValue = (value: unknown, parent?: string): unknown =>\n sanitizeAt(value, parent, parent === undefined ? [] : [parent]);\n\nconst invalid = (op: Statewire.Op) =>\n new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(\".\")}`);\n\n// Text-insert offsets are Unicode code points on the wire, not UTF-16 units.\nconst utf16Index = (text: string, offset: number): number => {\n let index = 0;\n for (let remaining = offset; remaining > 0; remaining -= 1) {\n if (index >= text.length) return -1;\n index += text.codePointAt(index)! > 0xffff ? 2 : 1;\n }\n return index;\n};\n\nconst countCodePoints = (text: string): number => {\n let count = 0;\n for (let index = 0; index < text.length; index += 1) {\n if (text.codePointAt(index)! > 0xffff) index += 1;\n count += 1;\n }\n return count;\n};\n\nlet last: { str: string; count: number } | null = null;\n\nconst insertedText = (\n current: string,\n op: Extract<Statewire.Op, { op: \"add\" }>,\n offset: number,\n): string => {\n if (typeof op.value !== \"string\") throw invalid(op);\n const memoized =\n last !== null && last.str === current && last.count === offset;\n const index = memoized ? current.length : utf16Index(current, offset);\n if (index < 0) throw invalid(op);\n const next = current.slice(0, index) + op.value + current.slice(index);\n if (index === current.length) {\n last = { str: next, count: offset + countCodePoints(op.value) };\n }\n return next;\n};\n\nconst applyAdd = (\n box: { state: unknown },\n op: Extract<Statewire.Op, { op: \"add\" }>,\n) => {\n const offset = op.path[op.path.length - 1];\n const walked = walkStatePath(box.state, op.path.slice(0, -1), \"resolve\");\n if (\n walked === undefined ||\n typeof offset !== \"number\" ||\n !Number.isInteger(offset) ||\n offset < 0\n ) {\n throw invalid(op);\n }\n const target = walked.leaf;\n if (Array.isArray(target)) {\n if (offset > target.length) throw invalid(op);\n target.splice(offset, 0, sanitizeValue(op.value));\n } else if (typeof target === \"string\") {\n const next = insertedText(target, op, offset);\n if (op.path.length === 1) {\n box.state = next;\n } else {\n (walked.parent as Record<string | number, unknown>)[walked.key] = next;\n }\n } else {\n throw invalid(op);\n }\n};\n\nconst applyInList = (list: unknown[], index: number, op: Statewire.Op) => {\n if (index >= list.length) throw invalid(op);\n if (op.op === \"replace\") {\n list[index] = sanitizeValue(op.value);\n } else {\n list.splice(index, 1);\n }\n};\n\nconst applyInObject = (\n obj: Record<string, unknown>,\n key: string,\n op: Statewire.Op,\n) => {\n if (op.op === \"replace\") {\n obj[key] = sanitizeValue(op.value, key);\n } else {\n if (!Object.hasOwn(obj, key)) throw invalid(op);\n delete obj[key];\n }\n};\n\nconst applyOpToDraft = (box: { state: unknown }, op: Statewire.Op): void => {\n if (op.op === \"add\") {\n if (op.path.length === 0) throw invalid(op);\n applyAdd(box, op);\n return;\n }\n if (op.path.length === 0) {\n if (op.op !== \"replace\") throw invalid(op);\n box.state = sanitizeValue(op.value);\n return;\n }\n const walked = walkStatePath(box.state, op.path, \"parent\");\n if (walked === undefined) throw invalid(op);\n if (typeof walked.key === \"number\") {\n applyInList(walked.parent as unknown[], walked.key, op);\n } else {\n applyInObject(walked.parent as Record<string, unknown>, walked.key, op);\n }\n};\n\nconst isMountPlaneOp = (op: { op?: unknown; doc?: unknown }) =>\n \"doc\" in op || op.op === \"mount\" || op.op === \"unmount\";\n\nconst warnedOps = new Set<unknown>();\nconst warnUnknownOp = (type: unknown) => {\n if (warnedOps.has(type)) return;\n warnedOps.add(type);\n console.warn(`statewire: ignoring unknown op type ${JSON.stringify(type)}`);\n};\n\nconst unboxPaths = (paths: readonly StatewirePath[]): StatewirePath[] =>\n paths.map((path) => path.slice(1));\n\nexport const applyOp = (\n root: unknown,\n op: Statewire.Op,\n): { state: unknown; changedPaths: StatewirePath[] } => {\n const recorded = recordMutations({ state: root }, (draft) =>\n applyOpToDraft(draft, op),\n );\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n\nexport const applyOps = (\n state: unknown,\n ops: readonly unknown[],\n): {\n state: unknown;\n changedPaths: StatewirePath[];\n} => {\n const mutable: Statewire.Op[] = [];\n for (const op of ops as Statewire.Op[]) {\n if (isMountPlaneOp(op)) continue;\n if (op.op === \"replace\" || op.op === \"add\" || op.op === \"remove\") {\n mutable.push(op);\n } else {\n warnUnknownOp((op as { op: unknown }).op);\n }\n }\n const recorded = recordMutations({ state }, (draft) => {\n for (const op of mutable) applyOpToDraft(draft, op);\n });\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statewire",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "State over the wire: snapshot-first delta streaming and command submission for any state and any command",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/host/draft.ts CHANGED
@@ -30,6 +30,16 @@ const invalid = (path: StatewirePath, message: string) =>
30
30
 
31
31
  type Container = Record<string | number, unknown>;
32
32
 
33
+ // Text-insert offsets are Unicode code points on the wire, not UTF-16 units.
34
+ const codePointLength = (text: string): number => {
35
+ let count = 0;
36
+ for (let index = 0; index < text.length; index += 1) {
37
+ if (text.codePointAt(index)! > 0xffff) index += 1;
38
+ count += 1;
39
+ }
40
+ return count;
41
+ };
42
+
33
43
  type Node = {
34
44
  parent: Node | null;
35
45
  key: string | number;
@@ -96,7 +106,11 @@ export const createStateDraft = <T>(host: DraftHost): StateDraft<T> => {
96
106
  if (suffix === "") return;
97
107
  prepareForWrite(node)[key] = existing + suffix;
98
108
  host.record(
99
- { op: "add", path: [...node.path, key, existing.length], value: suffix },
109
+ {
110
+ op: "add",
111
+ path: [...node.path, key, codePointLength(existing)],
112
+ value: suffix,
113
+ },
100
114
  changedPathFor(node, key),
101
115
  );
102
116
  };
package/src/wire/ops.ts CHANGED
@@ -64,15 +64,42 @@ export const sanitizeValue = (value: unknown, parent?: string): unknown =>
64
64
  const invalid = (op: Statewire.Op) =>
65
65
  new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(".")}`);
66
66
 
67
+ // Text-insert offsets are Unicode code points on the wire, not UTF-16 units.
68
+ const utf16Index = (text: string, offset: number): number => {
69
+ let index = 0;
70
+ for (let remaining = offset; remaining > 0; remaining -= 1) {
71
+ if (index >= text.length) return -1;
72
+ index += text.codePointAt(index)! > 0xffff ? 2 : 1;
73
+ }
74
+ return index;
75
+ };
76
+
77
+ const countCodePoints = (text: string): number => {
78
+ let count = 0;
79
+ for (let index = 0; index < text.length; index += 1) {
80
+ if (text.codePointAt(index)! > 0xffff) index += 1;
81
+ count += 1;
82
+ }
83
+ return count;
84
+ };
85
+
86
+ let last: { str: string; count: number } | null = null;
87
+
67
88
  const insertedText = (
68
89
  current: string,
69
90
  op: Extract<Statewire.Op, { op: "add" }>,
70
91
  offset: number,
71
92
  ): string => {
72
- if (typeof op.value !== "string" || offset > current.length) {
73
- throw invalid(op);
93
+ if (typeof op.value !== "string") throw invalid(op);
94
+ const memoized =
95
+ last !== null && last.str === current && last.count === offset;
96
+ const index = memoized ? current.length : utf16Index(current, offset);
97
+ if (index < 0) throw invalid(op);
98
+ const next = current.slice(0, index) + op.value + current.slice(index);
99
+ if (index === current.length) {
100
+ last = { str: next, count: offset + countCodePoints(op.value) };
74
101
  }
75
- return current.slice(0, offset) + op.value + current.slice(offset);
102
+ return next;
76
103
  };
77
104
 
78
105
  const applyAdd = (