@evolu/common 6.0.1-preview.20 → 6.0.1-preview.21

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.
@@ -1,4 +1,3 @@
1
- /* eslint-disable jsdoc/no-undefined-types */
2
1
  /**
3
2
  * Evolu Protocol
4
3
  *
@@ -21,7 +20,7 @@
21
20
  * transfer, ownership, real-time broadcasting, request-response semantics, and
22
21
  * error handling.
23
22
  *
24
- * ### Message Structure
23
+ * ### Message structure
25
24
  *
26
25
  * | Field | Notes |
27
26
  * | :----------------------------- | :------------------------ |
@@ -44,7 +43,7 @@
44
43
  * | - {@link NonNegativeInt} | Number of ranges. |
45
44
  * | - {@link Range} | |
46
45
  *
47
- * ### WriteKey Validation
46
+ * ### WriteKey validation
48
47
  *
49
48
  * The initiator sends a hasWriteKey flag and optionally a WriteKey. The
50
49
  * WriteKey is required when sending messages as a secure token proving the
@@ -80,33 +79,33 @@
80
79
  * initiator. In relay-to-relay or P2P sync, both sides may require the
81
80
  * {@link OwnerWriteKey} depending on who is the initiator.
82
81
  *
83
- * ### Protocol Errors
82
+ * ### Protocol errors
84
83
  *
85
84
  * The protocol uses error codes in the header to signal issues:
86
85
  *
87
86
  * - {@link ProtocolWriteKeyError}: The provided WriteKey is invalid or missing.
88
- * - {@link ProtocolWriteError}: A write operation failed (e.g., due to storage
89
- * limits or billing).
90
- * - {@link ProtocolSyncError}: A generic or unexpected synchronization failure
87
+ * - {@link ProtocolWriteError}: A serious relay-side write failure occurred.
88
+ * - {@link ProtocolSyncError}: A serious relay-side synchronization failure
91
89
  * occurred.
90
+ * - {@link ProtocolQuotaExceededError}: Storage or billing quota exceeded.
92
91
  * - {@link ProtocolUnsupportedVersionError}: Protocol version mismatch.
93
92
  * - {@link ProtocolInvalidDataError}: The message is malformed or corrupted.
94
93
  *
95
94
  * All protocol errors except `ProtocolInvalidDataError` include the `OwnerId`
96
95
  * to allow clients to associate errors with the correct owner.
97
96
  *
98
- * ### Message Size Limit
97
+ * ### Message size limit
99
98
  *
100
99
  * The protocol enforces a strict maximum size for all messages, defined by
101
- * {@link maxProtocolMessageSize}. This ensures every {@link ProtocolMessage} is
100
+ * {@link ProtocolMessageMaxSize}. This ensures every {@link ProtocolMessage} is
102
101
  * less than or equal to this limit, enabling stateless transports, simplified
103
102
  * relay implementation, and predictable memory usage. When all messages don't
104
103
  * fit within the limit, the protocol automatically continues synchronization in
105
104
  * subsequent rounds using range-based reconciliation.
106
105
  *
107
- * Individual database mutations are limited to `maxMutationSize` (640KB), which
108
- * is smaller than the protocol message limit to ensure efficient sync with
109
- * {@link maxProtocolMessageRangesSize}.
106
+ * Database mutations are limited to 640KB, which is smaller than the protocol
107
+ * message limit to ensure efficient sync with
108
+ * {@link defaultProtocolMessageRangesMaxSize}.
110
109
  *
111
110
  * ### Why Binary?
112
111
  *
@@ -149,6 +148,15 @@
149
148
  * Version negotiation is per-owner, allowing Evolu Protocol to evolve safely
150
149
  * over time and provide clear feedback about version mismatches.
151
150
  *
151
+ * ### Credible exit
152
+ *
153
+ * The protocol specification is intentionally non-configurable to ensure
154
+ * universal compatibility. This design allows applications (users) to switch
155
+ * between any compliant relay without negotiation or compatibility checks
156
+ * beyond version matching. Relays are generic infrastructure that any
157
+ * application can use interchangeably making exit from any single provider
158
+ * technically feasible and economically viable.
159
+ *
152
160
  * @module
153
161
  */
154
162
  import { Packr } from "msgpackr";
@@ -160,21 +168,62 @@ import { eqArrayNumber } from "../Eq.js";
160
168
  import { computeBalancedBuckets } from "../Number.js";
161
169
  import { objectToEntries } from "../Object.js";
162
170
  import { err, ok } from "../Result.js";
163
- import { Base64Url, base64UrlToUint8Array, DateIso, Id, idBytesToId, idBytesTypeValueLength, idToIdBytes, Json, jsonToJsonValue, NonNegativeInt, Number, uint8ArrayToBase64Url, } from "../Type.js";
171
+ import { Base64Url, base64UrlToUint8Array, between, DateIso, Id, idBytesToId, idBytesTypeValueLength, idToIdBytes, Int, Json, jsonToJsonValue, NonNegativeInt, Number, uint8ArrayToBase64Url, } from "../Type.js";
164
172
  import { ownerIdToOwnerIdBytes, ownerWriteKeyLength, } from "./Owner.js";
165
173
  import { fingerprintSize, InfiniteUpperBound, RangeType, } from "./Storage.js";
