statewire 0.0.0-0 → 0.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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -1
  3. package/dist/client/changes.d.ts +7 -0
  4. package/dist/client/changes.d.ts.map +1 -0
  5. package/dist/client/changes.js +32 -0
  6. package/dist/client/changes.js.map +1 -0
  7. package/dist/client/connection.d.ts +84 -0
  8. package/dist/client/connection.d.ts.map +1 -0
  9. package/dist/client/connection.js +204 -0
  10. package/dist/client/connection.js.map +1 -0
  11. package/dist/client/core.d.ts +18 -0
  12. package/dist/client/core.d.ts.map +1 -0
  13. package/dist/client/core.js +269 -0
  14. package/dist/client/core.js.map +1 -0
  15. package/dist/client/optimistic.d.ts +10 -0
  16. package/dist/client/optimistic.d.ts.map +1 -0
  17. package/dist/client/optimistic.js +128 -0
  18. package/dist/client/optimistic.js.map +1 -0
  19. package/dist/client/session.d.ts +88 -0
  20. package/dist/client/session.d.ts.map +1 -0
  21. package/dist/client/session.js +509 -0
  22. package/dist/client/session.js.map +1 -0
  23. package/dist/client/sessionStore.d.ts +16 -0
  24. package/dist/client/sessionStore.d.ts.map +1 -0
  25. package/dist/client/sessionStore.js +14 -0
  26. package/dist/client/sessionStore.js.map +1 -0
  27. package/dist/client/timing.d.ts +10 -0
  28. package/dist/client/timing.d.ts.map +1 -0
  29. package/dist/client/timing.js +11 -0
  30. package/dist/client/timing.js.map +1 -0
  31. package/dist/host/draft.d.ts +14 -0
  32. package/dist/host/draft.d.ts.map +1 -0
  33. package/dist/host/draft.js +124 -0
  34. package/dist/host/draft.js.map +1 -0
  35. package/dist/host/engine.d.ts +14 -0
  36. package/dist/host/engine.d.ts.map +1 -0
  37. package/dist/host/engine.js +17 -0
  38. package/dist/host/engine.js.map +1 -0
  39. package/dist/host/host.d.ts +79 -0
  40. package/dist/host/host.d.ts.map +1 -0
  41. package/dist/host/host.js +303 -0
  42. package/dist/host/host.js.map +1 -0
  43. package/dist/index.d.ts +28 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +6 -0
  46. package/dist/server.d.ts +15 -0
  47. package/dist/server.d.ts.map +1 -0
  48. package/dist/server.js +3 -0
  49. package/dist/statewire.d.ts +20 -0
  50. package/dist/statewire.d.ts.map +1 -0
  51. package/dist/statewire.js +65 -0
  52. package/dist/statewire.js.map +1 -0
  53. package/dist/transports/remote.d.ts +19 -0
  54. package/dist/transports/remote.d.ts.map +1 -0
  55. package/dist/transports/remote.js +40 -0
  56. package/dist/transports/remote.js.map +1 -0
  57. package/dist/transports/sse-parser.d.ts +5 -0
  58. package/dist/transports/sse-parser.d.ts.map +1 -0
  59. package/dist/transports/sse-parser.js +26 -0
  60. package/dist/transports/sse-parser.js.map +1 -0
  61. package/dist/transports/sse.d.ts +74 -0
  62. package/dist/transports/sse.d.ts.map +1 -0
  63. package/dist/transports/sse.js +136 -0
  64. package/dist/transports/sse.js.map +1 -0
  65. package/dist/transports/testing.d.ts +31 -0
  66. package/dist/transports/testing.d.ts.map +1 -0
  67. package/dist/transports/testing.js +121 -0
  68. package/dist/transports/testing.js.map +1 -0
  69. package/dist/transports/ws.d.ts +18 -0
  70. package/dist/transports/ws.d.ts.map +1 -0
  71. package/dist/transports/ws.js +172 -0
  72. package/dist/transports/ws.js.map +1 -0
  73. package/dist/types.d.ts +107 -0
  74. package/dist/types.d.ts.map +1 -0
  75. package/dist/types.js +24 -0
  76. package/dist/types.js.map +1 -0
  77. package/dist/wire/meta.d.ts +22 -0
  78. package/dist/wire/meta.d.ts.map +1 -0
  79. package/dist/wire/meta.js +73 -0
  80. package/dist/wire/meta.js.map +1 -0
  81. package/dist/wire/ops.d.ts +40 -0
  82. package/dist/wire/ops.d.ts.map +1 -0
  83. package/dist/wire/ops.js +125 -0
  84. package/dist/wire/ops.js.map +1 -0
  85. package/package.json +49 -6
  86. package/src/client/changes.ts +48 -0
  87. package/src/client/connection.ts +327 -0
  88. package/src/client/core.ts +346 -0
  89. package/src/client/optimistic.ts +0 -0
  90. package/src/client/session.ts +679 -0
  91. package/src/client/sessionStore.ts +23 -0
  92. package/src/client/timing.ts +8 -0
  93. package/src/host/draft.ts +155 -0
  94. package/src/host/engine.ts +13 -0
  95. package/src/host/host.ts +481 -0
  96. package/src/index.ts +65 -0
  97. package/src/server.ts +32 -0
  98. package/src/statewire.ts +107 -0
  99. package/src/transports/remote.ts +72 -0
  100. package/src/transports/sse-parser.ts +24 -0
  101. package/src/transports/sse.ts +280 -0
  102. package/src/transports/testing.ts +155 -0
  103. package/src/transports/ws.ts +272 -0
  104. package/src/types.ts +169 -0
  105. package/src/wire/meta.ts +106 -0
  106. package/src/wire/ops.ts +201 -0
  107. package/index.js +0 -1
