@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.
Files changed (57) hide show
  1. package/README.md +17 -0
  2. package/package.json +33 -0
  3. package/src/Document.ts +256 -0
  4. package/src/FractionalIndex.ts +1249 -0
  5. package/src/Operation.ts +59 -0
  6. package/src/OperationDefinition.ts +23 -0
  7. package/src/OperationPath.ts +197 -0
  8. package/src/Presence.ts +142 -0
  9. package/src/Primitive.ts +32 -0
  10. package/src/Proxy.ts +8 -0
  11. package/src/ProxyEnvironment.ts +52 -0
  12. package/src/Transaction.ts +72 -0
  13. package/src/Transform.ts +13 -0
  14. package/src/client/ClientDocument.ts +1163 -0
  15. package/src/client/Rebase.ts +309 -0
  16. package/src/client/StateMonitor.ts +307 -0
  17. package/src/client/Transport.ts +318 -0
  18. package/src/client/WebSocketTransport.ts +572 -0
  19. package/src/client/errors.ts +145 -0
  20. package/src/client/index.ts +61 -0
  21. package/src/index.ts +12 -0
  22. package/src/primitives/Array.ts +457 -0
  23. package/src/primitives/Boolean.ts +128 -0
  24. package/src/primitives/Lazy.ts +89 -0
  25. package/src/primitives/Literal.ts +128 -0
  26. package/src/primitives/Number.ts +169 -0
  27. package/src/primitives/String.ts +189 -0
  28. package/src/primitives/Struct.ts +348 -0
  29. package/src/primitives/Tree.ts +1120 -0
  30. package/src/primitives/TreeNode.ts +113 -0
  31. package/src/primitives/Union.ts +329 -0
  32. package/src/primitives/shared.ts +122 -0
  33. package/src/server/ServerDocument.ts +267 -0
  34. package/src/server/errors.ts +90 -0
  35. package/src/server/index.ts +40 -0
  36. package/tests/Document.test.ts +556 -0
  37. package/tests/FractionalIndex.test.ts +377 -0
  38. package/tests/OperationPath.test.ts +151 -0
  39. package/tests/Presence.test.ts +321 -0
  40. package/tests/Primitive.test.ts +381 -0
  41. package/tests/client/ClientDocument.test.ts +1398 -0
  42. package/tests/client/WebSocketTransport.test.ts +992 -0
  43. package/tests/primitives/Array.test.ts +418 -0
  44. package/tests/primitives/Boolean.test.ts +126 -0
  45. package/tests/primitives/Lazy.test.ts +143 -0
  46. package/tests/primitives/Literal.test.ts +122 -0
  47. package/tests/primitives/Number.test.ts +133 -0
  48. package/tests/primitives/String.test.ts +128 -0
  49. package/tests/primitives/Struct.test.ts +311 -0
  50. package/tests/primitives/Tree.test.ts +467 -0
  51. package/tests/primitives/TreeNode.test.ts +50 -0
  52. package/tests/primitives/Union.test.ts +210 -0
  53. package/tests/server/ServerDocument.test.ts +528 -0
  54. package/tsconfig.build.json +24 -0
  55. package/tsconfig.json +8 -0
  56. package/tsdown.config.ts +18 -0
  57. package/vitest.mts +11 -0