166
174
  import { Counter, eqTimestamp, Millis, timestampBytesLength, timestampBytesToTimestamp, timestampToTimestampBytes, } from "./Timestamp.js";
167
175
  /**
168
- * MessagePack serializer for standard compatibility and compact encoding.
176
+ * Evolu uses MessagePack for numbers and JSONs.
169
177
  *
170
178
  * - `variableMapSize: true` - More compact maps, ~5-10% slower encoding
171
179
  * - `useRecords: false` - Standard MessagePack without extensions
172
180
  */
173
181
  const packr = new Packr({ variableMapSize: true, useRecords: false });
174
- /** Maximum size of the entire protocol message in bytes. */
175
- export const maxProtocolMessageSize = 1_000_000;
176
- /** Maximum size of the ranges in bytes. */
177
- export const maxProtocolMessageRangesSize = 30_000;
182
+ const minProtocolMessageMaxSize = 1_000_000;
183
+ const maxProtocolMessageMaxSize = 100_000_000;
184
+ /**
185
+ * Protocol message maximum size.
186
+ *
187
+ * Defines the upper limit for how large a single protocol message can be.
188
+ * Implementations must enforce a maximum size between 1MB and 100MB to ensure
189
+ * compatibility across all Evolu implementations (the maximum size of mutation
190
+ * change is hardcoded and enforced hence the maximum size can't be smaller).
191
+ *
192
+ * Larger maximum sizes can be configured by relays to reduce roundtrips. For
193
+ * example, a dedicated relay with ample resources could configure a 100MB
194
+ * maximum to minimize roundtrips for large syncs.
195
+ *
196
+ * Only relays can safely configure larger sizes, as clients will handle them.
197
+ * Increasing this value on the client side would break compatibility with
198
+ * relays that enforce smaller limits.
199
+ */
200
+ export const ProtocolMessageMaxSize = between(minProtocolMessageMaxSize, maxProtocolMessageMaxSize)(Int);
201
+ /**
202
+ * Default {@link ProtocolMessageMaxSize} (1MB).
203
+ *
204
+ * The standard size used across Evolu implementations. Relays with more
205
+ * resources can configure larger sizes to reduce roundtrips.
206
+ */
207
+ export const defaultProtocolMessageMaxSize = minProtocolMessageMaxSize;
208
+ /**
209
+ * Protocol message ranges maximum size.
210
+ *
211
+ * Defines the upper limit for how large the ranges section of a protocol
212
+ * message can be. Implementations must enforce a maximum size between 3KB and
213
+ * 100KB to ensure compatibility.
214
+ *
215
+ * The upper bound is set to ensure ranges fit within the default 1MB
216
+ * {@link defaultProtocolMessageMaxSize}, maintaining compatibility between all
217
+ * clients and relays.
218
+ */
219
+ export const ProtocolMessageRangesMaxSize = between(3_000, 100_000)(Int);
220
+ /**
221
+ * Default {@link ProtocolMessageRangesMaxSize} (30KB).
222
+ *
223
+ * The standard size used across Evolu implementations. Relays with more
224
+ * resources can configure larger sizes to reduce roundtrips.
225
+ */
226
+ export const defaultProtocolMessageRangesMaxSize = 30_000;
178
227
  /** Evolu Protocol version. */
179
228
  export const protocolVersion = 0;
180
229
  export const MessageType = {
@@ -201,18 +250,20 @@ export const ProtocolErrorCode = {
201
250
  WriteError: 2,
202
251
  /** A code for {@link ProtocolSyncError}. */
203
252
  SyncError: 3,
253
+ /** A code for {@link ProtocolQuotaExceededError}. */
254
+ QuotaExceededError: 4,
204
255
  };
205
256
  /**
206
257
  * Creates a {@link ProtocolMessage} from CRDT messages.
207
258
  *
208
- * If the message size would exceed {@link maxProtocolMessageSize}, the protocol
209
- * ensures all messages will be sent in the next round(s) even over
259
+ * If the message size would exceed {@link defaultProtocolMessageMaxSize}, the
260
+ * protocol ensures all messages will be sent in the next round(s) even over
210
261
  * unidirectional and stateless transports.
211
262
  */
212
263
  export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages, maxSize) => {
213
264
  const buffer = createProtocolMessageBuffer(owner.id, {
214
265
  messageType: MessageType.Request,
215
- totalMaxSize: maxSize ?? maxProtocolMessageSize,
266
+ totalMaxSize: maxSize ?? defaultProtocolMessageMaxSize,
216
267
  writeKey: owner.writeKey,
217
268
  });
218
269
  let notAllMessagesSent = false;
@@ -236,8 +287,8 @@ export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages,
236
287
  *
237
288
  * The ideal approach would be to send three ranges (skip, fingerprint,
238
289
  * skip) where the fingerprint of unsent messages would act as narrow sync
239
- * probe. I think we can send {@link zeroFingerprint} which can be
240
- * interpreted as an indication that the other side should reply with
290
+ * probe. I think we can send `zeroFingerprint` which can be interpreted
291
+ * as an indication that the other side should reply with
241
292
  * {@link TimestampsRange}, so no need to restart syncing.
242
293
  *
243
294
  * For now, using a random fingerprint avoids extra complexity and is good
@@ -272,7 +323,7 @@ export const createProtocolMessageForUnsubscribe = (ownerId) => createProtocolMe
272
323
  subscriptionFlag: SubscriptionFlags.Unsubscribe,
273
324
  }).unwrap();
