@voidhash/mimic 0.0.1-alpha.1 → 0.0.1-alpha.10

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 (63) hide show
  1. package/.turbo/turbo-build.log +51 -0
  2. package/LICENSE.md +663 -0
  3. package/dist/Document-ChuFrTk1.cjs +571 -0
  4. package/dist/Document-CwiAFTIq.mjs +438 -0
  5. package/dist/Document-CwiAFTIq.mjs.map +1 -0
  6. package/dist/Presence-DKKP4v5X.d.cts +91 -0
  7. package/dist/Presence-DKKP4v5X.d.cts.map +1 -0
  8. package/dist/Presence-DdMVKcOv.mjs +110 -0
  9. package/dist/Presence-DdMVKcOv.mjs.map +1 -0
  10. package/dist/Presence-N8u7Eppr.d.mts +91 -0
  11. package/dist/Presence-N8u7Eppr.d.mts.map +1 -0
  12. package/dist/Presence-gWrmGBeu.cjs +126 -0
  13. package/dist/Primitive-CvFVxR8_.d.cts +1175 -0
  14. package/dist/Primitive-CvFVxR8_.d.cts.map +1 -0
  15. package/dist/Primitive-lEhQyGVL.d.mts +1175 -0
  16. package/dist/Primitive-lEhQyGVL.d.mts.map +1 -0
  17. package/dist/chunk-CLMFDpHK.mjs +18 -0
  18. package/dist/client/index.cjs +1456 -0
  19. package/dist/client/index.d.cts +692 -0
  20. package/dist/client/index.d.cts.map +1 -0
  21. package/dist/client/index.d.mts +692 -0
  22. package/dist/client/index.d.mts.map +1 -0
  23. package/dist/client/index.mjs +1413 -0
  24. package/dist/client/index.mjs.map +1 -0
  25. package/dist/index.cjs +2577 -0
  26. package/dist/index.d.cts +143 -0
  27. package/dist/index.d.cts.map +1 -0
  28. package/dist/index.d.mts +143 -0
  29. package/dist/index.d.mts.map +1 -0
  30. package/dist/index.mjs +2526 -0
  31. package/dist/index.mjs.map +1 -0
  32. package/dist/server/index.cjs +191 -0
  33. package/dist/server/index.d.cts +148 -0
  34. package/dist/server/index.d.cts.map +1 -0
  35. package/dist/server/index.d.mts +148 -0
  36. package/dist/server/index.d.mts.map +1 -0
  37. package/dist/server/index.mjs +182 -0
  38. package/dist/server/index.mjs.map +1 -0
  39. package/package.json +25 -13
  40. package/src/EffectSchema.ts +374 -0
  41. package/src/Primitive.ts +3 -0
  42. package/src/client/ClientDocument.ts +1 -1
  43. package/src/client/errors.ts +10 -10
  44. package/src/index.ts +1 -0
  45. package/src/primitives/Array.ts +57 -22
  46. package/src/primitives/Boolean.ts +33 -19
  47. package/src/primitives/Either.ts +379 -0
  48. package/src/primitives/Lazy.ts +16 -2
  49. package/src/primitives/Literal.ts +33 -20
  50. package/src/primitives/Number.ts +39 -26
  51. package/src/primitives/String.ts +40 -25
  52. package/src/primitives/Struct.ts +126 -29
  53. package/src/primitives/Tree.ts +119 -32
  54. package/src/primitives/TreeNode.ts +77 -30
  55. package/src/primitives/Union.ts +56 -29
  56. package/src/primitives/shared.ts +111 -9
  57. package/src/server/errors.ts +6 -6
  58. package/tests/EffectSchema.test.ts +546 -0
  59. package/tests/primitives/Array.test.ts +108 -0
  60. package/tests/primitives/Either.test.ts +707 -0
  61. package/tests/primitives/Struct.test.ts +250 -0
  62. package/tests/primitives/Tree.test.ts +250 -0
  63. package/tsdown.config.ts +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["Document.make","_transactionOrder: string[]","Transaction.isEmpty"],"sources":["../../src/server/ServerDocument.ts","../../src/server/errors.ts"],"sourcesContent":["import * as Document from \"../Document\";\nimport * as Transaction from \"../Transaction\";\nimport type * as Primitive from \"../Primitive\";\n\n// =============================================================================\n// Server Message Types (matching client's Transport expectations)\n// =============================================================================\n\n/**\n * Message sent when broadcasting a committed transaction.\n */\nexport interface TransactionMessage {\n readonly type: \"transaction\";\n readonly transaction: Transaction.Transaction;\n /** Server-assigned version number for ordering */\n readonly version: number;\n}\n\n/**\n * Message sent when a transaction is rejected.\n */\nexport interface ErrorMessage {\n readonly type: \"error\";\n readonly transactionId: string;\n readonly reason: string;\n}\n\n/**\n * Message sent as a full state snapshot.\n */\nexport interface SnapshotMessage {\n readonly type: \"snapshot\";\n readonly state: unknown;\n readonly version: number;\n}\n\n/**\n * Union of all server messages that can be broadcast.\n */\nexport type ServerMessage = TransactionMessage | ErrorMessage | SnapshotMessage;\n\n// =============================================================================\n// Submit Result Types\n// =============================================================================\n\n/**\n * Result of submitting a transaction to the server.\n */\nexport type SubmitResult =\n | { readonly success: true; readonly version: number }\n | { readonly success: false; readonly reason: string };\n\n// =============================================================================\n// Server Document Types\n// =============================================================================\n\n/**\n * Options for creating a ServerDocument.\n */\nexport interface ServerDocumentOptions<TSchema extends Primitive.AnyPrimitive> {\n /** The schema defining the document structure */\n readonly schema: TSchema;\n /** Initial state (optional, will use schema defaults if not provided) */\n readonly initialState?: Primitive.InferState<TSchema>;\n /** Initial version number (optional, defaults to 0) */\n readonly initialVersion?: number;\n /** Called when a transaction is successfully applied and should be broadcast */\n readonly onBroadcast: (message: TransactionMessage) => void;\n /** Called when a transaction is rejected (optional, for logging/metrics) */\n readonly onRejection?: (transactionId: string, reason: string) => void;\n /** Maximum number of processed transaction IDs to track for deduplication */\n readonly maxTransactionHistory?: number;\n}\n\n/**\n * A ServerDocument maintains the authoritative state and processes client transactions.\n */\nexport interface ServerDocument<TSchema extends Primitive.AnyPrimitive> {\n /** The schema defining this document's structure */\n readonly schema: TSchema;\n\n /** Returns the current authoritative state */\n get(): Primitive.InferState<TSchema> | undefined;\n\n /** Returns the current version number */\n getVersion(): number;\n\n /**\n * Submits a transaction for processing.\n * Validates and applies the transaction if valid, or rejects it with a reason.\n * @param transaction - The transaction to process\n * @returns SubmitResult indicating success with version or failure with reason\n */\n submit(transaction: Transaction.Transaction): SubmitResult;\n\n /**\n * Returns a snapshot of the current state and version.\n * Used to initialize new clients or resync after drift.\n */\n getSnapshot(): SnapshotMessage;\n\n /**\n * Checks if a transaction has already been processed.\n * @param transactionId - The transaction ID to check\n */\n hasProcessed(transactionId: string): boolean;\n}\n\n// =============================================================================\n// Server Document Implementation\n// =============================================================================\n\n/**\n * Creates a new ServerDocument for the given schema.\n */\nexport const make = <TSchema extends Primitive.AnyPrimitive>(\n options: ServerDocumentOptions<TSchema>\n): ServerDocument<TSchema> => {\n const {\n schema,\n initialState,\n initialVersion = 0,\n onBroadcast,\n onRejection,\n maxTransactionHistory = 1000,\n } = options;\n\n // ==========================================================================\n // Internal State\n // ==========================================================================\n\n // The authoritative document\n let _document = Document.make(schema, { initial: initialState });\n\n // Current version number (incremented on each successful transaction)\n let _version = initialVersion;\n\n // Track processed transaction IDs for deduplication\n const _processedTransactions = new Set<string>();\n const _transactionOrder: string[] = [];\n\n // ==========================================================================\n // Helper Functions\n // ==========================================================================\n\n /**\n * Records a transaction as processed, maintaining the history limit.\n */\n const recordTransaction = (transactionId: string): void => {\n _processedTransactions.add(transactionId);\n _transactionOrder.push(transactionId);\n\n // Evict oldest transactions if over limit\n while (_transactionOrder.length > maxTransactionHistory) {\n const oldest = _transactionOrder.shift();\n if (oldest) {\n _processedTransactions.delete(oldest);\n }\n }\n };\n\n /**\n * Validates that the transaction can be applied to the current state.\n * Creates a temporary document and attempts to apply the operations.\n */\n const validateTransaction = (\n transaction: Transaction.Transaction\n ): { valid: true } | { valid: false; reason: string } => {\n // Check for empty transaction\n if (Transaction.isEmpty(transaction)) {\n return { valid: false, reason: \"Transaction is empty\" };\n }\n\n // Check for duplicate transaction\n if (_processedTransactions.has(transaction.id)) {\n return { valid: false, reason: \"Transaction has already been processed\" };\n }\n\n // Create a temporary document with current state to test the operations\n const currentState = _document.get();\n const tempDoc = Document.make(schema, { initial: currentState });\n\n try {\n // Attempt to apply all operations\n tempDoc.apply(transaction.ops);\n return { valid: true };\n } catch (error) {\n // Operations failed to apply\n const message = error instanceof Error ? error.message : String(error);\n return { valid: false, reason: message };\n }\n };\n\n // ==========================================================================\n // Public API\n // ==========================================================================\n\n const serverDocument: ServerDocument<TSchema> = {\n schema,\n\n get: (): Primitive.InferState<TSchema> | undefined => {\n return _document.get();\n },\n\n getVersion: (): number => {\n return _version;\n },\n\n submit: (transaction: Transaction.Transaction): SubmitResult => {\n // Validate the transaction\n const validation = validateTransaction(transaction);\n\n if (!validation.valid) {\n // Notify rejection callback if provided\n onRejection?.(transaction.id, validation.reason);\n\n return {\n success: false,\n reason: validation.reason,\n };\n }\n\n // Apply the transaction to the authoritative state\n try {\n _document.apply(transaction.ops);\n } catch (error) {\n // This shouldn't happen since we validated, but handle gracefully\n const reason = error instanceof Error ? error.message : String(error);\n onRejection?.(transaction.id, reason);\n return { success: false, reason };\n }\n\n // Increment version\n _version += 1;\n\n // Record as processed\n recordTransaction(transaction.id);\n\n // Broadcast the confirmed transaction\n const message: TransactionMessage = {\n type: \"transaction\",\n transaction,\n version: _version,\n };\n onBroadcast(message);\n\n return {\n success: true,\n version: _version,\n };\n },\n\n getSnapshot: (): SnapshotMessage => {\n return {\n type: \"snapshot\",\n state: _document.get(),\n version: _version,\n };\n },\n\n hasProcessed: (transactionId: string): boolean => {\n return _processedTransactions.has(transactionId);\n },\n };\n\n return serverDocument;\n};\n","import type * as Transaction from \"../Transaction\";\n\n// =============================================================================\n// Server Errors\n// =============================================================================\n\n/**\n * Base error class for all mimic-server errors.\n */\nexport class MimicServerError extends Error {\n readonly _tag: string = \"MimicServerError\";\n constructor(message: string) {\n super(message);\n this.name = \"MimicServerError\";\n }\n}\n\n/**\n * Error thrown when a transaction fails validation.\n */\nexport class ValidationError extends MimicServerError {\n override readonly _tag = \"ValidationError\";\n readonly transactionId: string;\n\n constructor(transactionId: string, message: string) {\n super(`Transaction ${transactionId} validation failed: ${message}`);\n this.name = \"ValidationError\";\n this.transactionId = transactionId;\n }\n}\n\n/**\n * Error thrown when an operation is invalid for the current schema.\n */\nexport class InvalidOperationError extends MimicServerError {\n override readonly _tag = \"InvalidOperationError\";\n readonly operationKind: string;\n readonly path: string;\n\n constructor(operationKind: string, path: string, message: string) {\n super(`Invalid operation ${operationKind} at path \"${path}\": ${message}`);\n this.name = \"InvalidOperationError\";\n this.operationKind = operationKind;\n this.path = path;\n }\n}\n\n/**\n * Error thrown when an operation cannot be applied to the current state.\n */\nexport class StateValidationError extends MimicServerError {\n override readonly _tag = \"StateValidationError\";\n readonly transactionId: string;\n override readonly cause?: Error;\n\n constructor(transactionId: string, message: string, cause?: Error) {\n super(`Transaction ${transactionId} cannot be applied to current state: ${message}`);\n this.name = \"StateValidationError\";\n this.transactionId = transactionId;\n this.cause = cause;\n }\n}\n\n/**\n * Error thrown when attempting to apply an empty transaction.\n */\nexport class EmptyTransactionError extends MimicServerError {\n override readonly _tag = \"EmptyTransactionError\";\n readonly transactionId: string;\n\n constructor(transactionId: string) {\n super(`Transaction ${transactionId} is empty and cannot be applied`);\n this.name = \"EmptyTransactionError\";\n this.transactionId = transactionId;\n }\n}\n\n/**\n * Error thrown when a duplicate transaction is submitted.\n */\nexport class DuplicateTransactionError extends MimicServerError {\n override readonly _tag = \"DuplicateTransactionError\";\n readonly transactionId: string;\n\n constructor(transactionId: string) {\n super(`Transaction ${transactionId} has already been processed`);\n this.name = \"DuplicateTransactionError\";\n this.transactionId = transactionId;\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidhash/mimic",
3
- "version": "0.0.1-alpha.1",
3
+ "version": "0.0.1-alpha.10",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,25 +9,37 @@
9
9
  },