@@ -0,0 +1,267 @@
1
+ import * as Document from "../Document";
2
+ import * as Transaction from "../Transaction";
3
+ import type * as Primitive from "../Primitive";
4
+
5
+ // =============================================================================
6
+ // Server Message Types (matching client's Transport expectations)
7
+ // =============================================================================
8
+
9
+ /**
10
+ * Message sent when broadcasting a committed transaction.
11
+ */
12
+ export interface TransactionMessage {
13
+ readonly type: "transaction";
14
+ readonly transaction: Transaction.Transaction;
15
+ /** Server-assigned version number for ordering */
16
+ readonly version: number;
17
+ }
18
+
19
+ /**
20
+ * Message sent when a transaction is rejected.
21
+ */
22
+ export interface ErrorMessage {
23
+ readonly type: "error";
24
+ readonly transactionId: string;
25
+ readonly reason: string;
26
+ }
27
+
28
+ /**
29
+ * Message sent as a full state snapshot.
30
+ */
31
+ export interface SnapshotMessage {
32
+ readonly type: "snapshot";
33
+ readonly state: unknown;
34
+ readonly version: number;
35
+ }
36
+
37
+ /**
38
+ * Union of all server messages that can be broadcast.
39
+ */
40
+ export type ServerMessage = TransactionMessage | ErrorMessage | SnapshotMessage;
41
+
42
+ // =============================================================================
43
+ // Submit Result Types
44
+ // =============================================================================
45
+
46
+ /**
47
+ * Result of submitting a transaction to the server.
48
+ */
49
+ export type SubmitResult =
50
+ | { readonly success: true; readonly version: number }
51
+ | { readonly success: false; readonly reason: string };
52
+
53
+ // =============================================================================
54
+ // Server Document Types
55
+ // =============================================================================
56
+
57
+ /**
58
+ * Options for creating a ServerDocument.
59
+ */
60
+ export interface ServerDocumentOptions<TSchema extends Primitive.AnyPrimitive> {
61
+ /** The schema defining the document structure */
62
+ readonly schema: TSchema;
63
+ /** Initial state (optional, will use schema defaults if not provided) */
64
+ readonly initialState?: Primitive.InferState<TSchema>;
65
+ /** Initial version number (optional, defaults to 0) */
66
+ readonly initialVersion?: number;
67
+ /** Called when a transaction is successfully applied and should be broadcast */
68
+ readonly onBroadcast: (message: TransactionMessage) => void;
69
+ /** Called when a transaction is rejected (optional, for logging/metrics) */
70
+ readonly onRejection?: (transactionId: string, reason: string) => void;
71
+ /** Maximum number of processed transaction IDs to track for deduplication */
72
+ readonly maxTransactionHistory?: number;
73
+ }
74
+
75
+ /**
76
+ * A ServerDocument maintains the authoritative state and processes client transactions.
77
+ */
78
+ export interface ServerDocument<TSchema extends Primitive.AnyPrimitive> {
79
+ /** The schema defining this document's structure */
80
+ readonly schema: TSchema;
81
+
82
+ /** Returns the current authoritative state */
83
+ get(): Primitive.InferState<TSchema> | undefined;
84
+
85
+ /** Returns the current version number */
86
+ getVersion(): number;
87
+
88
+ /**
89
+ * Submits a transaction for processing.
90
+ * Validates and applies the transaction if valid, or rejects it with a reason.
91
+ * @param transaction - The transaction to process
92
+ * @returns SubmitResult indicating success with version or failure with reason
93
+ */
94
+ submit(transaction: Transaction.Transaction): SubmitResult;
95
+
96
+ /**
97
+ * Returns a snapshot of the current state and version.
98
+ * Used to initialize new clients or resync after drift.
99
+ */
100
+ getSnapshot(): SnapshotMessage;
101
+
102
+ /**
103
+ * Checks if a transaction has already been processed.
104
+ * @param transactionId - The transaction ID to check
105
+ */
106
+ hasProcessed(transactionId: string): boolean;
107
+ }
108
+
109
+ // =============================================================================
110
+ // Server Document Implementation
111
+ // =============================================================================
112
+
113
+ /**
114
+ * Creates a new ServerDocument for the given schema.
115
+ */
116
+ export const make = <TSchema extends Primitive.AnyPrimitive>(
117
+ options: ServerDocumentOptions<TSchema>
118
+ ): ServerDocument<TSchema> => {
119
+ const {
120
+ schema,
121
+ initialState,
122
+ initialVersion = 0,
123
+ onBroadcast,
124
+ onRejection,
125
+ maxTransactionHistory = 1000,
126
+ } = options;
127
+
128
+ // ==========================================================================
129
+ // Internal State
130
+ // ==========================================================================
131
+
132
+ // The authoritative document
133
+ let _document = Document.make(schema, { initial: initialState });
134
+
135
+ // Current version number (incremented on each successful transaction)
136
+ let _version = initialVersion;
137
+
138
+ // Track processed transaction IDs for deduplication
139
+ const _processedTransactions = new Set<string>();
140
+ const _transactionOrder: string[] = [];
141
+
142
+ // ==========================================================================
143
+ // Helper Functions
144
+ // ==========================================================================
145
+
146
+ /**
147
+ * Records a transaction as processed, maintaining the history limit.
148
+ */
149
+ const recordTransaction = (transactionId: string): void => {
150
+ _processedTransactions.add(transactionId);
151
+ _transactionOrder.push(transactionId);
152
+
153
+ // Evict oldest transactions if over limit
154
+ while (_transactionOrder.length > maxTransactionHistory) {
155
+ const oldest = _transactionOrder.shift();
156
+ if (oldest) {
157
+ _processedTransactions.delete(oldest);
158
+ }
159
+ }
160
+ };
161
+
162
+ /**
163
+ * Validates that the transaction can be applied to the current state.
164
+ * Creates a temporary document and attempts to apply the operations.
165
+ */
166
+ const validateTransaction = (
167
+ transaction: Transaction.Transaction
168
+ ): { valid: true } | { valid: false; reason: string } => {
169
+ // Check for empty transaction
170
+ if (Transaction.isEmpty(transaction)) {
171
+ return { valid: false, reason: "Transaction is empty" };
172
+ }
173
+
174
+ // Check for duplicate transaction
175
+ if (_processedTransactions.has(transaction.id)) {
176
+ return { valid: false, reason: "Transaction has already been processed" };
177
+ }
178
+
179
+ // Create a temporary document with current state to test the operations
180
+ const currentState = _document.get();
181
+ const tempDoc = Document.make(schema, { initial: currentState });
182
+
183
+ try {
184
+ // Attempt to apply all operations
185
+ tempDoc.apply(transaction.ops);
186
+ return { valid: true };
187
+ } catch (error) {
188
+ // Operations failed to apply
189
+ const message = error instanceof Error ? error.message : String(error);
190
+ return { valid: false, reason: message };
191
+ }
192
+ };
193
+
194
+ // ==========================================================================
195
+ // Public API
196
+ // ==========================================================================
197
+
198
+ const serverDocument: ServerDocument<TSchema> = {
199
+ schema,
200
+
201
+ get: (): Primitive.InferState<TSchema> | undefined => {
202
+ return _document.get();
203
+ },
204
+
205
+ getVersion: (): number => {
206
+ return _version;
207
+ },
208
+
209
+ submit: (transaction: Transaction.Transaction): SubmitResult => {
210
+ // Validate the transaction
211
+ const validation = validateTransaction(transaction);
212
+
213
+ if (!validation.valid) {
214
+ // Notify rejection callback if provided
215
+ onRejection?.(transaction.id, validation.reason);
216
+
217
+ return {
218
+ success: false,
219
+ reason: validation.reason,
220
+ };
221
+ }
222
+
223
+ // Apply the transaction to the authoritative state
224
+ try {
225
+ _document.apply(transaction.ops);
226
+ } catch (error) {
227
+ // This shouldn't happen since we validated, but handle gracefully
228
+ const reason = error instanceof Error ? error.message : String(error);
229
+ onRejection?.(transaction.id, reason);
230
+ return { success: false, reason };
231
+ }
232
+
233
+ // Increment version
234
+ _version += 1;
235
+
236
+ // Record as processed
237
+ recordTransaction(transaction.id);
238
+
239
+ // Broadcast the confirmed transaction
240
+ const message: TransactionMessage = {
241
+ type: "transaction",
242
+ transaction,
243
+ version: _version,
244
+ };
245
+ onBroadcast(message);
246
+
247
+ return {
248
+ success: true,
249
+ version: _version,
250
+ };
251
+ },
252
+
253
+ getSnapshot: (): SnapshotMessage => {
254
+ return {
255
+ type: "snapshot",
256
+ state: _document.get(),
257
+ version: _version,
258
+ };
259
+ },
260
+
261
+ hasProcessed: (transactionId: string): boolean => {
262
+ return _processedTransactions.has(transactionId);
263
+ },
264
+ };
265
+
266
+ return serverDocument;
267
+ };
@@ -0,0 +1,90 @@
1
+ import type * as Transaction from "../Transaction";
2
+
3
+ // =============================================================================
4
+ // Server Errors
5
+ // =============================================================================
6
+
7
+ /**
8
+ * Base error class for all mimic-server errors.
9
+ */
10
+ export class MimicServerError extends Error {
11
+ readonly _tag: string = "MimicServerError";
12
+ constructor(message: string) {
13
+ super(message);
14
+ this.name = "MimicServerError";
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Error thrown when a transaction fails validation.
20
+ */
21
+ export class ValidationError extends MimicServerError {
22
+ readonly _tag = "ValidationError";
23
+ readonly transactionId: string;
24
+
25
+ constructor(transactionId: string, message: string) {
26
+ super(`Transaction ${transactionId} validation failed: ${message}`);
27
+ this.name = "ValidationError";
28
+ this.transactionId = transactionId;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Error thrown when an operation is invalid for the current schema.
34
+ */
35
+ export class InvalidOperationError extends MimicServerError {
36
+ readonly _tag = "InvalidOperationError";
37
+ readonly operationKind: string;
38
+ readonly path: string;
39
+
40
+ constructor(operationKind: string, path: string, message: string) {
41
+ super(`Invalid operation ${operationKind} at path "${path}": ${message}`);
42
+ this.name = "InvalidOperationError";
43
+ this.operationKind = operationKind;
44
+ this.path = path;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Error thrown when an operation cannot be applied to the current state.
50
+ */
51
+ export class StateValidationError extends MimicServerError {
52
+ readonly _tag = "StateValidationError";
53
+ readonly transactionId: string;
54
+ readonly cause?: Error;
55
+
56
+ constructor(transactionId: string, message: string, cause?: Error) {
57
+ super(`Transaction ${transactionId} cannot be applied to current state: ${message}`);
58
+ this.name = "StateValidationError";
59
+ this.transactionId = transactionId;
60
+ this.cause = cause;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Error thrown when attempting to apply an empty transaction.
66
+ */
67
+ export class EmptyTransactionError extends MimicServerError {
68
+ readonly _tag = "EmptyTransactionError";
69
+ readonly transactionId: string;
70
+
71
+ constructor(transactionId: string) {
72
+ super(`Transaction ${transactionId} is empty and cannot be applied`);
73
+ this.name = "EmptyTransactionError";
74
+ this.transactionId = transactionId;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Error thrown when a duplicate transaction is submitted.
80
+ */
81
+ export class DuplicateTransactionError extends MimicServerError {
82
+ readonly _tag = "DuplicateTransactionError";
83
+ readonly transactionId: string;
84
+
85
+ constructor(transactionId: string) {
86
+ super(`Transaction ${transactionId} has already been processed`);
87
+ this.name = "DuplicateTransactionError";
88
+ this.transactionId = transactionId;
89
+ }
90
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @voidhash/mimic-server
3
+ *
4
+ * Server-side document management for the Mimic sync engine.
5
+ * Provides authoritative state management with transaction validation.
6
+ *
7
+ * @since 0.0.1
8
+ */
9
+
10
+ // =============================================================================
11
+ // Server Document
12
+ // =============================================================================
13
+
14
+ export * as ServerDocument from "./ServerDocument.js";
15
+
16
+ // =============================================================================
17
+ // Errors
18
+ // =============================================================================
19
+
20
+ export {
21
+ MimicServerError,
22
+ ValidationError,
23
+ InvalidOperationError,
24
+ StateValidationError,
25
+ EmptyTransactionError,
26
+ DuplicateTransactionError,
27
+ } from "./errors.js";
28
+
29
+ // =============================================================================
30
+ // Re-export Types (for convenience)
31
+ // =============================================================================
32
+
33
+ export type {
34
+ ServerMessage,
35
+ TransactionMessage,
36
+ ErrorMessage,
37
+ SnapshotMessage,
38
+ SubmitResult,
39
+ ServerDocumentOptions,
40
+ } from "./ServerDocument.js";