274
325
  export const createProtocolMessageBuffer = (ownerId, options) => {
275
- const { totalMaxSize = maxProtocolMessageSize, rangesMaxSize = maxProtocolMessageRangesSize, version = protocolVersion, } = options;
326
+ const { totalMaxSize = defaultProtocolMessageMaxSize, rangesMaxSize = defaultProtocolMessageRangesMaxSize, version = protocolVersion, } = options;
276
327
  const buffers = {
277
328
  header: createBuffer(),
278
329
  messages: {
@@ -507,6 +558,11 @@ export const applyProtocolMessageAsClient = (deps) => async (inputMessage, optio
507
558
  type: "ProtocolSyncError",
508
559
  ownerId,
509
560
  });
561
+ case ProtocolErrorCode.QuotaExceededError:
562
+ return err({
563
+ type: "ProtocolQuotaExceededError",
564
+ ownerId,
565
+ });
510
566
  default:
511
567
  throw new ProtocolDecodeError(`Invalid ProtocolErrorCode: ${errorCode}`);
512
568
  }
@@ -537,7 +593,6 @@ export const applyProtocolMessageAsClient = (deps) => async (inputMessage, optio
537
593
  const output = createProtocolMessageBuffer(ownerId, {
538
594
  messageType: MessageType.Request,
539
595
  writeKey,
540
- totalMaxSize: options.totalMaxSize,
541
596
  rangesMaxSize: options.rangesMaxSize,
542
597
  });
543
598
  const syncResult = sync(deps)(ranges, output, ownerIdBytes);
@@ -1,14 +1,81 @@
1
- import { ConsoleConfig } from "../Console.js";
1
+ import { ConsoleConfig, ConsoleDep } from "../Console.js";
2
2
  import { TimingSafeEqualDep } from "../Crypto.js";
3
+ import { LazyValue } from "../Function.js";
3
4
  import { Result } from "../Result.js";
4
5
  import { SqliteError } from "../Sqlite.js";
5
6
  import { SimpleName } from "../Type.js";
7
+ import { OwnerId } from "./Owner.js";
8
+ import { ProtocolInvalidDataError } from "./Protocol.js";
6
9
  import { CreateSqliteStorageBaseOptions, SqliteStorageDeps, Storage } from "./Storage.js";
7
10
  export interface Relay extends Disposable {
8
11
  }
9
12
  export interface RelayConfig extends ConsoleConfig {
13
+ /**
14
+ * The relay name.
15
+ *
16
+ * Implementations can use this for identification purposes (e.g., database
17
+ * file name, logging).
18
+ */
10
19
  readonly name?: SimpleName;
20
+ /**
21
+ * Optional callback to authenticate an {@link OwnerId} with the relay.
22
+ *
23
+ * If this callback is not provided, all owners are allowed.
24
+ *
25
+ * If provided, the callback receives the OwnerId and should return a promise
26
+ * that resolves to `true` to allow access, or `false` to deny.
27
+ *
28
+ * The callback returns a boolean rather than an error type because error
29
+ * handling and logging are the responsibility of the callback implementation,
30
+ * not the relay. This prevents leaking authentication implementation details
31
+ * into the generic relay interface.
32
+ *
33
+ * OwnerId is used for authentication rather than short-lived tokens because
34
+ * this only controls relay access, not write permissions. Since all data is
35
+ * encrypted on the relay, OwnerId exposure is safe.
36
+ *
37
+ * Owners specify which relays to connect to via {@link TransportConfig}. In
38
+ * WebSocket-based implementations, this check occurs before accepting the
39
+ * connection, with the OwnerId typically extracted from the URL path (e.g.,
40
+ * `ws://localhost:4000/<ownerId>`).
41
+ *
42
+ * ### Example
43
+ *
44
+ * ```ts
45
+ * const relay = await createNodeJsRelay(deps)({
46
+ * authenticateOwner: async (ownerId) => {
47
+ * const isRegistered = await db.checkOwner(ownerId);
48
+ * if (!isRegistered) {
49
+ * logger.warn("Unauthorized access attempt", { ownerId });
50
+ * }
51
+ * return isRegistered;
52
+ * },
53
+ * });
54
+ * ```
55
+ */
56
+ readonly authenticateOwner?: (ownerId: OwnerId) => Promise<boolean>;
11
57
  }
12
- export type RelaySqliteStorageDeps = SqliteStorageDeps & TimingSafeEqualDep;
13
- export declare const createRelayStorage: (deps: RelaySqliteStorageDeps) => (options: CreateSqliteStorageBaseOptions) => Result<Storage, SqliteError>;
58
+ export declare const createRelaySqliteStorage: (deps: SqliteStorageDeps & TimingSafeEqualDep) => (options: CreateSqliteStorageBaseOptions) => Result<Storage, SqliteError>;
59
+ export interface RelayLogger {
60
+ readonly started: (enableLogging: boolean, port: number) => void;
61
+ readonly storageError: (error: unknown) => void;
62
+ readonly upgradeSocketError: (error: Error) => void;
63
+ readonly invalidOrMissingOwnerIdInUrl: (url: string | undefined) => void;
64
+ readonly unauthorizedOwner: (ownerId: OwnerId) => void;
65
+ readonly authenticateOwnerError: (error: unknown) => void;
66
+ readonly connectionEstablished: (totalConnectionCount: number) => void;
67
+ readonly connectionWebSocketError: (error: Error) => void;
68
+ readonly relayOptionSubscribe: (ownerId: OwnerId, getSubscriberCount: LazyValue<number>) => void;
69
+ readonly relayOptionUnsubscribe: (ownerId: OwnerId, getSubscriberCount: LazyValue<number>) => void;
70
+ readonly relayOptionBroadcast: (ownerId: OwnerId, broadcastCount: number, subscriberCount: number) => void;
71
+ readonly messageLength: (messageLength: number) => void;
72
+ readonly applyProtocolMessageAsRelayError: (error: ProtocolInvalidDataError) => void;
73
+ readonly responseLength: (responseLength: number) => void;
74
+ readonly applyProtocolMessageAsRelayUnknownError: (error: unknown) => void;
75
+ readonly connectionClosed: (totalConnectionCount: number) => void;
76
+ readonly shuttingDown: () => void;
77
+ readonly webSocketServerDisposed: () => void;
78
+ readonly httpServerDisposed: () => void;
79
+ }
80
+ export declare const createRelayLogger: (deps: ConsoleDep) => RelayLogger;
14
81
  //# sourceMappingURL=Relay.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Relay.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAW,MAAM,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAO,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAEL,8BAA8B,EAE9B,iBAAiB,EACjB,OAAO,EACR,MAAM,cAAc,CAAC;AAGtB,MAAM,WAAW,KAAM,SAAQ,UAAU;CAAG;AAE5C,MAAM,WAAW,WAAY,SAAQ,aAAa;IAChD,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;AAE5E,eAAO,MAAM,kBAAkB,GAC5B,MAAM,sBAAsB,MAC5B,SAAS,8BAA8B,KAAG,MAAM,CAAC,OAAO,EAAE,WAAW,CAiKrE,CAAC"}
1
+ {"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Relay.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAW,MAAM,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAO,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,OAAO,EAAkC,MAAM,YAAY,CAAC;AACrE,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAEL,8BAA8B,EAE9B,iBAAiB,EACjB,OAAO,EACR,MAAM,cAAc,CAAC;AAGtB,MAAM,WAAW,KAAM,SAAQ,UAAU;CAAG;AAE5C,MAAM,WAAW,WAAY,SAAQ,aAAa;IAChD;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACrE;AAED,eAAO,MAAM,wBAAwB,GAClC,MAAM,iBAAiB,GAAG,kBAAkB,MAC5C,SAAS,8BAA8B,KAAG,MAAM,CAAC,OAAO,EAAE,WAAW,CAiKrE,CAAC;AAEJ,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,CAAC,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,QAAQ,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAChD,QAAQ,CAAC,kBAAkB,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACpD,QAAQ,CAAC,4BAA4B,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IACzE,QAAQ,CAAC,iBAAiB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACvD,QAAQ,CAAC,sBAAsB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1D,QAAQ,CAAC,qBAAqB,EAAE,CAAC,oBAAoB,EAAE,MAAM,KAAK,IAAI,CAAC;IACvE,QAAQ,CAAC,wBAAwB,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC1D,QAAQ,CAAC,oBAAoB,EAAE,CAC7B,OAAO,EAAE,OAAO,EAChB,kBAAkB,EAAE,SAAS,CAAC,MAAM,CAAC,KAClC,IAAI,CAAC;IACV,QAAQ,CAAC,sBAAsB,EAAE,CAC/B,OAAO,EAAE,OAAO,EAChB,kBAAkB,EAAE,SAAS,CAAC,MAAM,CAAC,KAClC,IAAI,CAAC;IACV,QAAQ,CAAC,oBAAoB,EAAE,CAC7B,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,KACpB,IAAI,CAAC;IACV,QAAQ,CAAC,aAAa,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD,QAAQ,CAAC,gCAAgC,EAAE,CACzC,KAAK,EAAE,wBAAwB,KAC5B,IAAI,CAAC;IACV,QAAQ,CAAC,cAAc,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,QAAQ,CAAC,uCAAuC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3E,QAAQ,CAAC,gBAAgB,EAAE,CAAC,oBAAoB,EAAE,MAAM,KAAK,IAAI,CAAC;IAClE,QAAQ,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC;IAClC,QAAQ,CAAC,uBAAuB,EAAE,MAAM,IAAI,CAAC;IAC7C,QAAQ,CAAC,kBAAkB,EAAE,MAAM,IAAI,CAAC;CACzC;AAED,eAAO,MAAM,iBAAiB,GAAI,MAAM,UAAU,KAAG,WA8FnD,CAAC"}
@@ -3,7 +3,7 @@ import { err, ok } from "../Result.js";
3
3
  import { sql } from "../Sqlite.js";
4
4
  import { createSqliteStorageBase, } from "./Storage.js";
5
5
  import { timestampToTimestampBytes } from "./Timestamp.js";
6
- export const createRelayStorage = (deps) => (options) => {
6
+ export const createRelaySqliteStorage = (deps) => (options) => {
7
7
  const sqliteStorageBase = createSqliteStorageBase(deps)(options);
8
8
  if (!sqliteStorageBase.ok)
9
9
  return sqliteStorageBase;
@@ -145,3 +145,76 @@ export const createRelayStorage = (deps) => (options) => {
145
145
  },
146
146
  });
147
147
  };
148
+ export const createRelayLogger = (deps) => ({
149
+ started: (enableLogging, port) => {
150
+ deps.console.enabled = true;
151
+ deps.console.log(`Evolu Relay started on port ${port}`);
152
+ deps.console.enabled = enableLogging;
153
+ },
154
+ storageError: (error) => {
155
+ deps.console.error("[relay]", "storage", error);
156
+ },
157
+ upgradeSocketError: (error) => {
158
+ deps.console.warn("[relay]", "socket error", { error });
159
+ },
160
+ invalidOrMissingOwnerIdInUrl: (url) => {
161
+ deps.console.warn("[relay]", "invalid or missing ownerId in URL", { url });
162
+ },
163
+ unauthorizedOwner: (ownerId) => {
164
+ deps.console.warn("[relay]", "unauthorized owner", { ownerId });
165
+ },
166
+ authenticateOwnerError: (error) => {
167
+ deps.console.error("[relay]", "authenticateOwner error", error);
168
+ },
169
+ connectionEstablished: (totalConnectionCount) => {
170
+ deps.console.log("[relay]", "connection", { totalConnectionCount });
171
+ },
172
+ connectionWebSocketError: (error) => {
173
+ deps.console.error("[relay]", "error", { error });
174
+ },
175
+ relayOptionSubscribe: (ownerId, getSubscriberCount) => {
176
+ if (deps.console.enabled)
177
+ deps.console.log("[relay]", "subscribe", {
178
+ ownerId,
179
+ subscriberCount: getSubscriberCount(),
180
+ });
181
+ },
182
+ relayOptionUnsubscribe: (ownerId, getSubscriberCount) => {
183
+ if (deps.console.enabled)
184
+ deps.console.log("[relay]", "unsubscribe", {
185
+ ownerId,
186
+ subscriberCount: getSubscriberCount(),
187
+ });
188
+ },
189
+ relayOptionBroadcast: (ownerId, broadcastCount, totalSubscribers) => {
190
+ deps.console.log("[relay]", "broadcast", {
191
+ ownerId,
192
+ broadcastCount,
193
+ totalSubscribers,
194
+ });
195
+ },
196
+ messageLength: (messageLength) => {
197
+ deps.console.log("[relay]", "on message", { messageLength });
198
+ },
199
+ applyProtocolMessageAsRelayError: (error) => {
200
+ deps.console.error("[relay]", "applyProtocolMessageAsRelay", error);
201
+ },
202
+ responseLength: (responseLength) => {
203
+ deps.console.log("[relay]", "responseLength", { responseLength });
204
+ },
205
+ applyProtocolMessageAsRelayUnknownError: (error) => {
206
+ deps.console.error("[relay]", "applyProtocolMessageAsRelayUnknownError", error);
207
+ },
208
+ connectionClosed: (totalConnectionCount) => {
209
+ deps.console.log("[relay]", "close", { totalConnectionCount });
210
+ },
211
+ shuttingDown: () => {
212
+ deps.console.log("Shutting down Evolu Relay...");
213
+ },
214
+ webSocketServerDisposed: () => {
215
+ deps.console.log("Evolu Relay WebSocketServer disposed");
216
+ },
217
+ httpServerDisposed: () => {
218
+ deps.console.log("Evolu Relay HTTP server disposed");
219
+ },
220
+ });
@@ -12,7 +12,7 @@ import { AppOwner, OwnerEncryptionKey, OwnerId, OwnerIdBytes, OwnerWriteKey, Tra
12
12
  import { ProtocolError, ProtocolInvalidDataError, ProtocolTimestampMismatchError } from "./Protocol.js";
13
13
  import { MutationChange } from "./Schema.js";
14
14
  import { CrdtMessage, SqliteStorageBase, Storage } from "./Storage.js";
15
- import { Millis, Timestamp, TimestampBytes, TimestampConfigDep, TimestampCounterOverflowError, TimestampDriftError, TimestampTimeOutOfRangeError } from "./Timestamp.js";
15
+ import { Millis, Timestamp, TimestampConfigDep, TimestampCounterOverflowError, TimestampDriftError, TimestampTimeOutOfRangeError } from "./Timestamp.js";
16
16
  export interface Sync extends Disposable {
17
17
  /**
18
18
  * Assigns or removes an owner to/from transports with reference counting.
@@ -68,6 +68,7 @@ export interface ClientStorageDep {
68
68
  readonly storage: ClientStorage;
69
69
  }
70
70
  export declare const applyLocalOnlyChange: (deps: SqliteDep & TimeDep) => (change: MutationChange) => Result<void, SqliteError>;
71
+ export declare const applyMessages: (deps: ClientStorageDep & ClockDep & RandomDep & SqliteDep) => (ownerId: OwnerId, messages: ReadonlyArray<CrdtMessage>) => Result<void, SqliteError>;
71
72
  export declare const applyMessageToTimestampAndHistoryTables: (deps: ClientStorageDep & SqliteDep) => (ownerId: OwnerIdBytes, message: CrdtMessage) => Result<void, SqliteError>;
72
73
  /**
73
74
  * TODO: Rework for the new owners API.
@@ -110,14 +111,4 @@ export interface PaymentRequiredError {
110
111
  readonly type: "PaymentRequiredError";
111
112
  }
112
113
  export declare const initialSyncState: SyncStateInitial;
113
- /**
114
- * Efficiently checks which binary timestamps already exist in the database
115
- * using a single CTE query instead of N individual queries. Crucial for WASM
116
- * SQLite performance where JS↔WASM boundary crossings are expensive.
117
- *
118
- * Used for fast idempotency detection in writeMessages before onMessage
119
- * validation. While applyMessages ensures internal idempotency, this pre-check
120
- * is faster and required for main thread message validation.
121
- */
122
- export declare const getExistingTimestamps: (deps: SqliteDep) => (ownerIdBytes: OwnerIdBytes, timestampsBytes: NonEmptyReadonlyArray<TimestampBytes>) => Result<ReadonlyArray<TimestampBytes>, SqliteError>;
123
114
  //# sourceMappingURL=Sync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Sync.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Sync.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Sync.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAGnE,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EACL,cAAc,EACd,2BAA2B,EAC3B,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAA2B,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGzE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAAM,MAAM,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAO,SAAS,EAAE,WAAW,EAAe,MAAM,cAAc,CAAC;AAExE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC,OAAO,EAAE,kBAAkB,EAAa,MAAM,iBAAiB,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EACL,QAAQ,EACR,kBAAkB,EAClB,OAAO,EACP,YAAY,EAGZ,aAAa,EAIb,eAAe,EAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAOL,aAAa,EACb,wBAAwB,EACxB,8BAA8B,EAE/B,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACL,WAAW,EAGX,iBAAiB,EACjB,OAAO,EACR,MAAM,cAAc,CAAC;AACtB,OAAO,EAEL,MAAM,EAGN,SAAS,EAET,kBAAkB,EAClB,6BAA6B,EAC7B,mBAAmB,EACnB,4BAA4B,EAG7B,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,IAAK,SAAQ,UAAU;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IAE5D,QAAQ,CAAC,YAAY,EAAE,CACrB,OAAO,EAAE,qBAAqB,CAAC,cAAc,CAAC,KAC3C,MAAM,CACT,IAAI,EACF,WAAW,GACX,6BAA6B,GAC7B,mBAAmB,GACnB,4BAA4B,CAC/B,CAAC;CACH;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,aAAa,EAAE,kBAAkB,CAAC;IAC3C,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAE5B,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAEpD;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAElC,QAAQ,CAAC,OAAO,EAAE,CAChB,KAAK,EACD,aAAa,GACb,wBAAwB,GACxB,8BAA8B,GAC9B,WAAW,GACX,2BAA2B,GAC3B,6BAA6B,GAC7B,mBAAmB,GACnB,4BAA4B,GAC5B,iBAAiB,KAClB,IAAI,CAAC;IAEV,QAAQ,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC;CAChC;AAED,eAAO,MAAM,UAAU,GAEnB,MAAM,QAAQ,GACZ,UAAU,GACV,kBAAkB,GAClB,cAAc,GACd,cAAc,GACd,SAAS,GACT,SAAS,GACT,kBAAkB,GAClB,OAAO,GACP,kBAAkB,MAErB,QAAQ,UAAU,KAAG,MAAM,CAAC,IAAI,EAAE,WAAW,CAwO7C,CAAC;AAEJ,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB;AAGD,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,SAAS,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;CACpE;AAED,eAAO,MAAM,WAAW,GACrB,MAAM,cAAc,GAAG,SAAS,MAChC,4BAA+C,KAAG,KAiBlD,CAAC;AAMJ,MAAM,WAAW,aAAc,SAAQ,iBAAiB,EAAE,OAAO;CAAG;AAEpE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;CACjC;AA4KD,eAAO,MAAM,oBAAoB,GAC9B,MAAM,SAAS,GAAG,OAAO,MACzB,QAAQ,cAAc,KAAG,MAAM,CAAC,IAAI,EAAE,WAAW,CAkCjD,CAAC;AAEJ,eAAO,MAAM,aAAa,GACvB,MAAM,gBAAgB,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,MAExD,SAAS,OAAO,EAChB,UAAU,aAAa,CAAC,WAAW,CAAC,KACnC,MAAM,CAAC,IAAI,EAAE,WAAW,CAe1B,CAAC;AAuCJ,eAAO,MAAM,uCAAuC,GACjD,MAAM,gBAAgB,GAAG,SAAS,MAClC,SAAS,YAAY,EAAE,SAAS,WAAW,KAAG,MAAM,CAAC,IAAI,EAAE,WAAW,CA0BtE,CAAC;AAEJ;;;;;;;;;;GAUG;AACH,MAAM,MAAM,SAAS,GACjB,gBAAgB,GAChB,kBAAkB,GAClB,iBAAiB,GACjB,oBAAoB,CAAC;AAEzB;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;CACnC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;CACrC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,WAAW,GAAG,oBAAoB,CAAC;CACnE;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;CAC/B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;CACvC;AAED,eAAO,MAAM,gBAAgB,EAAE,gBAA+C,CAAC"}
@@ -1,5 +1,4 @@
1
1
  import { assert } from "../Assert.js";
2
- import { concatBytes } from "../Buffer.js";
3
2
  import { eqArrayNumber } from "../Eq.js";
4
3
  import { createTransferableError } from "../Error.js";
5
4
  import { constFalse } from "../Function.js";
@@ -351,7 +350,7 @@ export const applyLocalOnlyChange = (deps) => (change) => {
351
350
  }
352
351
  return ok();
353
352
  };
354
- const applyMessages = (deps) => (ownerId, messages) => {
353
+ export const applyMessages = (deps) => (ownerId, messages) => {
355
354
  const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
356
355
  for (const message of messages) {
357
356
  const result1 = applyMessageToAppTable(deps)(ownerIdBytes, message);
@@ -369,30 +368,26 @@ const applyMessageToAppTable = (deps) => (ownerId, message) => {
369
368
  for (const [column, value] of objectToEntries(message.change.values)) {
370
369
  const result = deps.sqlite.exec(sql.prepared `
371
370
  with
372
- lastTimestamp as (
373
- select "timestamp"
371
+ existingTimestamp as (
372
+ select 1
374
373
  from evolu_history
375
374
  where
376
375
  "ownerId" = ${ownerId}
377
376
  and "table" = ${message.change.table}
378
- and "id" = ${message.change.id}
377
+ and "id" = ${idToIdBytes(message.change.id)}
379
378
  and "column" = ${column}
380
- order by "timestamp" desc
379
+ and "timestamp" >= ${timestamp}
381
380
  limit 1
382
381
  )
383
382
  insert into ${sql.identifier(message.change.table)}
384
383
  ("id", ${sql.identifier(column)}, updatedAt)
385
384
  select ${message.change.id}, ${value}, ${updatedAt}
386
- where
387
- (select "timestamp" from lastTimestamp) is null
388
- or (select "timestamp" from lastTimestamp) < ${timestamp}
385
+ where not exists (select 1 from existingTimestamp)
389
386
  on conflict ("id") do update
390
387
  set
391
388
  ${sql.identifier(column)} = ${value},
392
389
  updatedAt = ${updatedAt}
393
- where
394
- (select "timestamp" from lastTimestamp) is null
395
- or (select "timestamp" from lastTimestamp) < ${timestamp};
390
+ where not exists (select 1 from existingTimestamp);
396
391
  `);
397
392
  if (!result.ok)
398
393
  return result;
@@ -426,37 +421,3 @@ export const applyMessageToTimestampAndHistoryTables = (deps) => (ownerId, messa
426
421
  return ok();
427
422
  };
428
423
  export const initialSyncState = { type: "SyncStateInitial" };
429
- /**
430
- * Efficiently checks which binary timestamps already exist in the database
431
- * using a single CTE query instead of N individual queries. Crucial for WASM
432
- * SQLite performance where JS↔WASM boundary crossings are expensive.
433
- *
434
- * Used for fast idempotency detection in writeMessages before onMessage
435
- * validation. While applyMessages ensures internal idempotency, this pre-check
436
- * is faster and required for main thread message validation.
437
- */
438
- export const getExistingTimestamps = (deps) => (ownerIdBytes, timestampsBytes) => {
439
- const concatenatedTimestamps = concatBytes(...timestampsBytes);
440
- const result = deps.sqlite.exec(sql `
441
- with recursive
442
- split_timestamps(timestampBytes, pos) as (
443
- select
444
- substr(${concatenatedTimestamps}, 1, 16),
445
- 17 as pos
446
- union all
447
- select
448
- substr(${concatenatedTimestamps}, pos, 16),
449
- pos + 16
450
- from split_timestamps
451
- where pos <= length(${concatenatedTimestamps})
452
- )
453
- select s.timestampBytes
454
- from
455
- split_timestamps s
456
- join evolu_timestamp t
457
- on t.ownerId = ${ownerIdBytes} and s.timestampBytes = t.t;
458
- `);
459
- if (!result.ok)
460
- return result;
461
- return ok(result.value.rows.map((row) => row.timestampBytes));
462
- };
@@ -228,23 +228,26 @@
228
228
  *
229
229
  * ### FAQ
230
230
  *
231
- * #### What if my function doesn't return a value on success?
231
+ * #### When should a function return a plain value instead of `Result<T, E>`?
232
232
  *
233
- * If your function performs an operation but doesn't need to return a value on
234
- * success, you can use `Result<void, E>`. Using `Result<void, E>` is clearer
235
- * than using `Result<true, E>` or `Result<null, E>` because it communicates
236
- * that the function doesn't produce a value but can produce errors.
233
+ * Use `Result<T, E>` only when a function can fail with **known, expected
234
+ * errors** that callers need to handle. If a function cannot fail with a known
235
+ * error, return the value directly.
236
+ *
237
+ * - ✅ Return `Result<User, UserNotFoundError>` - can fail with a known error
238
+ * - ✅ Return `User` - cannot fail with a known error
239
+ * - ❌ Don't return `Result<User, never>` - unnecessary wrapper
237
240
  *
238
- * #### When can a function return `void` instead of `Result<void, E>`?
241
+ * This keeps the codebase clean and makes error handling intentional. The type
242
+ * system communicates which operations can fail and which cannot.
239
243
  *
240
- * A function can safely return `void` (instead of `Result<void, E>`) when all
241
- * unsafe code within it is properly wrapped with `trySync` or `tryAsync`. If
242
- * developers consistently wrap all potentially throwing operations, then any
243
- * function returning `void` is guaranteed not to throw and can be called
244
- * without error handling.
244
+ * Unsafe code from external libraries (not under our control) should be wrapped
245
+ * with `trySync` or `tryAsync` at the boundaries. Once wrapped, if the error is
246
+ * not important to callers, functions can safely return plain values. If the
247
+ * error matters, use `Result` with a typed error.
245
248
  *
246
249
  * ```ts
247
- * // ✅ Safe to return void - all unsafe code is wrapped
250
+ * // ✅ Safe to return void - unsafe code is wrapped and error is handled
248
251
  * const processData = (data: string): void => {
249
252
  * const parseResult = trySync(
250
253
  * () => JSON.parse(data),
@@ -252,7 +255,7 @@
252
255
  * );
253
256
  *
254
257
  * if (!parseResult.ok) {
255
- * logError(parseResult.error); // Handle error appropriately
258
+ * logError(parseResult.error);
256
259
  * return;
257
260
  * }
258
261
  *
@@ -263,9 +266,12 @@
263
266
  * processData(jsonString);
264
267
  * ```
265
268
  *
266
- * This approach creates a clear contract: functions returning `void` are safe
267
- * to call, while functions returning `Result<T, E>` require explicit error
268
- * handling.
269
+ * #### What if my function doesn't return a value on success?
270
+ *
271
+ * If your function performs an operation but doesn't need to return a value on
272
+ * success, you can use `Result<void, E>`. Using `Result<void, E>` is clearer
273
+ * than using `Result<true, E>` or `Result<null, E>` because it communicates
274
+ * that the function doesn't produce a value but can produce errors.
269
275
  *
270
276
  * #### How do I short-circuit processing of an array on the first error?
271
277
  *
@@ -441,6 +447,29 @@ export declare const err: <E>(error: E) => Err<E>;
441
447
  * Throws: `Error` with the original error attached as `cause`.
442
448
  */
443
449
  export declare const getOrThrow: <T, E>(result: Result<T, E>) => T;
450
+ /**
451
+ * Extracts the value from a {@link Result} if it is an `Ok`, or returns `null`
452
+ * if it is an `Err`.
453
+ *
454
+ * **Intended usage:**
455
+ *
456
+ * - When you need to convert a `Result` to a nullable value for APIs that expect
457
+ * `T | null`.
458
+ * - When the error is not important and you just want the value or nothing.
459
+ *
460
+ * ### Example
461
+ *
462
+ * ```ts
463
+ * const parseResult = parseJson('{"key": "value"}');
464
+ * const value = getOrNull(parseResult);
465
+ * // value is unknown | null
466
+ *
467
+ * if (value != null) {
468
+ * console.log("Parsed value:", value);
469
+ * }
470
+ * ```
471
+ */
472
+ export declare const getOrNull: <T, E>(result: Result<T, E>) => T | null;
444
473
  /**
445
474
  * Wraps synchronous functions that may throw exceptions, returning a
446
475
  * {@link Result}.
@@ -1 +1 @@
1
- {"version":3,"file":"Result.d.ts","sourceRoot":"","sources":["../../src/Result.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmUG;AACH,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;AAE1C,mCAAmC;AACnC,MAAM,WAAW,EAAE,CAAC,CAAC;IACnB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,GAAG,CAAC,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAC5C,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAEpC;;;;GAIG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAC7C,CAAC,SAAS,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAErC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;AAC/B,2DAA2D;AAC3D,wBAAgB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;AAKvC;;;;;;;;;GASG;AACH,eAAO,MAAM,GAAG,GAAI,CAAC,EAAE,OAAO,CAAC,KAAG,GAAG,CAAC,CAAC,CAA2B,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,CAMvD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,CAAC,EAC1B,IAAI,MAAM,CAAC,EACX,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,KAC9B,MAAM,CAAC,CAAC,EAAE,CAAC,CAMb,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,QAAQ,GAAU,CAAC,EAAE,CAAC,EACjC,WAAW,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,KAC9B,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAIpB,CAAC"}
1
+ {"version":3,"file":"Result.d.ts","sourceRoot":"","sources":["../../src/Result.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyUG;AACH,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;AAE1C,mCAAmC;AACnC,MAAM,WAAW,EAAE,CAAC,CAAC;IACnB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,GAAG,CAAC,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAC5C,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAEpC;;;;GAIG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAC7C,CAAC,SAAS,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAErC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;AAC/B,2DAA2D;AAC3D,wBAAgB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;AAKvC;;;;;;;;;GASG;AACH,eAAO,MAAM,GAAG,GAAI,CAAC,EAAE,OAAO,CAAC,KAAG,GAAG,CAAC,CAAC,CAA2B,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,CAMvD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,CAAC,GAAG,IAC1B,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,CAAC,EAC1B,IAAI,MAAM,CAAC,EACX,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,KAC9B,MAAM,CAAC,CAAC,EAAE,CAAC,CAMb,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,QAAQ,GAAU,CAAC,EAAE,CAAC,EACjC,WAAW,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,KAC9B,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAIpB,CAAC"}