@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
package/src/Operation.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
import * as OperationPath from "./OperationPath"
|
|
3
|
+
import * as OperationDefinition from "./OperationDefinition"
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export type Operation<TKind, TPayload extends Schema.Schema.Any, TDef extends OperationDefinition.OperationDefinition<TKind, TPayload, any>> = {
|
|
8
|
+
readonly kind: TKind
|
|
9
|
+
readonly path: OperationPath.OperationPath
|
|
10
|
+
readonly payload: Schema.Schema.Type<TPayload>,
|
|
11
|
+
|
|
12
|
+
} & TDef
|
|
13
|
+
|
|
14
|
+
export const fromDefinition = <TKind, TPayload extends Schema.Schema.Any, TDef extends OperationDefinition.OperationDefinition<TKind, TPayload, any>>(operationPath: OperationPath.OperationPath, definition: TDef, payload: Schema.Schema.Type<TPayload>): Operation<TKind, TPayload, TDef> => {
|
|
15
|
+
return {
|
|
16
|
+
kind: definition.kind,
|
|
17
|
+
path: operationPath,
|
|
18
|
+
payload: payload,
|
|
19
|
+
} as Operation<TKind, TPayload, TDef>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Encoded representation of an Operation for network transport.
|
|
24
|
+
*/
|
|
25
|
+
export interface EncodedOperation {
|
|
26
|
+
readonly kind: unknown
|
|
27
|
+
readonly path: OperationPath.EncodedOperationPath
|
|
28
|
+
readonly payload: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Encodes an Operation to a JSON-serializable format for network transport.
|
|
33
|
+
* @param operation - The operation to encode.
|
|
34
|
+
* @returns The encoded representation.
|
|
35
|
+
*/
|
|
36
|
+
export const encode = <TKind, TPayload extends Schema.Schema.Any, TDef extends OperationDefinition.OperationDefinition<TKind, TPayload, any>>(
|
|
37
|
+
operation: Operation<TKind, TPayload, TDef>
|
|
38
|
+
): EncodedOperation => {
|
|
39
|
+
return {
|
|
40
|
+
kind: operation.kind,
|
|
41
|
+
path: OperationPath.encode(operation.path),
|
|
42
|
+
payload: operation.payload,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Decodes an encoded operation back to an Operation.
|
|
48
|
+
* Note: This returns a partial operation without the definition methods.
|
|
49
|
+
* The caller must have the operation definitions to fully reconstruct if needed.
|
|
50
|
+
* @param encoded - The encoded representation.
|
|
51
|
+
* @returns The decoded Operation (without definition-specific methods).
|
|
52
|
+
*/
|
|
53
|
+
export const decode = (encoded: EncodedOperation): Operation<unknown, Schema.Schema.Any, any> => {
|
|
54
|
+
return {
|
|
55
|
+
kind: encoded.kind,
|
|
56
|
+
path: OperationPath.decode(encoded.path),
|
|
57
|
+
payload: encoded.payload,
|
|
58
|
+
} as Operation<unknown, Schema.Schema.Any, any>
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
type Mutable<T> = T extends ReadonlyArray<infer U> ? Array<U> : { -readonly [K in keyof T]: T[K] };
|
|
4
|
+
|
|
5
|
+
export interface OperationDefinition<TKind, TPayload extends Schema.Schema.Any, TTarget extends Schema.Schema.Any> {
|
|
6
|
+
readonly kind: TKind
|
|
7
|
+
readonly payload: TPayload
|
|
8
|
+
readonly target: TTarget
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const make = <TKind, TPayload extends Schema.Schema.Any, TTarget extends Schema.Schema.Any>(options: {
|
|
12
|
+
readonly kind: TKind
|
|
13
|
+
readonly payload: TPayload
|
|
14
|
+
readonly target: TTarget
|
|
15
|
+
readonly apply: (payload: Schema.Schema.Type<TPayload>, target: Mutable<Schema.Schema.Type<TTarget>>) => void
|
|
16
|
+
}) => {
|
|
17
|
+
return {
|
|
18
|
+
kind: options.kind,
|
|
19
|
+
payload: options.payload,
|
|
20
|
+
target: options.target,
|
|
21
|
+
apply: options.apply
|
|
22
|
+
} as const;
|
|
23
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// export type OperationPath = string
|
|
2
|
+
export type OperationPathToken = string
|
|
3
|
+
|
|
4
|
+
export interface OperationPath {
|
|
5
|
+
readonly _tag: "OperationPath"
|
|
6
|
+
readonly toTokens: () => ReadonlyArray<OperationPathToken>
|
|
7
|
+
readonly concat: (other: OperationPath) => OperationPath
|
|
8
|
+
readonly append: (token: OperationPathToken) => OperationPath
|
|
9
|
+
readonly pop: () => OperationPath
|
|
10
|
+
readonly shift: () => OperationPath
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const parseStringPath = (stringPath: string): ReadonlyArray<OperationPathToken> => {
|
|
14
|
+
return stringPath.split("/")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const makeStringPathFromTokens = (tokens: ReadonlyArray<OperationPathToken>): string => {
|
|
18
|
+
return tokens.join("/")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new operation path.
|
|
23
|
+
* @param stringPath - The string path to create the path from.
|
|
24
|
+
* @returns The new operation path.
|
|
25
|
+
*/
|
|
26
|
+
export function make(stringPath?: string): OperationPath {
|
|
27
|
+
|
|
28
|
+
const tokensInternal: ReadonlyArray<OperationPathToken> = stringPath ? parseStringPath(stringPath) : []
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the tokens of the path.
|
|
32
|
+
* @returns The tokens of the path.
|
|
33
|
+
*/
|
|
34
|
+
const toTokens = () => {
|
|
35
|
+
return tokensInternal
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Concatenates two paths.
|
|
40
|
+
* @param other - The other path to concatenate.
|
|
41
|
+
* @returns The new path.
|
|
42
|
+
*/
|
|
43
|
+
const concat = (other: OperationPath): OperationPath => {
|
|
44
|
+
return make(makeStringPathFromTokens(toTokens().concat(other.toTokens())))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Appends a token to the path.
|
|
49
|
+
* @param token - The token to append.
|
|
50
|
+
* @returns The new path.
|
|
51
|
+
*/
|
|
52
|
+
const append = (token: OperationPathToken): OperationPath => {
|
|
53
|
+
return make(makeStringPathFromTokens(toTokens().concat([token])))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Removes the last token from the path.
|
|
58
|
+
* @returns The new path.
|
|
59
|
+
*/
|
|
60
|
+
const pop = (): OperationPath => {
|
|
61
|
+
return make(makeStringPathFromTokens(toTokens().slice(0, -1)))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Removes the first token from the path.
|
|
66
|
+
* @returns The new path.
|
|
67
|
+
*/
|
|
68
|
+
const shift = (): OperationPath => {
|
|
69
|
+
return make(makeStringPathFromTokens(toTokens().slice(1)))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
_tag: "OperationPath",
|
|
74
|
+
toTokens,
|
|
75
|
+
concat,
|
|
76
|
+
append,
|
|
77
|
+
pop,
|
|
78
|
+
shift
|
|
79
|
+
} as const
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a new operation path from tokens.
|
|
84
|
+
* @param tokens - The tokens to create the path from.
|
|
85
|
+
* @returns The new operation path.
|
|
86
|
+
*/
|
|
87
|
+
export function fromTokens(tokens: ReadonlyArray<OperationPathToken>): OperationPath {
|
|
88
|
+
return make(makeStringPathFromTokens(tokens))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// =============================================================================
|
|
92
|
+
// Path Utility Functions
|
|
93
|
+
// =============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Checks if two operation paths overlap (one is prefix of the other or equal).
|
|
97
|
+
*/
|
|
98
|
+
export const pathsOverlap = (
|
|
99
|
+
pathA: OperationPath,
|
|
100
|
+
pathB: OperationPath
|
|
101
|
+
): boolean => {
|
|
102
|
+
const tokensA = pathA.toTokens().filter((t) => t !== "");
|
|
103
|
+
const tokensB = pathB.toTokens().filter((t) => t !== "");
|
|
104
|
+
|
|
105
|
+
const minLength = Math.min(tokensA.length, tokensB.length);
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < minLength; i++) {
|
|
108
|
+
if (tokensA[i] !== tokensB[i]) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return true;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks if pathA is a prefix of pathB (pathA is ancestor of pathB).
|
|
118
|
+
*/
|
|
119
|
+
export const isPrefix = (
|
|
120
|
+
pathA: OperationPath,
|
|
121
|
+
pathB: OperationPath
|
|
122
|
+
): boolean => {
|
|
123
|
+
const tokensA = pathA.toTokens().filter((t) => t !== "");
|
|
124
|
+
const tokensB = pathB.toTokens().filter((t) => t !== "");
|
|
125
|
+
|
|
126
|
+
if (tokensA.length > tokensB.length) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < tokensA.length; i++) {
|
|
131
|
+
if (tokensA[i] !== tokensB[i]) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return true;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Checks if two paths are exactly equal.
|
|
141
|
+
*/
|
|
142
|
+
export const pathsEqual = (
|
|
143
|
+
pathA: OperationPath,
|
|
144
|
+
pathB: OperationPath
|
|
145
|
+
): boolean => {
|
|
146
|
+
const tokensA = pathA.toTokens().filter((t) => t !== "");
|
|
147
|
+
const tokensB = pathB.toTokens().filter((t) => t !== "");
|
|
148
|
+
|
|
149
|
+
if (tokensA.length !== tokensB.length) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < tokensA.length; i++) {
|
|
154
|
+
if (tokensA[i] !== tokensB[i]) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return true;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Gets the relative path of pathB with respect to pathA.
|
|
164
|
+
* Assumes pathA is a prefix of pathB.
|
|
165
|
+
*/
|
|
166
|
+
export const getRelativePath = (
|
|
167
|
+
basePath: OperationPath,
|
|
168
|
+
fullPath: OperationPath
|
|
169
|
+
): string[] => {
|
|
170
|
+
const baseTokens = basePath.toTokens().filter((t) => t !== "");
|
|
171
|
+
const fullTokens = fullPath.toTokens().filter((t) => t !== "");
|
|
172
|
+
|
|
173
|
+
return fullTokens.slice(baseTokens.length);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Encoded representation of an OperationPath for network transport.
|
|
178
|
+
*/
|
|
179
|
+
export type EncodedOperationPath = string;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Encodes an OperationPath to a string for network transport.
|
|
183
|
+
* @param path - The operation path to encode.
|
|
184
|
+
* @returns The encoded string representation.
|
|
185
|
+
*/
|
|
186
|
+
export const encode = (path: OperationPath): EncodedOperationPath => {
|
|
187
|
+
return makeStringPathFromTokens(path.toTokens());
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Decodes an encoded string back to an OperationPath.
|
|
192
|
+
* @param encoded - The encoded string representation.
|
|
193
|
+
* @returns The decoded OperationPath.
|
|
194
|
+
*/
|
|
195
|
+
export const decode = (encoded: EncodedOperationPath): OperationPath => {
|
|
196
|
+
return make(encoded);
|
|
197
|
+
};
|
package/src/Presence.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 0.0.1
|
|
3
|
+
* Presence module for ephemeral per-connection state.
|
|
4
|
+
* Used by both client and server for schema validation.
|
|
5
|
+
*/
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Presence Types
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A Presence schema wrapper that holds an Effect Schema for validation.
|
|
14
|
+
* This is used by both client and server to validate presence data.
|
|
15
|
+
*/
|
|
16
|
+
export interface Presence<TData> {
|
|
17
|
+
readonly _tag: "Presence";
|
|
18
|
+
/** The Effect Schema used for validation */
|
|
19
|
+
readonly schema: Schema.Schema<TData>;
|
|
20
|
+
/** Branded type marker for inference */
|
|
21
|
+
readonly _Data: TData;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Options for creating a Presence instance.
|
|
26
|
+
*/
|
|
27
|
+
export interface PresenceOptions<TData> {
|
|
28
|
+
/** The Effect Schema defining the presence data structure */
|
|
29
|
+
readonly schema: Schema.Schema<TData>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Infer the data type from a Presence instance.
|
|
34
|
+
*/
|
|
35
|
+
export type Infer<P extends Presence<any>> = P["_Data"];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Any Presence type (for generic constraints).
|
|
39
|
+
*/
|
|
40
|
+
export type AnyPresence = Presence<any>;
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// Presence Entry (for storage/transport)
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A presence entry as stored/transmitted.
|
|
48
|
+
*/
|
|
49
|
+
export interface PresenceEntry<TData = unknown> {
|
|
50
|
+
/** The presence data */
|
|
51
|
+
readonly data: TData;
|
|
52
|
+
/** Optional user ID from authentication */
|
|
53
|
+
readonly userId?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// Factory Function
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Creates a new Presence schema wrapper.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* import { Presence } from "@voidhash/mimic";
|
|
66
|
+
* import { Schema } from "effect";
|
|
67
|
+
*
|
|
68
|
+
* const CursorPresence = Presence.make({
|
|
69
|
+
* schema: Schema.Struct({
|
|
70
|
+
* name: Schema.String,
|
|
71
|
+
* cursor: Schema.Struct({
|
|
72
|
+
* x: Schema.Number,
|
|
73
|
+
* y: Schema.Number,
|
|
74
|
+
* }),
|
|
75
|
+
* }),
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export const make = <TData,>(options: PresenceOptions<TData>): Presence<TData> => ({
|
|
80
|
+
_tag: "Presence",
|
|
81
|
+
schema: options.schema,
|
|
82
|
+
_Data: undefined as unknown as TData,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// =============================================================================
|
|
86
|
+
// Validation Functions
|
|
87
|
+
// =============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validates unknown data against a Presence schema.
|
|
91
|
+
* Throws a ParseError if validation fails.
|
|
92
|
+
*
|
|
93
|
+
* @param presence - The Presence instance with the schema
|
|
94
|
+
* @param data - Unknown data to validate
|
|
95
|
+
* @returns The validated and typed data
|
|
96
|
+
* @throws ParseError if validation fails
|
|
97
|
+
*/
|
|
98
|
+
export const validate = <TData,>(
|
|
99
|
+
presence: Presence<TData>,
|
|
100
|
+
data: unknown
|
|
101
|
+
): TData => {
|
|
102
|
+
return Schema.decodeUnknownSync(presence.schema)(data);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Safely validates unknown data against a Presence schema.
|
|
107
|
+
* Returns undefined if validation fails instead of throwing.
|
|
108
|
+
*
|
|
109
|
+
* @param presence - The Presence instance with the schema
|
|
110
|
+
* @param data - Unknown data to validate
|
|
111
|
+
* @returns The validated data or undefined if invalid
|
|
112
|
+
*/
|
|
113
|
+
export const validateSafe = <TData,>(
|
|
114
|
+
presence: Presence<TData>,
|
|
115
|
+
data: unknown
|
|
116
|
+
): TData | undefined => {
|
|
117
|
+
try {
|
|
118
|
+
return Schema.decodeUnknownSync(presence.schema)(data);
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Checks if unknown data is valid according to a Presence schema.
|
|
126
|
+
*
|
|
127
|
+
* @param presence - The Presence instance with the schema
|
|
128
|
+
* @param data - Unknown data to check
|
|
129
|
+
* @returns true if valid, false otherwise
|
|
130
|
+
*/
|
|
131
|
+
export const isValid = <TData,>(
|
|
132
|
+
presence: Presence<TData>,
|
|
133
|
+
data: unknown
|
|
134
|
+
): data is TData => {
|
|
135
|
+
try {
|
|
136
|
+
Schema.decodeUnknownSync(presence.schema)(data);
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
package/src/Primitive.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Re-export all primitives from separate files
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
export * from "./primitives/shared";
|
|
7
|
+
|
|
8
|
+
// String Primitive
|
|
9
|
+
export * from "./primitives/String";
|
|
10
|
+
// Struct Primitive
|
|
11
|
+
export * from "./primitives/Struct";
|
|
12
|
+
|
|
13
|
+
// Boolean Primitive
|
|
14
|
+
export * from "./primitives/Boolean";
|
|
15
|
+
|
|
16
|
+
// Number Primitive
|
|
17
|
+
export * from "./primitives/Number";
|
|
18
|
+
// Literal Primitive
|
|
19
|
+
export * from "./primitives/Literal";
|
|
20
|
+
|
|
21
|
+
// Array Primitive
|
|
22
|
+
export * from "./primitives/Array";
|
|
23
|
+
// Lazy Primitive
|
|
24
|
+
export * from "./primitives/Lazy";
|
|
25
|
+
|
|
26
|
+
// Union Primitive
|
|
27
|
+
export * from "./primitives/Union";
|
|
28
|
+
|
|
29
|
+
// TreeNode Primitive
|
|
30
|
+
export * from "./primitives/TreeNode";
|
|
31
|
+
// Tree Primitive
|
|
32
|
+
export * from "./primitives/Tree";
|
package/src/Proxy.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as ProxyEnvironment from "./ProxyEnvironment";
|
|
2
|
+
import * as OperationPath from "./OperationPath";
|
|
3
|
+
|
|
4
|
+
export type Proxy<T> = T
|
|
5
|
+
|
|
6
|
+
export const factory = <T,>(fn: (env: ProxyEnvironment.ProxyEnvironment, operationPath: OperationPath.OperationPath) => Proxy<T>) => {
|
|
7
|
+
return (env: ProxyEnvironment.ProxyEnvironment, operationPath: OperationPath.OperationPath) => fn(env, operationPath)
|
|
8
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as Operation from "./Operation";
|
|
2
|
+
import * as OperationPath from "./OperationPath";
|
|
3
|
+
|
|
4
|
+
export type ProxyEnvironment = {
|
|
5
|
+
/** Adds an operation to be collected/applied */
|
|
6
|
+
readonly addOperation: (operation: Operation.Operation<any, any, any>) => void;
|
|
7
|
+
/** Gets the current state at the given path */
|
|
8
|
+
readonly getState: (path: OperationPath.OperationPath) => unknown;
|
|
9
|
+
/** Generates a unique ID (UUID) for array elements */
|
|
10
|
+
readonly generateId: () => string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface ProxyEnvironmentOptions {
|
|
14
|
+
/** Callback when an operation is added */
|
|
15
|
+
readonly onOperation: (operation: Operation.Operation<any, any, any>) => void;
|
|
16
|
+
/** Function to retrieve current state at a path (defaults to returning undefined) */
|
|
17
|
+
readonly getState?: (path: OperationPath.OperationPath) => unknown;
|
|
18
|
+
/** Optional: Custom ID generator (defaults to crypto.randomUUID) */
|
|
19
|
+
readonly generateId?: () => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Default UUID generator using crypto.randomUUID */
|
|
23
|
+
const defaultGenerateId = (): string => {
|
|
24
|
+
return crypto.randomUUID();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Default state getter that always returns undefined */
|
|
28
|
+
const defaultGetState = (_path: OperationPath.OperationPath): unknown => {
|
|
29
|
+
return undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a ProxyEnvironment.
|
|
34
|
+
* @param optionsOrCallback - Either an options object or a simple callback for operations
|
|
35
|
+
*/
|
|
36
|
+
export const make = (
|
|
37
|
+
optionsOrCallback: ProxyEnvironmentOptions | ((operation: Operation.Operation<any, any, any>) => void)
|
|
38
|
+
): ProxyEnvironment => {
|
|
39
|
+
// Support both old callback style and new options object
|
|
40
|
+
const options: ProxyEnvironmentOptions =
|
|
41
|
+
typeof optionsOrCallback === "function"
|
|
42
|
+
? { onOperation: optionsOrCallback }
|
|
43
|
+
: optionsOrCallback;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
addOperation: (operation: Operation.Operation<any, any, any>) => {
|
|
47
|
+
options.onOperation(operation);
|
|
48
|
+
},
|
|
49
|
+
getState: options.getState ?? defaultGetState,
|
|
50
|
+
generateId: options.generateId ?? defaultGenerateId,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as Operation from "./Operation";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Transaction represents a group of operations that were applied atomically.
|
|
5
|
+
*/
|
|
6
|
+
export interface Transaction {
|
|
7
|
+
/** Unique identifier for this transaction */
|
|
8
|
+
readonly id: string;
|
|
9
|
+
/** Operations contained in this transaction */
|
|
10
|
+
readonly ops: ReadonlyArray<Operation.Operation<any, any, any>>;
|
|
11
|
+
/** Timestamp when the transaction was created */
|
|
12
|
+
readonly timestamp: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new Transaction with the given operations.
|
|
17
|
+
*/
|
|
18
|
+
export const make = (ops: ReadonlyArray<Operation.Operation<any, any, any>>): Transaction => ({
|
|
19
|
+
id: crypto.randomUUID(),
|
|
20
|
+
ops,
|
|
21
|
+
timestamp: Date.now(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates an empty Transaction.
|
|
26
|
+
*/
|
|
27
|
+
export const empty = (): Transaction => make([]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a transaction is empty (has no operations).
|
|
31
|
+
*/
|
|
32
|
+
export const isEmpty = (tx: Transaction): boolean => tx.ops.length === 0;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Merges multiple transactions into one.
|
|
36
|
+
*/
|
|
37
|
+
export const merge = (txs: ReadonlyArray<Transaction>): Transaction => {
|
|
38
|
+
const allOps = txs.flatMap(tx => tx.ops);
|
|
39
|
+
return make(allOps);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Encoded representation of a Transaction for network transport.
|
|
45
|
+
*/
|
|
46
|
+
export interface EncodedTransaction {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly ops: ReadonlyArray<Operation.EncodedOperation>;
|
|
49
|
+
readonly timestamp: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Encodes a Transaction to a JSON-serializable format for network transport.
|
|
54
|
+
* @param transaction - The transaction to encode.
|
|
55
|
+
* @returns The encoded representation.
|
|
56
|
+
*/
|
|
57
|
+
export const encode = (transaction: Transaction): EncodedTransaction => ({
|
|
58
|
+
id: transaction.id,
|
|
59
|
+
ops: transaction.ops.map(Operation.encode),
|
|
60
|
+
timestamp: transaction.timestamp,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Decodes an encoded transaction back to a Transaction.
|
|
65
|
+
* @param encoded - The encoded representation.
|
|
66
|
+
* @returns The decoded Transaction.
|
|
67
|
+
*/
|
|
68
|
+
export const decode = (encoded: EncodedTransaction): Transaction => ({
|
|
69
|
+
id: encoded.id,
|
|
70
|
+
ops: encoded.ops.map(Operation.decode),
|
|
71
|
+
timestamp: encoded.timestamp,
|
|
72
|
+
});
|
package/src/Transform.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type * as Operation from "./Operation";
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Transform Result Types
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Result of transforming an operation against another operation.
|
|
9
|
+
*/
|
|
10
|
+
export type TransformResult =
|
|
11
|
+
| { type: "transformed"; operation: Operation.Operation<any, any, any> }
|
|
12
|
+
| { type: "noop" } // Operation becomes a no-op (already superseded)
|
|
13
|
+
| { type: "conflict"; reason: string };
|