@voidhash/mimic 0.0.1-alpha.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/README.md +17 -0
- package/package.json +33 -0
- package/src/Document.ts +256 -0
- package/src/FractionalIndex.ts +1249 -0
- package/src/Operation.ts +59 -0
- package/src/OperationDefinition.ts +23 -0
- package/src/OperationPath.ts +197 -0
- package/src/Presence.ts +142 -0
- package/src/Primitive.ts +32 -0
- package/src/Proxy.ts +8 -0
- package/src/ProxyEnvironment.ts +52 -0
- package/src/Transaction.ts +72 -0
- package/src/Transform.ts +13 -0
- package/src/client/ClientDocument.ts +1163 -0
- package/src/client/Rebase.ts +309 -0
- package/src/client/StateMonitor.ts +307 -0
- package/src/client/Transport.ts +318 -0
- package/src/client/WebSocketTransport.ts +572 -0
- package/src/client/errors.ts +145 -0
- package/src/client/index.ts +61 -0
- package/src/index.ts +12 -0
- package/src/primitives/Array.ts +457 -0
- package/src/primitives/Boolean.ts +128 -0
- package/src/primitives/Lazy.ts +89 -0
- package/src/primitives/Literal.ts +128 -0
- package/src/primitives/Number.ts +169 -0
- package/src/primitives/String.ts +189 -0
- package/src/primitives/Struct.ts +348 -0
- package/src/primitives/Tree.ts +1120 -0
- package/src/primitives/TreeNode.ts +113 -0
- package/src/primitives/Union.ts +329 -0
- package/src/primitives/shared.ts +122 -0
- package/src/server/ServerDocument.ts +267 -0
- package/src/server/errors.ts +90 -0
- package/src/server/index.ts +40 -0
- package/tests/Document.test.ts +556 -0
- package/tests/FractionalIndex.test.ts +377 -0
- package/tests/OperationPath.test.ts +151 -0
- package/tests/Presence.test.ts +321 -0
- package/tests/Primitive.test.ts +381 -0
- package/tests/client/ClientDocument.test.ts +1398 -0
- package/tests/client/WebSocketTransport.test.ts +992 -0
- package/tests/primitives/Array.test.ts +418 -0
- package/tests/primitives/Boolean.test.ts +126 -0
- package/tests/primitives/Lazy.test.ts +143 -0
- package/tests/primitives/Literal.test.ts +122 -0
- package/tests/primitives/Number.test.ts +133 -0
- package/tests/primitives/String.test.ts +128 -0
- package/tests/primitives/Struct.test.ts +311 -0
- package/tests/primitives/Tree.test.ts +467 -0
- package/tests/primitives/TreeNode.test.ts +50 -0
- package/tests/primitives/Union.test.ts +210 -0
- package/tests/server/ServerDocument.test.ts +528 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +8 -0
- package/tsdown.config.ts +18 -0
- package/vitest.mts +11 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import * as OperationDefinition from "../OperationDefinition";
|
|
3
|
+
import * as Operation from "../Operation";
|
|
4
|
+
import * as OperationPath from "../OperationPath";
|
|
5
|
+
import * as ProxyEnvironment from "../ProxyEnvironment";
|
|
6
|
+
import * as Transform from "../Transform";
|
|
7
|
+
import type { Primitive, PrimitiveInternal, MaybeUndefined, AnyPrimitive, Validator, InferState, InferProxy, InferSnapshot } from "../Primitive";
|
|
8
|
+
import { ValidationError } from "../Primitive";
|
|
9
|
+
import { runValidators } from "./shared";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Maps a schema definition to its state type.
|
|
14
|
+
* { name: StringPrimitive, age: NumberPrimitive } -> { name: string, age: number }
|
|
15
|
+
*/
|
|
16
|
+
export type InferStructState<TFields extends Record<string, AnyPrimitive>> = {
|
|
17
|
+
readonly [K in keyof TFields]: InferState<TFields[K]>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maps a schema definition to its snapshot type.
|
|
22
|
+
* Each field's snapshot type is inferred from the field primitive.
|
|
23
|
+
*/
|
|
24
|
+
export type InferStructSnapshot<TFields extends Record<string, AnyPrimitive>> = {
|
|
25
|
+
readonly [K in keyof TFields]: InferSnapshot<TFields[K]>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maps a schema definition to its proxy type.
|
|
30
|
+
* Provides nested field access + get()/set()/toSnapshot() methods for the whole struct.
|
|
31
|
+
*/
|
|
32
|
+
export type StructProxy<TFields extends Record<string, AnyPrimitive>, TDefined extends boolean = false> = {
|
|
33
|
+
readonly [K in keyof TFields]: InferProxy<TFields[K]>;
|
|
34
|
+
} & {
|
|
35
|
+
/** Gets the entire struct value */
|
|
36
|
+
get(): MaybeUndefined<InferStructState<TFields>, TDefined>;
|
|
37
|
+
/** Sets the entire struct value */
|
|
38
|
+
set(value: InferStructState<TFields>): void;
|
|
39
|
+
/** Returns a readonly snapshot of the struct for rendering */
|
|
40
|
+
toSnapshot(): MaybeUndefined<InferStructSnapshot<TFields>, TDefined>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
interface StructPrimitiveSchema<TFields extends Record<string, AnyPrimitive>> {
|
|
44
|
+
readonly required: boolean;
|
|
45
|
+
readonly defaultValue: InferStructState<TFields> | undefined;
|
|
46
|
+
readonly fields: TFields;
|
|
47
|
+
readonly validators: readonly Validator<InferStructState<TFields>>[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefined extends boolean = false>
|
|
51
|
+
implements Primitive<InferStructState<TFields>, StructProxy<TFields, TDefined>>
|
|
52
|
+
{
|
|
53
|
+
readonly _tag = "StructPrimitive" as const;
|
|
54
|
+
readonly _State!: InferStructState<TFields>;
|
|
55
|
+
readonly _Proxy!: StructProxy<TFields, TDefined>;
|
|
56
|
+
|
|
57
|
+
private readonly _schema: StructPrimitiveSchema<TFields>;
|
|
58
|
+
|
|
59
|
+
private readonly _opDefinitions = {
|
|
60
|
+
set: OperationDefinition.make({
|
|
61
|
+
kind: "struct.set" as const,
|
|
62
|
+
payload: Schema.Unknown,
|
|
63
|
+
target: Schema.Unknown,
|
|
64
|
+
apply: (payload) => payload,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
constructor(schema: StructPrimitiveSchema<TFields>) {
|
|
69
|
+
this._schema = schema;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Mark this struct as required */
|
|
73
|
+
required(): StructPrimitive<TFields, true> {
|
|
74
|
+
return new StructPrimitive({
|
|
75
|
+
...this._schema,
|
|
76
|
+
required: true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Set a default value for this struct */
|
|
81
|
+
default(defaultValue: InferStructState<TFields>): StructPrimitive<TFields, true> {
|
|
82
|
+
return new StructPrimitive({
|
|
83
|
+
...this._schema,
|
|
84
|
+
defaultValue,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Get the fields schema */
|
|
89
|
+
get fields(): TFields {
|
|
90
|
+
return this._schema.fields;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Add a custom validation rule (useful for cross-field validation) */
|
|
94
|
+
refine(fn: (value: InferStructState<TFields>) => boolean, message: string): StructPrimitive<TFields, TDefined> {
|
|
95
|
+
return new StructPrimitive({
|
|
96
|
+
...this._schema,
|
|
97
|
+
validators: [...this._schema.validators, { validate: fn, message }],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
readonly _internal: PrimitiveInternal<InferStructState<TFields>, StructProxy<TFields, TDefined>> = {
|
|
102
|
+
createProxy: (env: ProxyEnvironment.ProxyEnvironment, operationPath: OperationPath.OperationPath): StructProxy<TFields, TDefined> => {
|
|
103
|
+
const fields = this._schema.fields;
|
|
104
|
+
const defaultValue = this._schema.defaultValue;
|
|
105
|
+
|
|
106
|
+
// Helper to build a snapshot by calling toSnapshot on each field
|
|
107
|
+
const buildSnapshot = (): InferStructSnapshot<TFields> | undefined => {
|
|
108
|
+
const state = env.getState(operationPath);
|
|
109
|
+
|
|
110
|
+
// Build snapshot from field proxies (they handle their own defaults)
|
|
111
|
+
const snapshot: Record<string, unknown> = {};
|
|
112
|
+
let hasAnyDefinedField = false;
|
|
113
|
+
|
|
114
|
+
for (const key in fields) {
|
|
115
|
+
const fieldPrimitive = fields[key]!;
|
|
116
|
+
const fieldPath = operationPath.append(key);
|
|
117
|
+
const fieldProxy = fieldPrimitive._internal.createProxy(env, fieldPath);
|
|
118
|
+
const fieldSnapshot = (fieldProxy as { toSnapshot(): unknown }).toSnapshot();
|
|
119
|
+
snapshot[key] = fieldSnapshot;
|
|
120
|
+
if (fieldSnapshot !== undefined) {
|
|
121
|
+
hasAnyDefinedField = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Return undefined only if there's no state, no struct default, and no field snapshots
|
|
126
|
+
if (state === undefined && defaultValue === undefined && !hasAnyDefinedField) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return snapshot as InferStructSnapshot<TFields>;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Create the base object with get/set/toSnapshot methods
|
|
134
|
+
const base = {
|
|
135
|
+
get: (): MaybeUndefined<InferStructState<TFields>, TDefined> => {
|
|
136
|
+
const state = env.getState(operationPath) as InferStructState<TFields> | undefined;
|
|
137
|
+
return (state ?? defaultValue) as MaybeUndefined<InferStructState<TFields>, TDefined>;
|
|
138
|
+
},
|
|
139
|
+
set: (value: InferStructState<TFields>) => {
|
|
140
|
+
env.addOperation(
|
|
141
|
+
Operation.fromDefinition(operationPath, this._opDefinitions.set, value)
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
toSnapshot: (): MaybeUndefined<InferStructSnapshot<TFields>, TDefined> => {
|
|
145
|
+
const snapshot = buildSnapshot();
|
|
146
|
+
return snapshot as MaybeUndefined<InferStructSnapshot<TFields>, TDefined>;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Use a JavaScript Proxy to intercept field access
|
|
151
|
+
return new globalThis.Proxy(base as StructProxy<TFields, TDefined>, {
|
|
152
|
+
get: (target, prop, receiver) => {
|
|
153
|
+
// Return base methods (get, set, toSnapshot)
|
|
154
|
+
if (prop === "get") {
|
|
155
|
+
return target.get;
|
|
156
|
+
}
|
|
157
|
+
if (prop === "set") {
|
|
158
|
+
return target.set;
|
|
159
|
+
}
|
|
160
|
+
if (prop === "toSnapshot") {
|
|
161
|
+
return target.toSnapshot;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Handle symbol properties (like Symbol.toStringTag)
|
|
165
|
+
if (typeof prop === "symbol") {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check if prop is a field in the schema
|
|
170
|
+
if (prop in fields) {
|
|
171
|
+
const fieldPrimitive = fields[prop as keyof TFields]!;
|
|
172
|
+
const fieldPath = operationPath.append(prop as string);
|
|
173
|
+
return fieldPrimitive._internal.createProxy(env, fieldPath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return undefined;
|
|
177
|
+
},
|
|
178
|
+
has: (target, prop) => {
|
|
179
|
+
if (prop === "get" || prop === "set" || prop === "toSnapshot") return true;
|
|
180
|
+
if (typeof prop === "string" && prop in fields) return true;
|
|
181
|
+
return false;
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
applyOperation: (
|
|
187
|
+
state: InferStructState<TFields> | undefined,
|
|
188
|
+
operation: Operation.Operation<any, any, any>
|
|
189
|
+
): InferStructState<TFields> => {
|
|
190
|
+
const path = operation.path;
|
|
191
|
+
const tokens = path.toTokens().filter((t: string) => t !== "");
|
|
192
|
+
|
|
193
|
+
let newState: InferStructState<TFields>;
|
|
194
|
+
|
|
195
|
+
// If path is empty or root, this is a struct.set operation
|
|
196
|
+
if (tokens.length === 0) {
|
|
197
|
+
if (operation.kind !== "struct.set") {
|
|
198
|
+
throw new ValidationError(`StructPrimitive root cannot apply operation of kind: ${operation.kind}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const payload = operation.payload;
|
|
202
|
+
if (typeof payload !== "object" || payload === null) {
|
|
203
|
+
throw new ValidationError(`StructPrimitive.set requires an object payload`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
newState = payload as InferStructState<TFields>;
|
|
207
|
+
} else {
|
|
208
|
+
// Otherwise, delegate to the appropriate field primitive
|
|
209
|
+
const fieldName = tokens[0] as keyof TFields;
|
|
210
|
+
if (!(fieldName in this._schema.fields)) {
|
|
211
|
+
throw new ValidationError(`Unknown field: ${globalThis.String(fieldName)}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const fieldPrimitive = this._schema.fields[fieldName]!;
|
|
215
|
+
const remainingPath = path.shift();
|
|
216
|
+
const fieldOperation = {
|
|
217
|
+
...operation,
|
|
218
|
+
path: remainingPath,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Get the current field state
|
|
222
|
+
const currentState = state ?? ({} as InferStructState<TFields>);
|
|
223
|
+
const currentFieldState = currentState[fieldName] as InferState<typeof fieldPrimitive> | undefined;
|
|
224
|
+
|
|
225
|
+
// Apply the operation to the field
|
|
226
|
+
const newFieldState = fieldPrimitive._internal.applyOperation(currentFieldState, fieldOperation);
|
|
227
|
+
|
|
228
|
+
// Build updated state
|
|
229
|
+
newState = {
|
|
230
|
+
...currentState,
|
|
231
|
+
[fieldName]: newFieldState,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Run validators on the new state
|
|
236
|
+
runValidators(newState, this._schema.validators);
|
|
237
|
+
|
|
238
|
+
return newState;
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
getInitialState: (): InferStructState<TFields> | undefined => {
|
|
242
|
+
if (this._schema.defaultValue !== undefined) {
|
|
243
|
+
return this._schema.defaultValue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Build initial state from field defaults
|
|
247
|
+
const fields = this._schema.fields;
|
|
248
|
+
const initialState: Record<string, unknown> = {};
|
|
249
|
+
let hasAnyDefault = false;
|
|
250
|
+
|
|
251
|
+
for (const key in fields) {
|
|
252
|
+
const fieldDefault = fields[key]!._internal.getInitialState();
|
|
253
|
+
if (fieldDefault !== undefined) {
|
|
254
|
+
initialState[key] = fieldDefault;
|
|
255
|
+
hasAnyDefault = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return hasAnyDefault ? (initialState as InferStructState<TFields>) : undefined;
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
transformOperation: (
|
|
263
|
+
clientOp: Operation.Operation<any, any, any>,
|
|
264
|
+
serverOp: Operation.Operation<any, any, any>
|
|
265
|
+
): Transform.TransformResult => {
|
|
266
|
+
const clientPath = clientOp.path;
|
|
267
|
+
const serverPath = serverOp.path;
|
|
268
|
+
|
|
269
|
+
// If paths don't overlap at all, no transformation needed
|
|
270
|
+
if (!OperationPath.pathsOverlap(clientPath, serverPath)) {
|
|
271
|
+
return { type: "transformed", operation: clientOp };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const clientTokens = clientPath.toTokens().filter((t: string) => t !== "");
|
|
275
|
+
const serverTokens = serverPath.toTokens().filter((t: string) => t !== "");
|
|
276
|
+
|
|
277
|
+
// If both are at root level (struct.set operations)
|
|
278
|
+
if (clientTokens.length === 0 && serverTokens.length === 0) {
|
|
279
|
+
// Client wins (last-write-wins)
|
|
280
|
+
return { type: "transformed", operation: clientOp };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// If server set entire struct and client is updating a field
|
|
284
|
+
if (serverTokens.length === 0 && serverOp.kind === "struct.set") {
|
|
285
|
+
// Client's field operation proceeds - optimistic update
|
|
286
|
+
// Server will validate/reject if needed
|
|
287
|
+
return { type: "transformed", operation: clientOp };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// If client set entire struct and server is updating a field
|
|
291
|
+
if (clientTokens.length === 0 && clientOp.kind === "struct.set") {
|
|
292
|
+
// Client's struct.set supersedes server's field update
|
|
293
|
+
return { type: "transformed", operation: clientOp };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Both operations target fields
|
|
297
|
+
if (clientTokens.length > 0 && serverTokens.length > 0) {
|
|
298
|
+
const clientField = clientTokens[0] as keyof TFields;
|
|
299
|
+
const serverField = serverTokens[0] as keyof TFields;
|
|
300
|
+
|
|
301
|
+
// Different fields - no conflict
|
|
302
|
+
if (clientField !== serverField) {
|
|
303
|
+
return { type: "transformed", operation: clientOp };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Same field - delegate to field primitive
|
|
307
|
+
const fieldPrimitive = this._schema.fields[clientField];
|
|
308
|
+
if (!fieldPrimitive) {
|
|
309
|
+
return { type: "transformed", operation: clientOp };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const clientOpForField = {
|
|
313
|
+
...clientOp,
|
|
314
|
+
path: clientOp.path.shift(),
|
|
315
|
+
};
|
|
316
|
+
const serverOpForField = {
|
|
317
|
+
...serverOp,
|
|
318
|
+
path: serverOp.path.shift(),
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const result = fieldPrimitive._internal.transformOperation(clientOpForField, serverOpForField);
|
|
322
|
+
|
|
323
|
+
if (result.type === "transformed") {
|
|
324
|
+
// Restore the original path
|
|
325
|
+
return {
|
|
326
|
+
type: "transformed",
|
|
327
|
+
operation: {
|
|
328
|
+
...result.operation,
|
|
329
|
+
path: clientOp.path,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Default: no transformation needed
|
|
338
|
+
return { type: "transformed", operation: clientOp };
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Creates a new StructPrimitive with the given fields */
|
|
344
|
+
export const Struct = <TFields extends Record<string, AnyPrimitive>>(
|
|
345
|
+
fields: TFields
|
|
346
|
+
): StructPrimitive<TFields, false> =>
|
|
347
|
+
new StructPrimitive({ required: false, defaultValue: undefined, fields, validators: [] });
|
|
348
|
+
|