@@ -0,0 +1,201 @@
1
+ import { recordMutations } from "../client/optimistic";
2
+
3
+ export type StatewirePath = readonly (string | number)[];
4
+
5
+ export type StatewireOp =
6
+ | { op: "replace"; path: StatewirePath; value: unknown }
7
+ | { op: "add"; path: StatewirePath; value: unknown }
8
+ | { op: "append"; path: StatewirePath; value: unknown }
9
+ | { op: "remove"; path: StatewirePath }
10
+ | { op: "event"; value: object };
11
+
12
+ const isUnsafe = (key: string, parent: string | number | undefined) =>
13
+ key === "__proto__" || (key === "prototype" && parent === "constructor");
14
+
15
+ const warnStripped = (key: string, parent: string | undefined) => {
16
+ if (
17
+ typeof process === "undefined" ||
18
+ process.env["NODE_ENV"] !== "production"
19
+ ) {
20
+ console.warn(
21
+ `statewire: stripped unsafe key "${key}"` +
22
+ (parent === undefined ? "" : ` under "${parent}"`),
23
+ );
24
+ }
25
+ };
26
+
27
+ export const sanitizeValue = (value: unknown, parent?: string): unknown => {
28
+ if (typeof value !== "object" || value === null) return value;
29
+ if (value instanceof Date) return new Date(value.getTime());
30
+ if (Array.isArray(value)) return value.map((item) => sanitizeValue(item));
31
+ const out: Record<string, unknown> = {};
32
+ for (const key of Object.keys(value)) {
33
+ if (isUnsafe(key, parent)) warnStripped(key, parent);
34
+ else out[key] = sanitizeValue((value as Record<string, unknown>)[key], key);
35
+ }
36
+ return out;
37
+ };
38
+
39
+ type MutableOp = Extract<
40
+ StatewireOp,
41
+ { op: "replace" | "add" | "append" | "remove" }
42
+ >;
43
+
44
+ const invalid = (op: MutableOp) =>
45
+ new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(".")}`);
46
+
47
+ const appended = (
48
+ current: unknown,
49
+ op: Extract<MutableOp, { op: "append" }>,
50
+ ): string => {
51
+ if (typeof current !== "string" || typeof op.value !== "string") {
52
+ throw invalid(op);
53
+ }
54
+ return current + op.value;
55
+ };
56
+
57
+ const applyAtRoot = (box: { state: unknown }, op: MutableOp) => {
58
+ if (op.op === "replace") {
59
+ box.state = sanitizeValue(op.value);
60
+ } else if (op.op === "append") {
61
+ box.state = appended(box.state, op);
62
+ } else {
63
+ throw invalid(op);
64
+ }
65
+ };
66
+
67
+ const applyInList = (list: unknown[], index: number, op: MutableOp) => {
68
+ if (op.op === "replace") {
69
+ if (index >= list.length) throw invalid(op);
70
+ list[index] = sanitizeValue(op.value);
71
+ } else if (op.op === "add") {
72
+ if (index > list.length) throw invalid(op);
73
+ list.splice(index, 0, sanitizeValue(op.value));
74
+ } else if (op.op === "append") {
75
+ if (index >= list.length) throw invalid(op);
76
+ list[index] = appended(list[index], op);
77
+ } else {
78
+ if (index >= list.length) throw invalid(op);
79
+ list.splice(index, 1);
80
+ }
81
+ };
82
+
83
+ const applyInObject = (
84
+ obj: Record<string, unknown>,
85
+ key: string,
86
+ op: MutableOp,
87
+ ) => {
88
+ const exists = Object.hasOwn(obj, key);
89
+ if (op.op === "replace") {
90
+ obj[key] = sanitizeValue(op.value, key);
91
+ } else if (op.op === "append") {
92
+ obj[key] = appended(exists ? obj[key] : undefined, op);
93
+ } else if (op.op === "remove") {
94
+ if (!exists) throw invalid(op);
95
+ delete obj[key];
96
+ } else {
97
+ throw invalid(op);
98
+ }
99
+ };
100
+
101
+ const applyOpToDraft = (box: { state: unknown }, op: MutableOp): void => {
102
+ if (op.path.length === 0) {
103
+ applyAtRoot(box, op);
104
+ return;
105
+ }
106
+ let node: unknown = box.state;
107
+ for (let depth = 0; depth < op.path.length; depth++) {
108
+ const key = op.path[depth]!;
109
+ const last = depth === op.path.length - 1;
110
+ if (typeof key === "number") {
111
+ if (!Array.isArray(node) || !Number.isInteger(key) || key < 0) {
112
+ throw invalid(op);
113
+ }
114
+ if (last) {
115
+ applyInList(node, key, op);
116
+ return;
117
+ }
118
+ if (key >= node.length) throw invalid(op);
119
+ node = node[key];
120
+ } else {
121
+ if (
122
+ isUnsafe(key, op.path[depth - 1]) ||
123
+ typeof node !== "object" ||
124
+ node === null ||
125
+ Array.isArray(node)
126
+ ) {
127
+ throw invalid(op);
128
+ }
129
+ const obj = node as Record<string, unknown>;
130
+ if (last) {
131
+ applyInObject(obj, key, op);
132
+ return;
133
+ }
134
+ if (!Object.hasOwn(obj, key)) throw invalid(op);
135
+ node = obj[key];
136
+ }
137
+ }
138
+ };
139
+
140
+ const warnedOps = new Set<unknown>();
141
+ const warnUnknownOp = (type: unknown) => {
142
+ if (warnedOps.has(type)) return;
143
+ warnedOps.add(type);
144
+ console.warn(`statewire: ignoring unknown op type ${JSON.stringify(type)}`);
145
+ };
146
+
147
+ const unboxPaths = (paths: readonly StatewirePath[]): StatewirePath[] =>
148
+ paths.map((path) => path.slice(1));
149
+
150
+ export const applyOp = (
151
+ root: unknown,
152
+ op: MutableOp,
153
+ ): { state: unknown; changedPaths: StatewirePath[] } => {
154
+ const recorded = recordMutations({ state: root }, (draft) =>
155
+ applyOpToDraft(draft, op),
156
+ );
157
+ return {
158
+ state: recorded.state.state,
159
+ changedPaths: unboxPaths(recorded.changedPaths),
160
+ };
161
+ };
162
+
163
+ export const applyOps = (
164
+ state: unknown,
165
+ ops: readonly unknown[],
166
+ ): {
167
+ state: unknown;
168
+ events: { event: unknown; state: unknown }[];
169
+ changedPaths: StatewirePath[];
170
+ } => {
171
+ const events: { event: unknown; state: unknown }[] = [];
172
+ const changedPaths: StatewirePath[] = [];
173
+ let segment: MutableOp[] = [];
174
+ const flush = () => {
175
+ if (segment.length === 0) return;
176
+ const ops = segment;
177
+ segment = [];
178
+ const recorded = recordMutations({ state }, (draft) => {
179
+ for (const op of ops) applyOpToDraft(draft, op);
180
+ });
181
+ state = recorded.state.state;
182
+ changedPaths.push(...unboxPaths(recorded.changedPaths));
183
+ };
184
+ for (const op of ops as StatewireOp[]) {
185
+ if (op.op === "event") {
186
+ flush();
187
+ events.push({ event: sanitizeValue(op.value), state });
188
+ } else if (
189
+ op.op === "replace" ||
190
+ op.op === "add" ||
191
+ op.op === "append" ||
192
+ op.op === "remove"
193
+ ) {
194
+ segment.push(op);
195
+ } else {
196
+ warnUnknownOp((op as { op: unknown }).op);
197
+ }
198
+ }
199
+ flush();
200
+ return { state, events, changedPaths };
201
+ };
package/index.js DELETED
@@ -1 +0,0 @@
1
- throw new Error("statewire: placeholder release — not yet published");