@evolu/common 6.0.1-preview.3 → 6.0.1-preview.30
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/dist/src/Array.d.ts +58 -5
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +53 -5
- package/dist/src/Assert.d.ts +6 -16
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +6 -18
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +8 -7
- package/dist/src/Cache.d.ts +44 -0
- package/dist/src/Cache.d.ts.map +1 -0
- package/dist/src/Cache.js +52 -0
- package/dist/src/Callbacks.d.ts +45 -12
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +14 -7
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +32 -45
- package/dist/src/Evolu/Db.d.ts +158 -65
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +286 -694
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +208 -133
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +188 -183
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +150 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +174 -0
- package/dist/src/Evolu/Owner.d.ts +264 -120
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +130 -104
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +277 -232
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +603 -378
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts +92 -7
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +243 -76
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +212 -26
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +137 -79
- package/dist/src/Evolu/Sync.d.ts +68 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +422 -20
- package/dist/src/Evolu/Timestamp.d.ts +85 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +77 -18
- package/dist/src/Identicon.d.ts +35 -0
- package/dist/src/Identicon.d.ts.map +1 -0
- package/dist/src/Identicon.js +143 -0
- package/dist/src/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/Instances.js +44 -0
- package/dist/src/ManyToManyMap.d.ts +71 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +4 -3
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/Platform.d.ts +20 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +22 -0
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Resources.d.ts +118 -0
- package/dist/src/Resources.d.ts.map +1 -0
- package/dist/src/Resources.js +197 -0
- package/dist/src/Result.d.ts +184 -52
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Skiplist.js +2 -1
- package/dist/src/Sqlite.d.ts +63 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +110 -9
- package/dist/src/Task.d.ts +586 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +469 -0
- package/dist/src/Time.d.ts +66 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +99 -5
- package/dist/src/Type.d.ts +621 -340
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +665 -464
- package/dist/src/Types.d.ts +1 -75
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -18
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/package.json +14 -13
- package/src/Array.ts +76 -11
- package/src/Assert.ts +6 -24
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +7 -7
- package/src/Cache.ts +85 -0
- package/src/Callbacks.ts +62 -22
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +97 -82
- package/src/Evolu/Db.ts +514 -1020
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +464 -355
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +463 -0
- package/src/Evolu/Owner.ts +369 -228
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +859 -676
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +420 -92
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +451 -118
- package/src/Evolu/Sync.ts +720 -37
- package/src/Evolu/Timestamp.ts +88 -35
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/ManyToManyMap.ts +124 -24
- package/src/Number.ts +6 -10
- package/src/Platform.ts +26 -0
- package/src/Random.ts +3 -2
- package/src/Resources.ts +367 -0
- package/src/Result.ts +191 -54
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +122 -17
- package/src/Task.ts +901 -0
- package/src/Time.ts +180 -5
- package/src/Type.ts +1083 -727
- package/src/Types.ts +1 -77
- package/src/WebSocket.ts +27 -25
- package/src/Worker.ts +72 -23
- package/src/index.ts +7 -2
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
|
@@ -1,10 +1,196 @@
|
|
|
1
|
+
import { NonEmptyReadonlyArray } from "../Array.js";
|
|
2
|
+
import { Brand } from "../Brand.js";
|
|
3
|
+
import { RandomDep } from "../Random.js";
|
|
4
|
+
import { Result } from "../Result.js";
|
|
5
|
+
import { SqliteDep, SqliteError } from "../Sqlite.js";
|
|
6
|
+
import { MaybeAsync } from "../Task.js";
|
|
7
|
+
import { NonNegativeInt, PositiveInt } from "../Type.js";
|
|
8
|
+
import { BaseOwnerError, OwnerId, OwnerIdBytes, OwnerWriteKey } from "./Owner.js";
|
|
9
|
+
import { Timestamp, TimestampBytes } from "./Timestamp.js";
|
|
10
|
+
export interface StorageConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Callback called before an attempt to write, to check if an {@link OwnerId}
|
|
13
|
+
* has sufficient quota for the write.
|
|
14
|
+
*
|
|
15
|
+
* The callback receives the {@link OwnerId} and the number of bytes required
|
|
16
|
+
* for the write, and returns a {@link MaybeAsync} boolean: `true` to allow the
|
|
17
|
+
* write, or `false` to deny it due to quota limits.
|
|
18
|
+
*
|
|
19
|
+
* The callback can be synchronous (for SQLite or in-memory checks) or
|
|
20
|
+
* asynchronous (for calling remote APIs).
|
|
21
|
+
*
|
|
22
|
+
* The callback returns a boolean rather than an error type because error
|
|
23
|
+
* handling and logging are the responsibility of the callback
|
|
24
|
+
* implementation.
|
|
25
|
+
*
|
|
26
|
+
* ### Example
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* // Client
|
|
30
|
+
* // evolu.subscribeError
|
|
31
|
+
*
|
|
32
|
+
* // Relay
|
|
33
|
+
* isOwnerWithinQuota: (ownerId, requiredBytes) => {
|
|
34
|
+
* console.log(ownerId, requiredBytes);
|
|
35
|
+
* // Check error via evolu.subscribeError
|
|
36
|
+
* return true;
|
|
37
|
+
* };
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
readonly isOwnerWithinQuota: (ownerId: OwnerId, requiredBytes: PositiveInt) => MaybeAsync<boolean>;
|
|
41
|
+
}
|
|
1
42
|
/**
|
|
2
|
-
* Evolu Storage
|
|
43
|
+
* Evolu Storage
|
|
44
|
+
*
|
|
45
|
+
* Evolu protocol using Storage is agnostic to storage implementation
|
|
46
|
+
* details—any storage can be plugged in, as long as it implements this
|
|
47
|
+
* interface. Implementations must handle their own errors; return values only
|
|
48
|
+
* indicate overall success or failure.
|
|
49
|
+
*
|
|
50
|
+
* The Storage API is synchronous because SQLite's synchronous API is the
|
|
51
|
+
* fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
|
|
52
|
+
* SQLite's C API directly with no context switching between the event loop and
|
|
53
|
+
* native code, and no promise microtasks or await overhead.
|
|
54
|
+
*
|
|
55
|
+
* The only exception is {@link Storage#writeMessages}, which is async to allow
|
|
56
|
+
* for async validation logic before writing to storage. The write operation
|
|
57
|
+
* itself remains synchronous.
|
|
58
|
+
*/
|
|
59
|
+
export interface Storage {
|
|
60
|
+
readonly getSize: (ownerId: OwnerIdBytes) => NonNegativeInt | null;
|
|
61
|
+
readonly fingerprint: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt) => Fingerprint | null;
|
|
62
|
+
/**
|
|
63
|
+
* Computes fingerprints with their upper bounds in one call.
|
|
64
|
+
*
|
|
65
|
+
* This function can be replaced with many fingerprint/findLowerBound calls,
|
|
66
|
+
* but implementations can leverage it for batching and more efficient
|
|
67
|
+
* fingerprint computation.
|
|
68
|
+
*/
|
|
69
|
+
readonly fingerprintRanges: (ownerId: OwnerIdBytes, buckets: ReadonlyArray<NonNegativeInt>, upperBound?: RangeUpperBound) => ReadonlyArray<FingerprintRange> | null;
|
|
70
|
+
readonly findLowerBound: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt, upperBound: RangeUpperBound) => NonNegativeInt | null;
|
|
71
|
+
readonly iterate: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt, callback: (timestamp: TimestampBytes, index: NonNegativeInt) => boolean) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Validates the {@link OwnerWriteKey} for the given {@link Owner}.
|
|
74
|
+
*
|
|
75
|
+
* Returns `true` if the write key is valid, `false` otherwise.
|
|
76
|
+
*/
|
|
77
|
+
readonly validateWriteKey: (ownerId: OwnerIdBytes, writeKey: OwnerWriteKey) => boolean;
|
|
78
|
+
/** Sets the {@link OwnerWriteKey} for the given {@link Owner}. */
|
|
79
|
+
readonly setWriteKey: (ownerId: OwnerIdBytes, writeKey: OwnerWriteKey) => boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Write encrypted {@link CrdtMessage}s to storage.
|
|
82
|
+
*
|
|
83
|
+
* Must use a mutex per ownerId to ensure sequential processing and proper
|
|
84
|
+
* protocol logic handling during sync operations.
|
|
85
|
+
*
|
|
86
|
+
* TODO: Use MaybeAsync
|
|
87
|
+
*/
|
|
88
|
+
readonly writeMessages: (ownerIdBytes: OwnerIdBytes, messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>) => MaybeAsync<Result<void, StorageWriteError | StorageQuotaError>>;
|
|
89
|
+
/** Read encrypted {@link DbChange}s from storage. */
|
|
90
|
+
readonly readDbChange: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => EncryptedDbChange | null;
|
|
91
|
+
/**
|
|
92
|
+
* Delete all data for the given {@link Owner}.
|
|
93
|
+
*
|
|
94
|
+
* Returns `true` on success, `false` on failure.
|
|
95
|
+
*/
|
|
96
|
+
readonly deleteOwner: (ownerId: OwnerIdBytes) => boolean;
|
|
97
|
+
}
|
|
98
|
+
export interface StorageDep {
|
|
99
|
+
readonly storage: Storage;
|
|
100
|
+
}
|
|
101
|
+
/** Error indicating a serious write failure. */
|
|
102
|
+
export interface StorageWriteError extends BaseOwnerError {
|
|
103
|
+
readonly type: "StorageWriteError";
|
|
104
|
+
}
|
|
105
|
+
/** Error when storage or billing quota is exceeded. */
|
|
106
|
+
export interface StorageQuotaError extends BaseOwnerError {
|
|
107
|
+
readonly type: "StorageQuotaError";
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A cryptographic hash used for efficiently comparing collections of
|
|
111
|
+
* {@link TimestampBytes}s.
|
|
112
|
+
*
|
|
113
|
+
* It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
|
|
114
|
+
* one or more timestamps.
|
|
115
|
+
*/
|
|
116
|
+
export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
|
|
117
|
+
export declare const fingerprintSize: number & Brand<"Int"> & Brand<"NonNegative">;
|
|
118
|
+
/** A fingerprint of an empty range. */
|
|
119
|
+
export declare const zeroFingerprint: Fingerprint;
|
|
120
|
+
export interface BaseRange {
|
|
121
|
+
readonly upperBound: RangeUpperBound;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Union type for Range's upperBound: either a {@link TimestampBytes} or
|
|
125
|
+
* {@link InfiniteUpperBound}.
|
|
126
|
+
*/
|
|
127
|
+
export type RangeUpperBound = TimestampBytes | InfiniteUpperBound;
|
|
128
|
+
export declare const InfiniteUpperBound: unique symbol;
|
|
129
|
+
export type InfiniteUpperBound = typeof InfiniteUpperBound;
|
|
130
|
+
export declare const RangeType: {
|
|
131
|
+
readonly Fingerprint: 1;
|
|
132
|
+
readonly Skip: 0;
|
|
133
|
+
readonly Timestamps: 2;
|
|
134
|
+
};
|
|
135
|
+
export type RangeType = (typeof RangeType)[keyof typeof RangeType];
|
|
136
|
+
export interface SkipRange extends BaseRange {
|
|
137
|
+
readonly type: typeof RangeType.Skip;
|
|
138
|
+
}
|
|
139
|
+
export interface FingerprintRange extends BaseRange {
|
|
140
|
+
readonly type: typeof RangeType.Fingerprint;
|
|
141
|
+
readonly fingerprint: Fingerprint;
|
|
142
|
+
}
|
|
143
|
+
export interface TimestampsRange extends BaseRange {
|
|
144
|
+
readonly type: typeof RangeType.Timestamps;
|
|
145
|
+
readonly timestamps: ReadonlyArray<TimestampBytes>;
|
|
146
|
+
}
|
|
147
|
+
export type Range = SkipRange | FingerprintRange | TimestampsRange;
|
|
148
|
+
/** An encrypted {@link CrdtMessage}. */
|
|
149
|
+
export interface EncryptedCrdtMessage {
|
|
150
|
+
readonly timestamp: Timestamp;
|
|
151
|
+
readonly change: EncryptedDbChange;
|
|
152
|
+
}
|
|
153
|
+
/** Encrypted DbChange */
|
|
154
|
+
export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
|
|
155
|
+
/**
|
|
156
|
+
* A CRDT message combining a unique {@link Timestamp} with a {@link DbChange}.
|
|
157
|
+
*
|
|
158
|
+
* Used in Evolu's sync protocol to replicate data changes across devices. Evolu
|
|
159
|
+
* operates as a durable queue, providing exactly-once delivery guarantees for
|
|
160
|
+
* reliable synchronization across application restarts and network failures.
|
|
161
|
+
*/
|
|
162
|
+
export interface CrdtMessage {
|
|
163
|
+
readonly timestamp: Timestamp;
|
|
164
|
+
readonly change: DbChange;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A DbChange is a change to a table row. Together with a unique
|
|
168
|
+
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
169
|
+
*/
|
|
170
|
+
export declare const DbChange: import("../Type.js").ObjectType<{
|
|
171
|
+
table: import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>;
|
|
172
|
+
id: import("../Type.js").BrandType<import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>, "Id", import("../Type.js").IdError, import("../Type.js").StringError>;
|
|
173
|
+
values: import("../Type.js").RecordType<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError, import("../Type.js").UnionType<[import("../Type.js").Type<"Null", null, null, import("../Type.js").NullError, null, import("../Type.js").NullError>, import("../Type.js").Type<"String", string, string, import("../Type.js").StringError, string, import("../Type.js").StringError>, import("../Type.js").Type<"Number", number, number, import("../Type.js").NumberError, number, import("../Type.js").NumberError>, import("../Type.js").Type<"Uint8Array", Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>, import("../Type.js").Uint8ArrayError, Uint8Array<ArrayBufferLike>, import("../Type.js").Uint8ArrayError>]>>;
|
|
174
|
+
}>;
|
|
175
|
+
export type DbChange = typeof DbChange.Type;
|
|
176
|
+
/**
|
|
177
|
+
* Common interface for both client and relay SQLite storages.
|
|
3
178
|
*
|
|
4
179
|
* Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
|
|
5
180
|
* implemented in SQL, so it doesn't have to make roundtrips to the DB.
|
|
6
181
|
*
|
|
7
|
-
*
|
|
182
|
+
* While the SQL implementation may look sophisticated, it's conceptually simple
|
|
183
|
+
* and LLMs can explain how it works. The Skiplist data structure is well
|
|
184
|
+
* explained in [this Stack Overflow
|
|
185
|
+
* answer](https://stackoverflow.com/questions/61944198/what-is-a-zip-tree-and-how-does-it-work).
|
|
186
|
+
* The logic resembles [Negentropy's C++
|
|
187
|
+
* storage](https://github.com/hoytech/negentropy), except we use a Skiplist to
|
|
188
|
+
* leverage SQLite indexes, which makes the code simpler.
|
|
189
|
+
*
|
|
190
|
+
* Note: A paid review by the SQLite team is planned, as they use the same
|
|
191
|
+
* algorithm for their rsync tool.
|
|
192
|
+
*
|
|
193
|
+
* The ideal storage for a Relay should use an architecture like
|
|
8
194
|
* [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
|
|
9
195
|
* to ensure that insertion order doesn't matter (local-first apps can often
|
|
10
196
|
* write in the past.)
|
|
@@ -19,31 +205,31 @@
|
|
|
19
205
|
* each other, if necessary. One relay should handle hundreds of thousands of
|
|
20
206
|
* users, and when it goes down, nothing happens, because it will be
|
|
21
207
|
* synchronized later.
|
|
22
|
-
*
|
|
23
|
-
* @module
|
|
24
208
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
readonly insertTimestamp: (ownerId:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
readonly
|
|
39
|
-
}
|
|
40
|
-
export interface
|
|
41
|
-
readonly storage:
|
|
42
|
-
}
|
|
43
|
-
export type SqliteStorageDeps =
|
|
44
|
-
export interface
|
|
209
|
+
export interface BaseSqliteStorage extends Pick<Storage, "getSize" | "fingerprint" | "fingerprintRanges" | "findLowerBound" | "iterate" | "deleteOwner"> {
|
|
210
|
+
/**
|
|
211
|
+
* Inserts a timestamp for an owner into the skiplist-based storage.
|
|
212
|
+
*
|
|
213
|
+
* Must be idempotent - inserting the same timestamp multiple times has no
|
|
214
|
+
* effect after the first insertion. This is crucial for sync reliability as
|
|
215
|
+
* messages may be received and processed multiple times.
|
|
216
|
+
*/
|
|
217
|
+
readonly insertTimestamp: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => Result<void, SqliteError>;
|
|
218
|
+
/**
|
|
219
|
+
* Efficiently checks which timestamps already exist in the database using a
|
|
220
|
+
* single CTE query instead of N individual queries.
|
|
221
|
+
*/
|
|
222
|
+
readonly getExistingTimestamps: (ownerIdBytes: OwnerIdBytes, timestampsBytes: NonEmptyReadonlyArray<TimestampBytes>) => Result<ReadonlyArray<TimestampBytes>, SqliteError>;
|
|
223
|
+
}
|
|
224
|
+
export interface BaseSqliteStorageDep {
|
|
225
|
+
readonly storage: BaseSqliteStorage;
|
|
226
|
+
}
|
|
227
|
+
export type SqliteStorageDeps = RandomDep & SqliteDep;
|
|
228
|
+
export interface CreateBaseSqliteStorageConfig extends StorageConfig {
|
|
45
229
|
onStorageError: (error: SqliteError) => void;
|
|
46
230
|
}
|
|
47
|
-
export declare const
|
|
48
|
-
export declare const
|
|
231
|
+
export declare const createBaseSqliteStorage: (deps: SqliteStorageDeps) => (config: CreateBaseSqliteStorageConfig) => BaseSqliteStorage;
|
|
232
|
+
export declare const createBaseSqliteStorageTables: (deps: SqliteDep) => Result<void, SqliteError>;
|
|
233
|
+
export declare const timestampBytesToFingerprint: (timestamp: TimestampBytes) => Fingerprint;
|
|
234
|
+
export declare const getTimestampByIndex: (deps: SqliteDep) => (ownerId: OwnerIdBytes, index: NonNegativeInt) => Result<TimestampBytes, SqliteError>;
|
|
49
235
|
//# sourceMappingURL=Storage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Storage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,75 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
* Evolu Storage for SQLite
|
|
3
|
-
*
|
|
4
|
-
* Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
|
|
5
|
-
* implemented in SQL, so it doesn't have to make roundtrips to the DB.
|
|
6
|
-
*
|
|
7
|
-
* The ideal storage for a Relay should use a similar architecture to
|
|
8
|
-
* [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
|
|
9
|
-
* to ensure that insertion order doesn't matter (local-first apps can often
|
|
10
|
-
* write in the past.)
|
|
11
|
-
*
|
|
12
|
-
* The ideal client implementation should probably use the SQLite extension
|
|
13
|
-
* instead of SQL or even a KV storage, when such a thing for browsers/native
|
|
14
|
-
* will exist and will be faster than SQLite.
|
|
15
|
-
*
|
|
16
|
-
* # Scaling
|
|
17
|
-
*
|
|
18
|
-
* The load can be distributed by deploying multiple relays, synchronized with
|
|
19
|
-
* each other, if necessary. One relay should handle hundreds of thousands of
|
|
20
|
-
* users, and when it goes down, nothing happens, because it will be
|
|
21
|
-
* synchronized later.
|
|
22
|
-
*
|
|
23
|
-
* @module
|
|
24
|
-
*/
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
25
2
|
import { assert } from "../Assert.js";
|
|
3
|
+
import { concatBytes } from "../Buffer.js";
|
|
26
4
|
import { decrement } from "../Number.js";
|
|
27
5
|
import { ok } from "../Result.js";
|
|
28
|
-
import { sql } from "../Sqlite.js";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
6
|
+
import { sql, SqliteValue } from "../Sqlite.js";
|
|
7
|
+
import { Id, NonNegativeInt, object, PositiveInt, record, String, } from "../Type.js";
|
|
8
|
+
import { ownerIdBytesToOwnerId, } from "./Owner.js";
|
|
9
|
+
import { orderTimestampBytes } from "./Timestamp.js";
|
|
10
|
+
export const fingerprintSize = NonNegativeInt.orThrow(12);
|
|
11
|
+
/** A fingerprint of an empty range. */
|
|
12
|
+
export const zeroFingerprint = new Uint8Array(fingerprintSize);
|
|
13
|
+
export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
|
|
14
|
+
export const RangeType = {
|
|
15
|
+
Fingerprint: 1,
|
|
16
|
+
Skip: 0,
|
|
17
|
+
Timestamps: 2,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A DbChange is a change to a table row. Together with a unique
|
|
21
|
+
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
22
|
+
*/
|
|
23
|
+
export const DbChange = object({
|
|
24
|
+
table: String,
|
|
25
|
+
id: Id,
|
|
26
|
+
values: record(String, SqliteValue),
|
|
27
|
+
});
|
|
28
|
+
export const createBaseSqliteStorage = (deps) => (config) => {
|
|
29
|
+
// TODO: Use evolu_usage table.
|
|
35
30
|
const ownerStats = new Map();
|
|
36
|
-
return
|
|
37
|
-
insertTimestamp: (ownerId, timestamp) => {
|
|
38
|
-
const ownerIdString = binaryOwnerIdToOwnerId(ownerId);
|
|
39
|
-
const level = randomSkiplistLevel(deps);
|
|
40
|
-
let stats = ownerStats.get(ownerIdString);
|
|
41
|
-
if (!stats) {
|
|
42
|
-
const result = deps.sqlite.exec(sql.prepared `
|
|
43
|
-
select min(t) as minT, max(t) as maxT
|
|
44
|
-
from evolu_timestamp
|
|
45
|
-
where ownerId = ${ownerId};
|
|
46
|
-
`);
|
|
47
|
-
if (!result.ok)
|
|
48
|
-
return result;
|
|
49
|
-
stats = {
|
|
50
|
-
minT: result.value.rows[0].minT ?? timestamp,
|
|
51
|
-
maxT: result.value.rows[0].maxT ?? timestamp,
|
|
52
|
-
};
|
|
53
|
-
ownerStats.set(ownerIdString, stats);
|
|
54
|
-
}
|
|
55
|
-
let strategy;
|
|
56
|
-
if (orderBinaryTimestamp(timestamp, stats.maxT) === 1) {
|
|
57
|
-
strategy = "append";
|
|
58
|
-
stats.maxT = timestamp;
|
|
59
|
-
}
|
|
60
|
-
else if (orderBinaryTimestamp(timestamp, stats.minT) === -1) {
|
|
61
|
-
strategy = "prepend";
|
|
62
|
-
stats.minT = timestamp;
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
strategy = "insert";
|
|
66
|
-
}
|
|
67
|
-
return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
|
|
68
|
-
},
|
|
31
|
+
return {
|
|
69
32
|
getSize: (ownerId) => {
|
|
70
33
|
const size = getSize(deps)(ownerId);
|
|
71
34
|
if (!size.ok) {
|
|
72
|
-
|
|
35
|
+
config.onStorageError(size.error);
|
|
73
36
|
return null;
|
|
74
37
|
}
|
|
75
38
|
return size.value;
|
|
@@ -78,7 +41,7 @@ export const createSqliteStorageBase = (deps) => (options) => {
|
|
|
78
41
|
assertBeginEnd(begin, end);
|
|
79
42
|
const result = fingerprint(deps)(ownerId, begin, end);
|
|
80
43
|
if (!result.ok) {
|
|
81
|
-
|
|
44
|
+
config.onStorageError(result.error);
|
|
82
45
|
return null;
|
|
83
46
|
}
|
|
84
47
|
return result.value;
|
|
@@ -86,7 +49,7 @@ export const createSqliteStorageBase = (deps) => (options) => {
|
|
|
86
49
|
fingerprintRanges: (ownerId, buckets, upperBound) => {
|
|
87
50
|
const ranges = fingerprintRanges(deps)(ownerId, buckets, upperBound);
|
|
88
51
|
if (!ranges.ok) {
|
|
89
|
-
|
|
52
|
+
config.onStorageError(ranges.error);
|
|
90
53
|
return null;
|
|
91
54
|
}
|
|
92
55
|
return ranges.value;
|
|
@@ -94,7 +57,7 @@ export const createSqliteStorageBase = (deps) => (options) => {
|
|
|
94
57
|
findLowerBound: (ownerId, begin, end, upperBound) => {
|
|
95
58
|
const lowerBound = findLowerBound(deps)(ownerId, begin, end, upperBound);
|
|
96
59
|
if (!lowerBound.ok) {
|
|
97
|
-
|
|
60
|
+
config.onStorageError(lowerBound.error);
|
|
98
61
|
return null;
|
|
99
62
|
}
|
|
100
63
|
return lowerBound.value;
|
|
@@ -107,7 +70,7 @@ export const createSqliteStorageBase = (deps) => (options) => {
|
|
|
107
70
|
// This is much faster than SQL limit with offset.
|
|
108
71
|
const first = getTimestampByIndex(deps)(ownerId, begin);
|
|
109
72
|
if (!first.ok) {
|
|
110
|
-
|
|
73
|
+
config.onStorageError(first.error);
|
|
111
74
|
return;
|
|
112
75
|
}
|
|
113
76
|
if (!callback(first.value, begin))
|
|
@@ -132,21 +95,88 @@ export const createSqliteStorageBase = (deps) => (options) => {
|
|
|
132
95
|
limit ${length - 1};
|
|
133
96
|
`);
|
|
134
97
|
if (!result.ok) {
|
|
135
|
-
|
|
98
|
+
config.onStorageError(result.error);
|
|
136
99
|
return;
|
|
137
100
|
}
|
|
138
101
|
for (let i = 0; i < result.value.rows.length; i++) {
|
|
139
|
-
const index = (begin + 1 + i);
|
|
102
|
+
const index = NonNegativeInt.orThrow(begin + 1 + i);
|
|
140
103
|
if (!callback(result.value.rows[i].t, index))
|
|
141
104
|
return;
|
|
142
105
|
}
|
|
143
106
|
},
|
|
144
|
-
|
|
107
|
+
deleteOwner: (ownerId) => {
|
|
108
|
+
const result = deps.sqlite.exec(sql `
|
|
109
|
+
delete from evolu_timestamp where ownerId = ${ownerId};
|
|
110
|
+
`);
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
config.onStorageError(result.error);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
},
|
|
117
|
+
insertTimestamp: (ownerId, timestamp) => {
|
|
118
|
+
const ownerIdString = ownerIdBytesToOwnerId(ownerId);
|
|
119
|
+
const level = randomSkiplistLevel(deps);
|
|
120
|
+
let stats = ownerStats.get(ownerIdString);
|
|
121
|
+
if (!stats) {
|
|
122
|
+
const result = deps.sqlite.exec(sql.prepared `
|
|
123
|
+
select min(t) as minT, max(t) as maxT
|
|
124
|
+
from evolu_timestamp
|
|
125
|
+
where ownerId = ${ownerId};
|
|
126
|
+
`);
|
|
127
|
+
if (!result.ok)
|
|
128
|
+
return result;
|
|
129
|
+
stats = {
|
|
130
|
+
minT: result.value.rows[0].minT ?? timestamp,
|
|
131
|
+
maxT: result.value.rows[0].maxT ?? timestamp,
|
|
132
|
+
};
|
|
133
|
+
ownerStats.set(ownerIdString, stats);
|
|
134
|
+
}
|
|
135
|
+
let strategy;
|
|
136
|
+
if (orderTimestampBytes(timestamp, stats.maxT) === 1) {
|
|
137
|
+
strategy = "append";
|
|
138
|
+
stats.maxT = timestamp;
|
|
139
|
+
}
|
|
140
|
+
else if (orderTimestampBytes(timestamp, stats.minT) === -1) {
|
|
141
|
+
strategy = "prepend";
|
|
142
|
+
stats.minT = timestamp;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
strategy = "insert";
|
|
146
|
+
}
|
|
147
|
+
return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
|
|
148
|
+
},
|
|
149
|
+
getExistingTimestamps: (ownerIdBytes, timestampsBytes) => {
|
|
150
|
+
const concatenatedTimestamps = concatBytes(...timestampsBytes);
|
|
151
|
+
const result = deps.sqlite.exec(sql `
|
|
152
|
+
with recursive
|
|
153
|
+
split_timestamps(timestampBytes, pos) as (
|
|
154
|
+
select
|
|
155
|
+
substr(${concatenatedTimestamps}, 1, 16),
|
|
156
|
+
17 as pos
|
|
157
|
+
union all
|
|
158
|
+
select
|
|
159
|
+
substr(${concatenatedTimestamps}, pos, 16),
|
|
160
|
+
pos + 16
|
|
161
|
+
from split_timestamps
|
|
162
|
+
where pos <= length(${concatenatedTimestamps})
|
|
163
|
+
)
|
|
164
|
+
select s.timestampBytes
|
|
165
|
+
from
|
|
166
|
+
split_timestamps s
|
|
167
|
+
join evolu_timestamp t
|
|
168
|
+
on t.ownerId = ${ownerIdBytes} and s.timestampBytes = t.t;
|
|
169
|
+
`);
|
|
170
|
+
if (!result.ok)
|
|
171
|
+
return result;
|
|
172
|
+
return ok(result.value.rows.map((row) => row.timestampBytes));
|
|
173
|
+
},
|
|
174
|
+
};
|
|
145
175
|
};
|
|
146
176
|
const assertBeginEnd = (begin, end) => {
|
|
147
177
|
assert(begin <= end, "invalid begin or end");
|
|
148
178
|
};
|
|
149
|
-
const
|
|
179
|
+
export const createBaseSqliteStorageTables = (deps) => {
|
|
150
180
|
for (const query of [
|
|
151
181
|
/**
|
|
152
182
|
* Creates the `evolu_timestamp` table for storing timestamps of multiple
|
|
@@ -159,7 +189,7 @@ const createTables = (deps) => {
|
|
|
159
189
|
*
|
|
160
190
|
* Columns:
|
|
161
191
|
*
|
|
162
|
-
* - `t` –
|
|
192
|
+
* - `t` – TimestampBytes
|
|
163
193
|
* - `h1`/`h2` – 12-byte fingerprint split into two integers for fast XOR
|
|
164
194
|
* - `c` – incremental count
|
|
165
195
|
* - `l` – Skiplist level (1 to 32)
|
|
@@ -171,7 +201,7 @@ const createTables = (deps) => {
|
|
|
171
201
|
* enough even without it.
|
|
172
202
|
*/
|
|
173
203
|
sql `
|
|
174
|
-
create table
|
|
204
|
+
create table evolu_timestamp (
|
|
175
205
|
"ownerId" blob not null,
|
|
176
206
|
"t" blob not null,
|
|
177
207
|
"h1" integer,
|
|
@@ -183,7 +213,7 @@ const createTables = (deps) => {
|
|
|
183
213
|
strict;
|
|
184
214
|
`,
|
|
185
215
|
sql `
|
|
186
|
-
create index
|
|
216
|
+
create index evolu_timestamp_index on evolu_timestamp (
|
|
187
217
|
"ownerId",
|
|
188
218
|
"l",
|
|
189
219
|
"t",
|
|
@@ -191,6 +221,30 @@ const createTables = (deps) => {
|
|
|
191
221
|
"h2",
|
|
192
222
|
"c"
|
|
193
223
|
);
|
|
224
|
+
`,
|
|
225
|
+
/**
|
|
226
|
+
* Creates the `evolu_usage` table for tracking data consumption per owner.
|
|
227
|
+
*
|
|
228
|
+
* Columns:
|
|
229
|
+
*
|
|
230
|
+
* - `ownerId` – OwnerIdBytes (primary key)
|
|
231
|
+
* - `storedBytes` – total bytes stored in database
|
|
232
|
+
* - `receivedBytes` – TODO: Decide how to use
|
|
233
|
+
* - `sentBytes` – TODO: Decide how to use
|
|
234
|
+
* - `firstTimestamp` – TODO: Decide how to use (nullable)
|
|
235
|
+
* - `lastTimestamp` – TODO: Decide how to use (nullable)
|
|
236
|
+
*/
|
|
237
|
+
sql `
|
|
238
|
+
create table evolu_usage (
|
|
239
|
+
"ownerId" blob primary key,
|
|
240
|
+
"storedBytes" integer not null
|
|
241
|
+
-- TODO: Decide how to use receivedBytes, sentBytes, firstTimestamp, lastTimestamp
|
|
242
|
+
-- "receivedBytes" integer not null,
|
|
243
|
+
-- "sentBytes" integer not null,
|
|
244
|
+
-- "firstTimestamp" blob,
|
|
245
|
+
-- "lastTimestamp" blob
|
|
246
|
+
)
|
|
247
|
+
strict;
|
|
194
248
|
`,
|
|
195
249
|
]) {
|
|
196
250
|
const result = deps.sqlite.exec(query);
|
|
@@ -208,7 +262,7 @@ const createTables = (deps) => {
|
|
|
208
262
|
// they will prepend. They are always sorted in ascending order by the
|
|
209
263
|
// Protocol.
|
|
210
264
|
const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
|
|
211
|
-
const [h1, h2] = fingerprintToSqliteFingerprint(
|
|
265
|
+
const [h1, h2] = fingerprintToSqliteFingerprint(timestampBytesToFingerprint(timestamp));
|
|
212
266
|
let queries = [];
|
|
213
267
|
switch (strategy) {
|
|
214
268
|
case "append":
|
|
@@ -717,6 +771,10 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
|
|
|
717
771
|
}
|
|
718
772
|
return ok();
|
|
719
773
|
};
|
|
774
|
+
export const timestampBytesToFingerprint = (timestamp) => {
|
|
775
|
+
const hash = sha256(timestamp).slice(0, fingerprintSize);
|
|
776
|
+
return hash;
|
|
777
|
+
};
|
|
720
778
|
/**
|
|
721
779
|
* Generates a random skiplist level in the range [1, skiplistMaxLevel].
|
|
722
780
|
* Probabilistic approach avoids the need for explicit tree balancing.
|
|
@@ -727,7 +785,7 @@ const randomSkiplistLevel = (deps) => {
|
|
|
727
785
|
level < skiplistMaxLevel) {
|
|
728
786
|
level += 1;
|
|
729
787
|
}
|
|
730
|
-
return level;
|
|
788
|
+
return PositiveInt.orThrow(level);
|
|
731
789
|
};
|
|
732
790
|
/**
|
|
733
791
|
* Probability used for generating Skiplist levels.
|
|
@@ -830,7 +888,7 @@ const findLowerBound = (deps) => (ownerId, begin, end, upperBound) => {
|
|
|
830
888
|
if (!count.ok)
|
|
831
889
|
return count;
|
|
832
890
|
// `decrement` converts a count to an index.
|
|
833
|
-
return ok(decrement(count.value));
|
|
891
|
+
return ok(NonNegativeInt.orThrow(decrement(count.value)));
|
|
834
892
|
};
|
|
835
893
|
const getTimestampCount = (deps) => (ownerId, timestamp) => {
|
|
836
894
|
const result = deps.sqlite.exec(sql.prepared `
|