10
10
  "main": "./src/index.ts",
11
11
  "exports": {
12
- ".": "./src/index.ts",
13
- "./server": "./src/server/index.ts",
14
- "./client": "./src/client/index.ts"
15
- },
16
- "scripts": {
17
- "build": "tsdown",
18
- "lint": "biome check .",
19
- "typecheck": "tsc --noEmit",
20
- "test": "vitest run -c vitest.mts"
12
+ ".": {
13
+ "import": "./dist/index.mjs",
14
+ "types": "./dist/index.d.mts",
15
+ "require": "./dist/index.cjs"
16
+ },
17
+ "./server": {
18
+ "import": "./dist/server/index.mjs",
19
+ "types": "./dist/server/index.d.mts",
20
+ "require": "./dist/server/index.cjs"
21
+ },
22
+ "./client": {
23
+ "import": "./dist/client/index.mjs",
24
+ "types": "./dist/client/index.d.mts",
25
+ "require": "./dist/client/index.cjs"
26
+ }
21
27
  },
22
28
  "devDependencies": {
23
29
  "@effect/vitest": "^0.27.0",
24
- "@voidhash/tsconfig": "workspace:*",
25
30
  "tsdown": "^0.18.2",
26
31
  "typescript": "5.8.3",
27
32
  "vite-tsconfig-paths": "^5.1.4",
28
- "vitest": "^3.2.4"
33
+ "vitest": "^3.2.4",
34
+ "@voidhash/tsconfig": "0.0.1-alpha.10"
29
35
  },
