@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
package/src/Evolu/Storage.ts
CHANGED
|
@@ -1,10 +1,279 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2
|
+
import { NonEmptyReadonlyArray } from "../Array.js";
|
|
3
|
+
import { assert } from "../Assert.js";
|
|
4
|
+
import { Brand } from "../Brand.js";
|
|
5
|
+
import { concatBytes } from "../Buffer.js";
|
|
6
|
+
import { decrement } from "../Number.js";
|
|
7
|
+
import { RandomDep } from "../Random.js";
|
|
8
|
+
import { ok, Result } from "../Result.js";
|
|
9
|
+
import { sql, SqliteDep, SqliteError, SqliteValue } from "../Sqlite.js";
|
|
10
|
+
import { MaybeAsync } from "../Task.js";
|
|
11
|
+
import {
|
|
12
|
+
Id,
|
|
13
|
+
Int64String,
|
|
14
|
+
NonNegativeInt,
|
|
15
|
+
object,
|
|
16
|
+
PositiveInt,
|
|
17
|
+
record,
|
|
18
|
+
String,
|
|
19
|
+
} from "../Type.js";
|
|
20
|
+
import {
|
|
21
|
+
BaseOwnerError,
|
|
22
|
+
Owner,
|
|
23
|
+
OwnerId,
|
|
24
|
+
OwnerIdBytes,
|
|
25
|
+
ownerIdBytesToOwnerId,
|
|
26
|
+
OwnerWriteKey,
|
|
27
|
+
} from "./Owner.js";
|
|
28
|
+
import { orderTimestampBytes, Timestamp, TimestampBytes } from "./Timestamp.js";
|
|
29
|
+
|
|
30
|
+
export interface StorageConfig {
|
|
31
|
+
/**
|
|
32
|
+
* Callback called before an attempt to write, to check if an {@link OwnerId}
|
|
33
|
+
* has sufficient quota for the write.
|
|
34
|
+
*
|
|
35
|
+
* The callback receives the {@link OwnerId} and the number of bytes required
|
|
36
|
+
* for the write, and returns a {@link MaybeAsync} boolean: `true` to allow the
|
|
37
|
+
* write, or `false` to deny it due to quota limits.
|
|
38
|
+
*
|
|
39
|
+
* The callback can be synchronous (for SQLite or in-memory checks) or
|
|
40
|
+
* asynchronous (for calling remote APIs).
|
|
41
|
+
*
|
|
42
|
+
* The callback returns a boolean rather than an error type because error
|
|
43
|
+
* handling and logging are the responsibility of the callback
|
|
44
|
+
* implementation.
|
|
45
|
+
*
|
|
46
|
+
* ### Example
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* // Client
|
|
50
|
+
* // evolu.subscribeError
|
|
51
|
+
*
|
|
52
|
+
* // Relay
|
|
53
|
+
* isOwnerWithinQuota: (ownerId, requiredBytes) => {
|
|
54
|
+
* console.log(ownerId, requiredBytes);
|
|
55
|
+
* // Check error via evolu.subscribeError
|
|
56
|
+
* return true;
|
|
57
|
+
* };
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
readonly isOwnerWithinQuota: (
|
|
61
|
+
ownerId: OwnerId,
|
|
62
|
+
requiredBytes: PositiveInt,
|
|
63
|
+
) => MaybeAsync<boolean>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Evolu Storage
|
|
68
|
+
*
|
|
69
|
+
* Evolu protocol using Storage is agnostic to storage implementation
|
|
70
|
+
* details—any storage can be plugged in, as long as it implements this
|
|
71
|
+
* interface. Implementations must handle their own errors; return values only
|
|
72
|
+
* indicate overall success or failure.
|
|
73
|
+
*
|
|
74
|
+
* The Storage API is synchronous because SQLite's synchronous API is the
|
|
75
|
+
* fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
|
|
76
|
+
* SQLite's C API directly with no context switching between the event loop and
|
|
77
|
+
* native code, and no promise microtasks or await overhead.
|
|
78
|
+
*
|
|
79
|
+
* The only exception is {@link Storage#writeMessages}, which is async to allow
|
|
80
|
+
* for async validation logic before writing to storage. The write operation
|
|
81
|
+
* itself remains synchronous.
|
|
82
|
+
*/
|
|
83
|
+
export interface Storage {
|
|
84
|
+
readonly getSize: (ownerId: OwnerIdBytes) => NonNegativeInt | null;
|
|
85
|
+
|
|
86
|
+
readonly fingerprint: (
|
|
87
|
+
ownerId: OwnerIdBytes,
|
|
88
|
+
begin: NonNegativeInt,
|
|
89
|
+
end: NonNegativeInt,
|
|
90
|
+
) => Fingerprint | null;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Computes fingerprints with their upper bounds in one call.
|
|
94
|
+
*
|
|
95
|
+
* This function can be replaced with many fingerprint/findLowerBound calls,
|
|
96
|
+
* but implementations can leverage it for batching and more efficient
|
|
97
|
+
* fingerprint computation.
|
|
98
|
+
*/
|
|
99
|
+
readonly fingerprintRanges: (
|
|
100
|
+
ownerId: OwnerIdBytes,
|
|
101
|
+
buckets: ReadonlyArray<NonNegativeInt>,
|
|
102
|
+
upperBound?: RangeUpperBound,
|
|
103
|
+
) => ReadonlyArray<FingerprintRange> | null;
|
|
104
|
+
|
|
105
|
+
readonly findLowerBound: (
|
|
106
|
+
ownerId: OwnerIdBytes,
|
|
107
|
+
begin: NonNegativeInt,
|
|
108
|
+
end: NonNegativeInt,
|
|
109
|
+
upperBound: RangeUpperBound,
|
|
110
|
+
) => NonNegativeInt | null;
|
|
111
|
+
|
|
112
|
+
readonly iterate: (
|
|
113
|
+
ownerId: OwnerIdBytes,
|
|
114
|
+
begin: NonNegativeInt,
|
|
115
|
+
end: NonNegativeInt,
|
|
116
|
+
callback: (timestamp: TimestampBytes, index: NonNegativeInt) => boolean,
|
|
117
|
+
) => void;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validates the {@link OwnerWriteKey} for the given {@link Owner}.
|
|
121
|
+
*
|
|
122
|
+
* Returns `true` if the write key is valid, `false` otherwise.
|
|
123
|
+
*/
|
|
124
|
+
readonly validateWriteKey: (
|
|
125
|
+
ownerId: OwnerIdBytes,
|
|
126
|
+
writeKey: OwnerWriteKey,
|
|
127
|
+
) => boolean;
|
|
128
|
+
|
|
129
|
+
/** Sets the {@link OwnerWriteKey} for the given {@link Owner}. */
|
|
130
|
+
readonly setWriteKey: (
|
|
131
|
+
ownerId: OwnerIdBytes,
|
|
132
|
+
writeKey: OwnerWriteKey,
|
|
133
|
+
) => boolean;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Write encrypted {@link CrdtMessage}s to storage.
|
|
137
|
+
*
|
|
138
|
+
* Must use a mutex per ownerId to ensure sequential processing and proper
|
|
139
|
+
* protocol logic handling during sync operations.
|
|
140
|
+
*
|
|
141
|
+
* TODO: Use MaybeAsync
|
|
142
|
+
*/
|
|
143
|
+
readonly writeMessages: (
|
|
144
|
+
ownerIdBytes: OwnerIdBytes,
|
|
145
|
+
messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>,
|
|
146
|
+
) => MaybeAsync<Result<void, StorageWriteError | StorageQuotaError>>;
|
|
147
|
+
|
|
148
|
+
/** Read encrypted {@link DbChange}s from storage. */
|
|
149
|
+
readonly readDbChange: (
|
|
150
|
+
ownerId: OwnerIdBytes,
|
|
151
|
+
timestamp: TimestampBytes,
|
|
152
|
+
) => EncryptedDbChange | null;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Delete all data for the given {@link Owner}.
|
|
156
|
+
*
|
|
157
|
+
* Returns `true` on success, `false` on failure.
|
|
158
|
+
*/
|
|
159
|
+
readonly deleteOwner: (ownerId: OwnerIdBytes) => boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface StorageDep {
|
|
163
|
+
readonly storage: Storage;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Error indicating a serious write failure. */
|
|
167
|
+
export interface StorageWriteError extends BaseOwnerError {
|
|
168
|
+
readonly type: "StorageWriteError";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Error when storage or billing quota is exceeded. */
|
|
172
|
+
export interface StorageQuotaError extends BaseOwnerError {
|
|
173
|
+
readonly type: "StorageQuotaError";
|
|
174
|
+
}
|
|
175
|
+
|
|
1
176
|
/**
|
|
2
|
-
*
|
|
177
|
+
* A cryptographic hash used for efficiently comparing collections of
|
|
178
|
+
* {@link TimestampBytes}s.
|
|
179
|
+
*
|
|
180
|
+
* It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
|
|
181
|
+
* one or more timestamps.
|
|
182
|
+
*/
|
|
183
|
+
export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
|
|
184
|
+
|
|
185
|
+
export const fingerprintSize = NonNegativeInt.orThrow(12);
|
|
186
|
+
|
|
187
|
+
/** A fingerprint of an empty range. */
|
|
188
|
+
export const zeroFingerprint = new Uint8Array(fingerprintSize) as Fingerprint;
|
|
189
|
+
|
|
190
|
+
export interface BaseRange {
|
|
191
|
+
readonly upperBound: RangeUpperBound;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Union type for Range's upperBound: either a {@link TimestampBytes} or
|
|
196
|
+
* {@link InfiniteUpperBound}.
|
|
197
|
+
*/
|
|
198
|
+
export type RangeUpperBound = TimestampBytes | InfiniteUpperBound;
|
|
199
|
+
|
|
200
|
+
export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
|
|
201
|
+
export type InfiniteUpperBound = typeof InfiniteUpperBound;
|
|
202
|
+
|
|
203
|
+
export const RangeType = {
|
|
204
|
+
Fingerprint: 1,
|
|
205
|
+
Skip: 0,
|
|
206
|
+
Timestamps: 2,
|
|
207
|
+
} as const;
|
|
208
|
+
|
|
209
|
+
export type RangeType = (typeof RangeType)[keyof typeof RangeType];
|
|
210
|
+
|
|
211
|
+
export interface SkipRange extends BaseRange {
|
|
212
|
+
readonly type: typeof RangeType.Skip;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface FingerprintRange extends BaseRange {
|
|
216
|
+
readonly type: typeof RangeType.Fingerprint;
|
|
217
|
+
readonly fingerprint: Fingerprint;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface TimestampsRange extends BaseRange {
|
|
221
|
+
readonly type: typeof RangeType.Timestamps;
|
|
222
|
+
readonly timestamps: ReadonlyArray<TimestampBytes>;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type Range = SkipRange | FingerprintRange | TimestampsRange;
|
|
226
|
+
|
|
227
|
+
/** An encrypted {@link CrdtMessage}. */
|
|
228
|
+
export interface EncryptedCrdtMessage {
|
|
229
|
+
readonly timestamp: Timestamp;
|
|
230
|
+
readonly change: EncryptedDbChange;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Encrypted DbChange */
|
|
234
|
+
export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A CRDT message combining a unique {@link Timestamp} with a {@link DbChange}.
|
|
238
|
+
*
|
|
239
|
+
* Used in Evolu's sync protocol to replicate data changes across devices. Evolu
|
|
240
|
+
* operates as a durable queue, providing exactly-once delivery guarantees for
|
|
241
|
+
* reliable synchronization across application restarts and network failures.
|
|
242
|
+
*/
|
|
243
|
+
export interface CrdtMessage {
|
|
244
|
+
readonly timestamp: Timestamp;
|
|
245
|
+
readonly change: DbChange;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A DbChange is a change to a table row. Together with a unique
|
|
250
|
+
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
251
|
+
*/
|
|
252
|
+
export const DbChange = object({
|
|
253
|
+
table: String,
|
|
254
|
+
id: Id,
|
|
255
|
+
values: record(String, SqliteValue),
|
|
256
|
+
});
|
|
257
|
+
export type DbChange = typeof DbChange.Type;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Common interface for both client and relay SQLite storages.
|
|
3
261
|
*
|
|
4
262
|
* Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
|
|
5
263
|
* implemented in SQL, so it doesn't have to make roundtrips to the DB.
|
|
6
264
|
*
|
|
7
|
-
*
|
|
265
|
+
* While the SQL implementation may look sophisticated, it's conceptually simple
|
|
266
|
+
* and LLMs can explain how it works. The Skiplist data structure is well
|
|
267
|
+
* explained in [this Stack Overflow
|
|
268
|
+
* answer](https://stackoverflow.com/questions/61944198/what-is-a-zip-tree-and-how-does-it-work).
|
|
269
|
+
* The logic resembles [Negentropy's C++
|
|
270
|
+
* storage](https://github.com/hoytech/negentropy), except we use a Skiplist to
|
|
271
|
+
* leverage SQLite indexes, which makes the code simpler.
|
|
272
|
+
*
|
|
273
|
+
* Note: A paid review by the SQLite team is planned, as they use the same
|
|
274
|
+
* algorithm for their rsync tool.
|
|
275
|
+
*
|
|
276
|
+
* The ideal storage for a Relay should use an architecture like
|
|
8
277
|
* [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
|
|
9
278
|
* to ensure that insertion order doesn't matter (local-first apps can often
|
|
10
279
|
* write in the past.)
|
|
@@ -19,116 +288,66 @@
|
|
|
19
288
|
* each other, if necessary. One relay should handle hundreds of thousands of
|
|
20
289
|
* users, and when it goes down, nothing happens, because it will be
|
|
21
290
|
* synchronized later.
|
|
22
|
-
*
|
|
23
|
-
* @module
|
|
24
291
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
RangeUpperBound,
|
|
43
|
-
Storage,
|
|
44
|
-
zeroFingerprint,
|
|
45
|
-
} from "./Protocol.js";
|
|
46
|
-
import { BinaryTimestamp, orderBinaryTimestamp } from "./Timestamp.js";
|
|
47
|
-
|
|
48
|
-
/** Common interface for both client and relay SQLite storages. */
|
|
49
|
-
export interface SqliteStorageBase {
|
|
292
|
+
export interface BaseSqliteStorage
|
|
293
|
+
extends Pick<
|
|
294
|
+
Storage,
|
|
295
|
+
| "getSize"
|
|
296
|
+
| "fingerprint"
|
|
297
|
+
| "fingerprintRanges"
|
|
298
|
+
| "findLowerBound"
|
|
299
|
+
| "iterate"
|
|
300
|
+
| "deleteOwner"
|
|
301
|
+
> {
|
|
302
|
+
/**
|
|
303
|
+
* Inserts a timestamp for an owner into the skiplist-based storage.
|
|
304
|
+
*
|
|
305
|
+
* Must be idempotent - inserting the same timestamp multiple times has no
|
|
306
|
+
* effect after the first insertion. This is crucial for sync reliability as
|
|
307
|
+
* messages may be received and processed multiple times.
|
|
308
|
+
*/
|
|
50
309
|
readonly insertTimestamp: (
|
|
51
|
-
ownerId:
|
|
52
|
-
timestamp:
|
|
310
|
+
ownerId: OwnerIdBytes,
|
|
311
|
+
timestamp: TimestampBytes,
|
|
53
312
|
) => Result<void, SqliteError>;
|
|
54
313
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
readonly
|
|
314
|
+
/**
|
|
315
|
+
* Efficiently checks which timestamps already exist in the database using a
|
|
316
|
+
* single CTE query instead of N individual queries.
|
|
317
|
+
*/
|
|
318
|
+
readonly getExistingTimestamps: (
|
|
319
|
+
ownerIdBytes: OwnerIdBytes,
|
|
320
|
+
timestampsBytes: NonEmptyReadonlyArray<TimestampBytes>,
|
|
321
|
+
) => Result<ReadonlyArray<TimestampBytes>, SqliteError>;
|
|
60
322
|
}
|
|
61
323
|
|
|
62
|
-
export interface
|
|
63
|
-
readonly storage:
|
|
324
|
+
export interface BaseSqliteStorageDep {
|
|
325
|
+
readonly storage: BaseSqliteStorage;
|
|
64
326
|
}
|
|
65
327
|
|
|
66
|
-
export type SqliteStorageDeps =
|
|
328
|
+
export type SqliteStorageDeps = RandomDep & SqliteDep;
|
|
67
329
|
|
|
68
|
-
export interface
|
|
330
|
+
export interface CreateBaseSqliteStorageConfig extends StorageConfig {
|
|
69
331
|
onStorageError: (error: SqliteError) => void;
|
|
70
332
|
}
|
|
71
333
|
|
|
72
|
-
export const
|
|
334
|
+
export const createBaseSqliteStorage =
|
|
73
335
|
(deps: SqliteStorageDeps) =>
|
|
74
|
-
(
|
|
75
|
-
|
|
76
|
-
): Result<SqliteStorageBase, SqliteError> => {
|
|
77
|
-
const createTablesResult = createTables(deps);
|
|
78
|
-
if (!createTablesResult.ok) return createTablesResult;
|
|
79
|
-
|
|
336
|
+
(config: CreateBaseSqliteStorageConfig): BaseSqliteStorage => {
|
|
337
|
+
// TODO: Use evolu_usage table.
|
|
80
338
|
const ownerStats = new Map<
|
|
81
339
|
OwnerId,
|
|
82
340
|
{
|
|
83
|
-
minT:
|
|
84
|
-
maxT:
|
|
341
|
+
minT: TimestampBytes;
|
|
342
|
+
maxT: TimestampBytes;
|
|
85
343
|
}
|
|
86
344
|
>();
|
|
87
345
|
|
|
88
|
-
return
|
|
89
|
-
insertTimestamp: (ownerId: BinaryOwnerId, timestamp: BinaryTimestamp) => {
|
|
90
|
-
const ownerIdString = binaryOwnerIdToOwnerId(ownerId);
|
|
91
|
-
const level = randomSkiplistLevel(deps);
|
|
92
|
-
|
|
93
|
-
let stats = ownerStats.get(ownerIdString);
|
|
94
|
-
|
|
95
|
-
if (!stats) {
|
|
96
|
-
const result = deps.sqlite.exec<{
|
|
97
|
-
maxT: BinaryTimestamp | null;
|
|
98
|
-
minT: BinaryTimestamp | null;
|
|
99
|
-
}>(sql.prepared`
|
|
100
|
-
select min(t) as minT, max(t) as maxT
|
|
101
|
-
from evolu_timestamp
|
|
102
|
-
where ownerId = ${ownerId};
|
|
103
|
-
`);
|
|
104
|
-
if (!result.ok) return result;
|
|
105
|
-
|
|
106
|
-
stats = {
|
|
107
|
-
minT: result.value.rows[0].minT ?? timestamp,
|
|
108
|
-
maxT: result.value.rows[0].maxT ?? timestamp,
|
|
109
|
-
};
|
|
110
|
-
ownerStats.set(ownerIdString, stats);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let strategy: InsertTimestampStrategy;
|
|
114
|
-
|
|
115
|
-
if (orderBinaryTimestamp(timestamp, stats.maxT) === 1) {
|
|
116
|
-
strategy = "append";
|
|
117
|
-
stats.maxT = timestamp;
|
|
118
|
-
} else if (orderBinaryTimestamp(timestamp, stats.minT) === -1) {
|
|
119
|
-
strategy = "prepend";
|
|
120
|
-
stats.minT = timestamp;
|
|
121
|
-
} else {
|
|
122
|
-
strategy = "insert";
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
|
|
126
|
-
},
|
|
127
|
-
|
|
346
|
+
return {
|
|
128
347
|
getSize: (ownerId) => {
|
|
129
348
|
const size = getSize(deps)(ownerId);
|
|
130
349
|
if (!size.ok) {
|
|
131
|
-
|
|
350
|
+
config.onStorageError(size.error);
|
|
132
351
|
return null;
|
|
133
352
|
}
|
|
134
353
|
return size.value;
|
|
@@ -138,7 +357,7 @@ export const createSqliteStorageBase =
|
|
|
138
357
|
assertBeginEnd(begin, end);
|
|
139
358
|
const result = fingerprint(deps)(ownerId, begin, end);
|
|
140
359
|
if (!result.ok) {
|
|
141
|
-
|
|
360
|
+
config.onStorageError(result.error);
|
|
142
361
|
return null;
|
|
143
362
|
}
|
|
144
363
|
return result.value;
|
|
@@ -147,7 +366,7 @@ export const createSqliteStorageBase =
|
|
|
147
366
|
fingerprintRanges: (ownerId, buckets, upperBound) => {
|
|
148
367
|
const ranges = fingerprintRanges(deps)(ownerId, buckets, upperBound);
|
|
149
368
|
if (!ranges.ok) {
|
|
150
|
-
|
|
369
|
+
config.onStorageError(ranges.error);
|
|
151
370
|
return null;
|
|
152
371
|
}
|
|
153
372
|
return ranges.value;
|
|
@@ -161,7 +380,7 @@ export const createSqliteStorageBase =
|
|
|
161
380
|
upperBound,
|
|
162
381
|
);
|
|
163
382
|
if (!lowerBound.ok) {
|
|
164
|
-
|
|
383
|
+
config.onStorageError(lowerBound.error);
|
|
165
384
|
return null;
|
|
166
385
|
}
|
|
167
386
|
return lowerBound.value;
|
|
@@ -175,7 +394,7 @@ export const createSqliteStorageBase =
|
|
|
175
394
|
// This is much faster than SQL limit with offset.
|
|
176
395
|
const first = getTimestampByIndex(deps)(ownerId, begin);
|
|
177
396
|
if (!first.ok) {
|
|
178
|
-
|
|
397
|
+
config.onStorageError(first.error);
|
|
179
398
|
return;
|
|
180
399
|
}
|
|
181
400
|
|
|
@@ -192,7 +411,7 @@ export const createSqliteStorageBase =
|
|
|
192
411
|
* implementing chunking, be sure to run performance tests (including
|
|
193
412
|
* fetching one by one).
|
|
194
413
|
*/
|
|
195
|
-
const result = deps.sqlite.exec<{ t:
|
|
414
|
+
const result = deps.sqlite.exec<{ t: TimestampBytes }>(sql`
|
|
196
415
|
select t
|
|
197
416
|
from evolu_timestamp
|
|
198
417
|
where ownerId = ${ownerId} and t > ${first.value}
|
|
@@ -200,23 +419,105 @@ export const createSqliteStorageBase =
|
|
|
200
419
|
limit ${length - 1};
|
|
201
420
|
`);
|
|
202
421
|
if (!result.ok) {
|
|
203
|
-
|
|
422
|
+
config.onStorageError(result.error);
|
|
204
423
|
return;
|
|
205
424
|
}
|
|
206
425
|
|
|
207
426
|
for (let i = 0; i < result.value.rows.length; i++) {
|
|
208
|
-
const index = (begin + 1 + i)
|
|
427
|
+
const index = NonNegativeInt.orThrow(begin + 1 + i);
|
|
209
428
|
if (!callback(result.value.rows[i].t, index)) return;
|
|
210
429
|
}
|
|
211
430
|
},
|
|
212
|
-
|
|
431
|
+
|
|
432
|
+
deleteOwner: (ownerId) => {
|
|
433
|
+
const result = deps.sqlite.exec(sql`
|
|
434
|
+
delete from evolu_timestamp where ownerId = ${ownerId};
|
|
435
|
+
`);
|
|
436
|
+
if (!result.ok) {
|
|
437
|
+
config.onStorageError(result.error);
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
return true;
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
insertTimestamp: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => {
|
|
444
|
+
const ownerIdString = ownerIdBytesToOwnerId(ownerId);
|
|
445
|
+
const level = randomSkiplistLevel(deps);
|
|
446
|
+
|
|
447
|
+
let stats = ownerStats.get(ownerIdString);
|
|
448
|
+
|
|
449
|
+
if (!stats) {
|
|
450
|
+
const result = deps.sqlite.exec<{
|
|
451
|
+
maxT: TimestampBytes | null;
|
|
452
|
+
minT: TimestampBytes | null;
|
|
453
|
+
}>(sql.prepared`
|
|
454
|
+
select min(t) as minT, max(t) as maxT
|
|
455
|
+
from evolu_timestamp
|
|
456
|
+
where ownerId = ${ownerId};
|
|
457
|
+
`);
|
|
458
|
+
if (!result.ok) return result;
|
|
459
|
+
|
|
460
|
+
stats = {
|
|
461
|
+
minT: result.value.rows[0].minT ?? timestamp,
|
|
462
|
+
maxT: result.value.rows[0].maxT ?? timestamp,
|
|
463
|
+
};
|
|
464
|
+
ownerStats.set(ownerIdString, stats);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let strategy: InsertTimestampStrategy;
|
|
468
|
+
|
|
469
|
+
if (orderTimestampBytes(timestamp, stats.maxT) === 1) {
|
|
470
|
+
strategy = "append";
|
|
471
|
+
stats.maxT = timestamp;
|
|
472
|
+
} else if (orderTimestampBytes(timestamp, stats.minT) === -1) {
|
|
473
|
+
strategy = "prepend";
|
|
474
|
+
stats.minT = timestamp;
|
|
475
|
+
} else {
|
|
476
|
+
strategy = "insert";
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
getExistingTimestamps: (ownerIdBytes, timestampsBytes) => {
|
|
483
|
+
const concatenatedTimestamps = concatBytes(...timestampsBytes);
|
|
484
|
+
|
|
485
|
+
const result = deps.sqlite.exec<{
|
|
486
|
+
timestampBytes: TimestampBytes;
|
|
487
|
+
}>(sql`
|
|
488
|
+
with recursive
|
|
489
|
+
split_timestamps(timestampBytes, pos) as (
|
|
490
|
+
select
|
|
491
|
+
substr(${concatenatedTimestamps}, 1, 16),
|
|
492
|
+
17 as pos
|
|
493
|
+
union all
|
|
494
|
+
select
|
|
495
|
+
substr(${concatenatedTimestamps}, pos, 16),
|
|
496
|
+
pos + 16
|
|
497
|
+
from split_timestamps
|
|
498
|
+
where pos <= length(${concatenatedTimestamps})
|
|
499
|
+
)
|
|
500
|
+
select s.timestampBytes
|
|
501
|
+
from
|
|
502
|
+
split_timestamps s
|
|
503
|
+
join evolu_timestamp t
|
|
504
|
+
on t.ownerId = ${ownerIdBytes} and s.timestampBytes = t.t;
|
|
505
|
+
`);
|
|
506
|
+
|
|
507
|
+
if (!result.ok) return result;
|
|
508
|
+
|
|
509
|
+
return ok(result.value.rows.map((row) => row.timestampBytes));
|
|
510
|
+
},
|
|
511
|
+
};
|
|
213
512
|
};
|
|
214
513
|
|
|
215
514
|
const assertBeginEnd = (begin: NonNegativeInt, end: NonNegativeInt) => {
|
|
216
515
|
assert(begin <= end, "invalid begin or end");
|
|
217
516
|
};
|
|
218
517
|
|
|
219
|
-
const
|
|
518
|
+
export const createBaseSqliteStorageTables = (
|
|
519
|
+
deps: SqliteDep,
|
|
520
|
+
): Result<void, SqliteError> => {
|
|
220
521
|
for (const query of [
|
|
221
522
|
/**
|
|
222
523
|
* Creates the `evolu_timestamp` table for storing timestamps of multiple
|
|
@@ -229,7 +530,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
229
530
|
*
|
|
230
531
|
* Columns:
|
|
231
532
|
*
|
|
232
|
-
* - `t` –
|
|
533
|
+
* - `t` – TimestampBytes
|
|
233
534
|
* - `h1`/`h2` – 12-byte fingerprint split into two integers for fast XOR
|
|
234
535
|
* - `c` – incremental count
|
|
235
536
|
* - `l` – Skiplist level (1 to 32)
|
|
@@ -241,7 +542,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
241
542
|
* enough even without it.
|
|
242
543
|
*/
|
|
243
544
|
sql`
|
|
244
|
-
create table
|
|
545
|
+
create table evolu_timestamp (
|
|
245
546
|
"ownerId" blob not null,
|
|
246
547
|
"t" blob not null,
|
|
247
548
|
"h1" integer,
|
|
@@ -254,7 +555,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
254
555
|
`,
|
|
255
556
|
|
|
256
557
|
sql`
|
|
257
|
-
create index
|
|
558
|
+
create index evolu_timestamp_index on evolu_timestamp (
|
|
258
559
|
"ownerId",
|
|
259
560
|
"l",
|
|
260
561
|
"t",
|
|
@@ -263,6 +564,31 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
263
564
|
"c"
|
|
264
565
|
);
|
|
265
566
|
`,
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Creates the `evolu_usage` table for tracking data consumption per owner.
|
|
570
|
+
*
|
|
571
|
+
* Columns:
|
|
572
|
+
*
|
|
573
|
+
* - `ownerId` – OwnerIdBytes (primary key)
|
|
574
|
+
* - `storedBytes` – total bytes stored in database
|
|
575
|
+
* - `receivedBytes` – TODO: Decide how to use
|
|
576
|
+
* - `sentBytes` – TODO: Decide how to use
|
|
577
|
+
* - `firstTimestamp` – TODO: Decide how to use (nullable)
|
|
578
|
+
* - `lastTimestamp` – TODO: Decide how to use (nullable)
|
|
579
|
+
*/
|
|
580
|
+
sql`
|
|
581
|
+
create table evolu_usage (
|
|
582
|
+
"ownerId" blob primary key,
|
|
583
|
+
"storedBytes" integer not null
|
|
584
|
+
-- TODO: Decide how to use receivedBytes, sentBytes, firstTimestamp, lastTimestamp
|
|
585
|
+
-- "receivedBytes" integer not null,
|
|
586
|
+
-- "sentBytes" integer not null,
|
|
587
|
+
-- "firstTimestamp" blob,
|
|
588
|
+
-- "lastTimestamp" blob
|
|
589
|
+
)
|
|
590
|
+
strict;
|
|
591
|
+
`,
|
|
266
592
|
]) {
|
|
267
593
|
const result = deps.sqlite.exec(query);
|
|
268
594
|
if (!result.ok) return result;
|
|
@@ -283,13 +609,13 @@ type InsertTimestampStrategy = "append" | "prepend" | "insert";
|
|
|
283
609
|
const insertTimestamp =
|
|
284
610
|
(deps: SqliteDep) =>
|
|
285
611
|
(
|
|
286
|
-
ownerId:
|
|
287
|
-
timestamp:
|
|
612
|
+
ownerId: OwnerIdBytes,
|
|
613
|
+
timestamp: TimestampBytes,
|
|
288
614
|
level: PositiveInt,
|
|
289
615
|
strategy: InsertTimestampStrategy,
|
|
290
616
|
): Result<void, SqliteError> => {
|
|
291
617
|
const [h1, h2] = fingerprintToSqliteFingerprint(
|
|
292
|
-
|
|
618
|
+
timestampBytesToFingerprint(timestamp),
|
|
293
619
|
);
|
|
294
620
|
|
|
295
621
|
let queries: Array<ReturnType<typeof sql.prepared>> = [];
|
|
@@ -807,6 +1133,13 @@ const insertTimestamp =
|
|
|
807
1133
|
return ok();
|
|
808
1134
|
};
|
|
809
1135
|
|
|
1136
|
+
export const timestampBytesToFingerprint = (
|
|
1137
|
+
timestamp: TimestampBytes,
|
|
1138
|
+
): Fingerprint => {
|
|
1139
|
+
const hash = sha256(timestamp).slice(0, fingerprintSize);
|
|
1140
|
+
return hash as Fingerprint;
|
|
1141
|
+
};
|
|
1142
|
+
|
|
810
1143
|
/**
|
|
811
1144
|
* Generates a random skiplist level in the range [1, skiplistMaxLevel].
|
|
812
1145
|
* Probabilistic approach avoids the need for explicit tree balancing.
|
|
@@ -819,7 +1152,7 @@ const randomSkiplistLevel = (deps: RandomDep): PositiveInt => {
|
|
|
819
1152
|
) {
|
|
820
1153
|
level += 1;
|
|
821
1154
|
}
|
|
822
|
-
return level
|
|
1155
|
+
return PositiveInt.orThrow(level);
|
|
823
1156
|
};
|
|
824
1157
|
|
|
825
1158
|
/**
|
|
@@ -887,7 +1220,7 @@ const sqliteFingerprintToFingerprint = ([
|
|
|
887
1220
|
|
|
888
1221
|
const getSize =
|
|
889
1222
|
(deps: SqliteDep) =>
|
|
890
|
-
(ownerId:
|
|
1223
|
+
(ownerId: OwnerIdBytes): Result<NonNegativeInt, SqliteError> => {
|
|
891
1224
|
const result = deps.sqlite.exec<{ size: NonNegativeInt }>(sql.prepared`
|
|
892
1225
|
with
|
|
893
1226
|
ml(ml) as (
|
|
@@ -930,7 +1263,7 @@ const getSize =
|
|
|
930
1263
|
const findLowerBound =
|
|
931
1264
|
(deps: SqliteDep) =>
|
|
932
1265
|
(
|
|
933
|
-
ownerId:
|
|
1266
|
+
ownerId: OwnerIdBytes,
|
|
934
1267
|
begin: NonNegativeInt,
|
|
935
1268
|
end: NonNegativeInt,
|
|
936
1269
|
upperBound: RangeUpperBound,
|
|
@@ -942,7 +1275,7 @@ const findLowerBound =
|
|
|
942
1275
|
}
|
|
943
1276
|
|
|
944
1277
|
const result = deps.sqlite.exec<{
|
|
945
|
-
t:
|
|
1278
|
+
t: TimestampBytes;
|
|
946
1279
|
}>(sql.prepared`
|
|
947
1280
|
select t
|
|
948
1281
|
from evolu_timestamp
|
|
@@ -960,14 +1293,14 @@ const findLowerBound =
|
|
|
960
1293
|
if (!count.ok) return count;
|
|
961
1294
|
|
|
962
1295
|
// `decrement` converts a count to an index.
|
|
963
|
-
return ok(decrement(count.value)
|
|
1296
|
+
return ok(NonNegativeInt.orThrow(decrement(count.value)));
|
|
964
1297
|
};
|
|
965
1298
|
|
|
966
1299
|
const getTimestampCount =
|
|
967
1300
|
(deps: SqliteDep) =>
|
|
968
1301
|
(
|
|
969
|
-
ownerId:
|
|
970
|
-
timestamp:
|
|
1302
|
+
ownerId: OwnerIdBytes,
|
|
1303
|
+
timestamp: TimestampBytes,
|
|
971
1304
|
): Result<PositiveInt, SqliteError> => {
|
|
972
1305
|
const result = deps.sqlite.exec<{
|
|
973
1306
|
count: PositiveInt;
|
|
@@ -1024,7 +1357,7 @@ const getTimestampCount =
|
|
|
1024
1357
|
const fingerprint =
|
|
1025
1358
|
(deps: SqliteDep) =>
|
|
1026
1359
|
(
|
|
1027
|
-
ownerId:
|
|
1360
|
+
ownerId: OwnerIdBytes,
|
|
1028
1361
|
begin: NonNegativeInt,
|
|
1029
1362
|
end: NonNegativeInt,
|
|
1030
1363
|
): Result<Fingerprint, SqliteError> => {
|
|
@@ -1056,14 +1389,14 @@ const fingerprint =
|
|
|
1056
1389
|
const fingerprintRanges =
|
|
1057
1390
|
(deps: SqliteDep) =>
|
|
1058
1391
|
(
|
|
1059
|
-
ownerId:
|
|
1392
|
+
ownerId: OwnerIdBytes,
|
|
1060
1393
|
buckets: ReadonlyArray<NonNegativeInt>,
|
|
1061
1394
|
upperBound: RangeUpperBound = InfiniteUpperBound,
|
|
1062
1395
|
): Result<ReadonlyArray<FingerprintRange>, SqliteError> => {
|
|
1063
1396
|
const bucketsJson = JSON.stringify(buckets);
|
|
1064
1397
|
|
|
1065
1398
|
const result = deps.sqlite.exec<{
|
|
1066
|
-
b:
|
|
1399
|
+
b: TimestampBytes | null;
|
|
1067
1400
|
h1: Int64String;
|
|
1068
1401
|
h2: Int64String;
|
|
1069
1402
|
}>(sql.prepared`
|
|
@@ -1199,11 +1532,11 @@ const x = (a: string, b: string) => sql.raw(`(${a} | ${b}) - (${a} & ${b})`);
|
|
|
1199
1532
|
export const getTimestampByIndex =
|
|
1200
1533
|
(deps: SqliteDep) =>
|
|
1201
1534
|
(
|
|
1202
|
-
ownerId:
|
|
1535
|
+
ownerId: OwnerIdBytes,
|
|
1203
1536
|
index: NonNegativeInt,
|
|
1204
|
-
): Result<
|
|
1537
|
+
): Result<TimestampBytes, SqliteError> => {
|
|
1205
1538
|
const result = deps.sqlite.exec<{
|
|
1206
|
-
readonly pt:
|
|
1539
|
+
readonly pt: TimestampBytes;
|
|
1207
1540
|
}>(sql.prepared`
|
|
1208
1541
|
with
|
|
1209
1542
|
fi(b, cl, ic, pt, mt, nt, nc) as (
|