30
36
  "peerDependencies": {
31
- "effect": "catalog:"
37
+ "effect": "^3.19.12"
38
+ },
39
+ "scripts": {
40
+ "build": "tsdown",
41
+ "lint": "biome check .",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "vitest run -c vitest.mts"
32
44
  }
33
45
  }
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Effect.Schema utilities for converting Mimic primitives to Effect.Schema schemas.
3
+ *
4
+ * @since 0.0.1
5
+ */
6
+ import { Schema } from "effect";
7
+ import type { AnyPrimitive, InferSetInput, InferUpdateInput } from "./primitives/shared";
8
+ import type { LiteralPrimitive, LiteralValue } from "./primitives/Literal";
9
+ import type { StructPrimitive } from "./primitives/Struct";
10
+ import type { ArrayPrimitive } from "./primitives/Array";
11
+ import type { UnionPrimitive, UnionVariants } from "./primitives/Union";
12
+ import type { EitherPrimitive, ScalarPrimitive } from "./primitives/Either";
13
+ import type { LazyPrimitive } from "./primitives/Lazy";
14
+ import type { TreeNodePrimitive, AnyTreeNodePrimitive } from "./primitives/TreeNode";
15
+
16
+ // =============================================================================
17
+ // Type-level Schema Inference
18
+ // =============================================================================
19
+
20
+ /**
21
+ * Infer the Effect.Schema type for a primitive's set input.
22
+ */
23
+ export type ToSetSchema<T extends AnyPrimitive> = Schema.Schema<InferSetInput<T>>;
24
+
25
+ /**
26
+ * Infer the Effect.Schema type for a primitive's update input.
27
+ */
28
+ export type ToUpdateSchema<T extends AnyPrimitive> = Schema.Schema<InferUpdateInput<T>>;
29
+
30
+ /**
31
+ * Type for TreeNode set schema - uses the node's data set input type
32
+ */
33
+ export type ToTreeNodeSetSchema<T extends AnyTreeNodePrimitive> = Schema.Schema<InferSetInput<T["data"]>>;
34
+
35
+ /**
36
+ * Type for TreeNode update schema - uses the node's data update input type
37
+ */
38
+ export type ToTreeNodeUpdateSchema<T extends AnyTreeNodePrimitive> = Schema.Schema<InferUpdateInput<T["data"]>>;
39
+
40
+ // =============================================================================
41
+ // Schema for TreeNodeState
42
+ // =============================================================================
43
+
44
+ /**
45
+ * Schema for a tree node state (flat storage format).
46
+ */
47
+ export const TreeNodeStateSchema = Schema.Struct({
48
+ id: Schema.String,
49
+ type: Schema.String,
50
+ parentId: Schema.NullOr(Schema.String),
51
+ pos: Schema.String,
52
+ data: Schema.Unknown,
53
+ });
54
+
55
+ // =============================================================================
56
+ // Internal type for primitives (including those that don't implement full Primitive interface)
57
+ // =============================================================================
58
+
59
+ /**
60
+ * Internal type for anything that can be converted to a schema.
61
+ * This includes both AnyPrimitive and AnyTreeNodePrimitive.
62
+ */
63
+ type ConvertiblePrimitive = { _tag: string };
64
+
65
+ // =============================================================================
66
+ // Runtime Conversion Functions
67
+ // =============================================================================
68
+
69
+ /**
70
+ * Check if a field is required for set operations.
71
+ * A field is required if: TRequired is true AND THasDefault is false.
72
+ *
73
+ * We determine this by checking the primitive's schema properties.
74
+ */
75
+ function isRequiredForSet(primitive: ConvertiblePrimitive): boolean {
76
+ // Access the private schema to check required and default status
77
+ const schema = (primitive as any)._schema;
78
+ if (!schema) return false;
79
+
80
+ return schema.required === true && schema.defaultValue === undefined;
81
+ }
82
+
83
+ /**
84
+ * Get the base Effect.Schema for a primitive type (without optional wrapper).
85
+ */
86
+ function getBaseSchema(primitive: ConvertiblePrimitive): Schema.Schema<any> {
87
+ switch (primitive._tag) {
88
+ case "StringPrimitive":
89
+ return Schema.String;
90
+
91
+ case "NumberPrimitive":
92
+ return Schema.Number;
93
+
94
+ case "BooleanPrimitive":
95
+ return Schema.Boolean;
96
+
97
+ case "LiteralPrimitive": {
98
+ const literalPrimitive = primitive as unknown as LiteralPrimitive<LiteralValue, any, any>;
99
+ const literalValue = (literalPrimitive as any)._schema?.literal ?? (literalPrimitive as any).literal;
100
+ return Schema.Literal(literalValue);
101
+ }
102
+
103
+ case "StructPrimitive": {
104
+ const structPrimitive = primitive as unknown as StructPrimitive<Record<string, AnyPrimitive>, any, any>;
105
+ return buildStructSetSchema(structPrimitive);
106
+ }
107
+
108
+ case "ArrayPrimitive": {
109
+ const arrayPrimitive = primitive as unknown as ArrayPrimitive<AnyPrimitive, any, any>;
110
+ const elementSchema = buildElementSetSchema(arrayPrimitive.element);
111
+ return Schema.Array(elementSchema);
112
+ }
113
+
114
+ case "UnionPrimitive": {
115
+ const unionPrimitive = primitive as unknown as UnionPrimitive<UnionVariants, any, any, any>;
116
+ return buildUnionSetSchema(unionPrimitive);
117
+ }
118
+
119
+ case "EitherPrimitive": {
120
+ const eitherPrimitive = primitive as unknown as EitherPrimitive<readonly ScalarPrimitive[], any, any>;
121
+ return buildEitherSchema(eitherPrimitive);
122
+ }
123
+
124
+ case "LazyPrimitive": {
125
+ const lazyPrimitive = primitive as unknown as LazyPrimitive<() => AnyPrimitive>;
126
+ // Resolve the lazy primitive and get its schema
127
+ const resolved = (lazyPrimitive as any)._resolve?.() ?? (lazyPrimitive as any)._thunk();
128
+ return getBaseSchema(resolved);
129
+ }
130
+
131
+ case "TreeNodePrimitive": {
132
+ const treeNodePrimitive = primitive as unknown as TreeNodePrimitive<string, StructPrimitive<any>, any>;
133
+ // TreeNode delegates to its data struct
134
+ return buildStructSetSchema(treeNodePrimitive.data);
135
+ }
136
+
137
+ case "TreePrimitive": {
138
+ // Tree returns an array of TreeNodeState
139
+ return Schema.Array(TreeNodeStateSchema);
140
+ }
141
+
142
+ default:
143
+ return Schema.Unknown;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Build the set schema for a struct primitive.
149
+ * Required fields (required=true, no default) are non-optional.
150
+ * Other fields are wrapped with Schema.optional.
151
+ */
152
+ function buildStructSetSchema(structPrimitive: StructPrimitive<Record<string, AnyPrimitive>, any, any>): Schema.Schema<any> {
153
+ const fields = structPrimitive.fields;
154
+ // Use any to avoid complex Schema type constraints
155
+ const schemaFields: Record<string, any> = {};
156
+
157
+ for (const key in fields) {
158
+ const fieldPrimitive = fields[key]!;
159
+ const baseSchema = getBaseSchema(fieldPrimitive);
160
+
161
+ if (isRequiredForSet(fieldPrimitive)) {
162
+ // Required field - use base schema directly
163
+ schemaFields[key] = baseSchema;
164
+ } else {
165
+ // Optional field - wrap with Schema.optional
166
+ schemaFields[key] = Schema.optional(baseSchema);
167
+ }
168
+ }
169
+
170
+ return Schema.Struct(schemaFields) as any;
171
+ }
172
+
173
+ /**
174
+ * Build the update schema for a struct primitive.
175
+ * All fields are optional for partial updates.
176
+ */
177
+ function buildStructUpdateSchema(structPrimitive: StructPrimitive<Record<string, AnyPrimitive>, any, any>): Schema.Schema<any> {
178
+ const fields = structPrimitive.fields;
179
+ // Use any to avoid complex Schema type constraints
180
+ const schemaFields: Record<string, any> = {};
181
+
182
+ for (const key in fields) {
183
+ const fieldPrimitive = fields[key]!;
184
+ // For update, use the update schema for nested structs, otherwise base schema
185
+ let fieldSchema: Schema.Schema<any>;
186
+
187
+ if (fieldPrimitive._tag === "StructPrimitive") {
188
+ fieldSchema = buildStructUpdateSchema(fieldPrimitive as StructPrimitive<Record<string, AnyPrimitive>, any, any>);
189
+ } else {
190
+ fieldSchema = getBaseSchema(fieldPrimitive);
191
+ }
192
+
193
+ // All fields are optional in update
194
+ schemaFields[key] = Schema.optional(fieldSchema);
195
+ }
196
+
197
+ return Schema.Struct(schemaFields) as any;
198
+ }
199
+
200
+ /**
201
+ * Build the set schema for an array element.
202
+ * For struct elements, uses the struct's set input schema.
203
+ */
204
+ function buildElementSetSchema(elementPrimitive: AnyPrimitive): Schema.Schema<any> {
205
+ if (elementPrimitive._tag === "StructPrimitive") {
206
+ return buildStructSetSchema(elementPrimitive as StructPrimitive<Record<string, AnyPrimitive>, any, any>);
207
+ }
208
+ return getBaseSchema(elementPrimitive);
209
+ }
210
+
211
+ /**
212
+ * Build the set schema for a union primitive.
213
+ * Creates a Schema.Union of all variant schemas.
214
+ */
215
+ function buildUnionSetSchema(unionPrimitive: UnionPrimitive<UnionVariants, any, any, any>): Schema.Schema<any> {
216
+ const variants = unionPrimitive.variants;
217
+ const variantSchemas: Schema.Schema<any>[] = [];
218
+
219
+ for (const key in variants) {
220
+ const variantPrimitive = variants[key]!;
221
+ variantSchemas.push(buildStructSetSchema(variantPrimitive));
222
+ }
223
+
224
+ if (variantSchemas.length === 0) {
225
+ return Schema.Unknown;
226
+ }
227
+
228
+ if (variantSchemas.length === 1) {
229
+ return variantSchemas[0]!;
230
+ }
231
+
232
+ return Schema.Union(...variantSchemas as [Schema.Schema<any>, Schema.Schema<any>, ...Schema.Schema<any>[]]);
233
+ }
234
+
235
+ /**
236
+ * Build the schema for an either primitive.
237
+ * Creates a Schema.Union of all scalar variant types.
238
+ */
239
+ function buildEitherSchema(eitherPrimitive: EitherPrimitive<readonly ScalarPrimitive[], any, any>): Schema.Schema<any> {
240
+ const variants = eitherPrimitive.variants;
241
+ const variantSchemas: Schema.Schema<any>[] = [];
242
+
243
+ for (const variant of variants) {
244
+ variantSchemas.push(getBaseSchema(variant as unknown as ConvertiblePrimitive));
245
+ }
246
+
247
+ if (variantSchemas.length === 0) {
248
+ return Schema.Unknown;
249
+ }
250
+
251
+ if (variantSchemas.length === 1) {
252
+ return variantSchemas[0]!;
253
+ }
254
+
255
+ return Schema.Union(...variantSchemas as [Schema.Schema<any>, Schema.Schema<any>, ...Schema.Schema<any>[]]);
256
+ }
257
+
258
+ /**
259
+ * Build the update schema for a union primitive.
260
+ * Creates a Schema.Union of all variant update schemas.
261
+ */
262
+ function buildUnionUpdateSchema(unionPrimitive: UnionPrimitive<UnionVariants, any, any, any>): Schema.Schema<any> {
263
+ const variants = unionPrimitive.variants;
264
+ const variantSchemas: Schema.Schema<any>[] = [];
265
+
266
+ for (const key in variants) {
267
+ const variantPrimitive = variants[key]!;
268
+ variantSchemas.push(buildStructUpdateSchema(variantPrimitive));
269
+ }
270
+
271
+ if (variantSchemas.length === 0) {
272
+ return Schema.Unknown;
273
+ }
274
+
275
+ if (variantSchemas.length === 1) {
276
+ return variantSchemas[0]!;
277
+ }
278
+
279
+ return Schema.Union(...variantSchemas as [Schema.Schema<any>, Schema.Schema<any>, ...Schema.Schema<any>[]]);
280
+ }
281
+
282
+ /**
283
+ * Get the update schema for a primitive.
284
+ * For structs, all fields are optional (partial updates).
285
+ * For simple primitives, same as set schema.
286
+ */
287
+ function getUpdateSchema(primitive: ConvertiblePrimitive): Schema.Schema<any> {
288
+ switch (primitive._tag) {
289
+ case "StructPrimitive": {
290
+ const structPrimitive = primitive as unknown as StructPrimitive<Record<string, AnyPrimitive>, any, any>;
291
+ return buildStructUpdateSchema(structPrimitive);
292
+ }
293
+
294
+ case "UnionPrimitive": {
295
+ const unionPrimitive = primitive as unknown as UnionPrimitive<UnionVariants, any, any, any>;
296
+ return buildUnionUpdateSchema(unionPrimitive);
297
+ }
298
+
299
+ case "TreeNodePrimitive": {
300
+ const treeNodePrimitive = primitive as unknown as TreeNodePrimitive<string, StructPrimitive<any>, any>;
301
+ // TreeNode update delegates to data struct's update schema (all fields optional)
302
+ return buildStructUpdateSchema(treeNodePrimitive.data);
303
+ }
304
+
305
+ case "LazyPrimitive": {
306
+ const lazyPrimitive = primitive as unknown as LazyPrimitive<() => AnyPrimitive>;
307
+ const resolved = (lazyPrimitive as any)._resolve?.() ?? (lazyPrimitive as any)._thunk();
308
+ return getUpdateSchema(resolved);
309
+ }
310
+
311
+ default:
312
+ // For simple primitives, update schema is same as set schema
313
+ return getBaseSchema(primitive);
314
+ }
315
+ }
316
+
317
+ // =============================================================================
318
+ // Public API
319
+ // =============================================================================
320
+
321
+ /**
322
+ * Convert a Mimic primitive to an Effect.Schema for set operations.
323
+ *
324
+ * The resulting schema:
325
+ * - For structs: required fields (required=true, no default) are non-optional, others are optional
326
+ * - For arrays: uses the element's set schema
327
+ * - For unions: creates a Schema.Union of variant schemas
328
+ * - For TreeNode: delegates to the node's data struct schema
329
+ * - For Tree: returns Schema.Array of TreeNodeState
330
+ *
331
+ * @example
332
+ * ```typescript
333
+ * const UserSchema = Primitive.Struct({
334
+ * name: Primitive.String().required(),
335
+ * age: Primitive.Number().default(0),
336
+ * email: Primitive.String(),
337
+ * });
338
+ *
339
+ * const SetSchema = toSetSchema(UserSchema);
340
+ * // { name: string, age?: number, email?: string }
341
+ * ```
342
+ */
343
+ export function toSetSchema<T extends AnyPrimitive>(primitive: T): ToSetSchema<T>;
344
+ export function toSetSchema<T extends AnyTreeNodePrimitive>(primitive: T): ToTreeNodeSetSchema<T>;
345
+ export function toSetSchema(primitive: ConvertiblePrimitive): Schema.Schema<any> {
346
+ return getBaseSchema(primitive);
347
+ }
348
+
349
+ /**
350
+ * Convert a Mimic primitive to an Effect.Schema for update operations.
351
+ *
352
+ * The resulting schema:
353
+ * - For structs: all fields are optional (partial updates)
354
+ * - For unions: all variant fields are optional
355
+ * - For TreeNode: delegates to the node's data struct update schema
356
+ * - For simple primitives: same as set schema
357
+ *
358
+ * @example
359
+ * ```typescript
360
+ * const UserSchema = Primitive.Struct({
361
+ * name: Primitive.String().required(),
362
+ * age: Primitive.Number().default(0),
363
+ * email: Primitive.String(),
364
+ * });
365
+ *
366
+ * const UpdateSchema = toUpdateSchema(UserSchema);
367
+ * // { name?: string, age?: string, email?: string }
368
+ * ```
369
+ */
370
+ export function toUpdateSchema<T extends AnyPrimitive>(primitive: T): ToUpdateSchema<T>;
371
+ export function toUpdateSchema<T extends AnyTreeNodePrimitive>(primitive: T): ToTreeNodeUpdateSchema<T>;
372
+ export function toUpdateSchema(primitive: ConvertiblePrimitive): Schema.Schema<any> {
373
+ return getUpdateSchema(primitive);
374
+ }
package/src/Primitive.ts CHANGED
@@ -26,6 +26,9 @@ export * from "./primitives/Lazy";
26
26
  // Union Primitive
27
27
  export * from "./primitives/Union";
28
28
 
29
+ // Either Primitive
30
+ export * from "./primitives/Either";
31
+
29
32
  // TreeNode Primitive
30
33
  export * from "./primitives/TreeNode";
31
34
  // Tree Primitive
@@ -43,7 +43,7 @@ type InitState =
43
43
  /**
44
44
  * Listener for presence changes.
45
45
  */
46
- export interface PresenceListener<TData> {
46
+ export interface PresenceListener<_TData> {
47
47
  /** Called when any presence changes (self or others) */
48
48
  readonly onPresenceChange?: () => void;
49
49
  }
@@ -19,7 +19,7 @@ export class MimicClientError extends Error {
19
19
  * Error thrown when a transaction is rejected by the server.
20
20
  */
21
21
  export class TransactionRejectedError extends MimicClientError {
22
- readonly _tag = "TransactionRejectedError";
22
+ override readonly _tag = "TransactionRejectedError";
23
23
  readonly transaction: Transaction.Transaction;
24
24
  readonly reason: string;
25
25
 
@@ -35,7 +35,7 @@ export class TransactionRejectedError extends MimicClientError {
35
35
  * Error thrown when the transport is not connected.
36
36
  */
37
37
  export class NotConnectedError extends MimicClientError {
38
- readonly _tag = "NotConnectedError";
38
+ override readonly _tag = "NotConnectedError";
39
39
  constructor() {
40
40
  super("Transport is not connected");
41
41
  this.name = "NotConnectedError";
@@ -46,8 +46,8 @@ export class NotConnectedError extends MimicClientError {
46
46
  * Error thrown when connection to the server fails.
47
47
  */
48
48
  export class ConnectionError extends MimicClientError {
49
- readonly _tag = "ConnectionError";
50
- readonly cause?: Error;
49
+ override readonly _tag = "ConnectionError";
50
+ override readonly cause?: Error;
51
51
 
52
52
  constructor(message: string, cause?: Error) {
53
53
  super(message);
@@ -60,7 +60,7 @@ export class ConnectionError extends MimicClientError {
60
60
  * Error thrown when state drift is detected and cannot be recovered.
61
61
  */
62
62
  export class StateDriftError extends MimicClientError {
63
- readonly _tag = "StateDriftError";
63
+ override readonly _tag = "StateDriftError";
64
64
  readonly expectedVersion: number;
65
65
  readonly receivedVersion: number;
66
66
 
@@ -78,7 +78,7 @@ export class StateDriftError extends MimicClientError {
78
78
  * Error thrown when a pending transaction times out waiting for confirmation.
79
79
  */
80
80
  export class TransactionTimeoutError extends MimicClientError {
81
- readonly _tag = "TransactionTimeoutError";
81
+ override readonly _tag = "TransactionTimeoutError";
82
82
  readonly transaction: Transaction.Transaction;
83
83
  readonly timeoutMs: number;
84
84
 
@@ -96,7 +96,7 @@ export class TransactionTimeoutError extends MimicClientError {
96
96
  * Error thrown when rebasing operations fails.
97
97
  */
98
98
  export class RebaseError extends MimicClientError {
99
- readonly _tag = "RebaseError";
99
+ override readonly _tag = "RebaseError";
100
100
  readonly transactionId: string;
101
101
 
102
102
  constructor(transactionId: string, message: string) {
@@ -110,7 +110,7 @@ export class RebaseError extends MimicClientError {
110
110
  * Error thrown when the client document is in an invalid state.
111
111
  */
112
112
  export class InvalidStateError extends MimicClientError {
113
- readonly _tag = "InvalidStateError";
113
+ override readonly _tag = "InvalidStateError";
114
114
  constructor(message: string) {
115
115
  super(message);
116
116
  this.name = "InvalidStateError";
@@ -121,7 +121,7 @@ export class InvalidStateError extends MimicClientError {
121
121
  * Error thrown when WebSocket connection or communication fails.
122
122
  */
123
123
  export class WebSocketError extends MimicClientError {
124
- readonly _tag = "WebSocketError";
124
+ override readonly _tag = "WebSocketError";
125
125
  readonly code?: number;
126
126
  readonly reason?: string;
127
127
 
@@ -137,7 +137,7 @@ export class WebSocketError extends MimicClientError {
137
137
  * Error thrown when authentication fails.
138
138
  */
139
139
  export class AuthenticationError extends MimicClientError {
140
- readonly _tag = "AuthenticationError";
140
+ override readonly _tag = "AuthenticationError";
141
141
  constructor(message: string) {
142
142
  super(message);
143
143
  this.name = "AuthenticationError";
package/src/index.ts CHANGED
@@ -10,3 +10,4 @@ export * as OperationPath from "./OperationPath.js";
10
10
  export * as ProxyEnvironment from "./ProxyEnvironment.js";
11
11
  export * as Transform from "./Transform.js";
12
12
  export * as Presence from "./Presence.js";
13
+ export * as EffectSchema from "./EffectSchema.js";