@evolu/common 6.0.1-preview.19 → 6.0.1-preview.20
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/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +1 -1
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- 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 +48 -37
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +27 -50
- package/dist/src/Evolu/Db.d.ts +138 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +248 -645
- 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 +79 -116
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +275 -132
- 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 +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +129 -83
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +80 -89
- 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 +114 -191
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +409 -416
- 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/Relay.d.ts +1 -2
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +11 -9
- package/dist/src/Evolu/Schema.d.ts +88 -27
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +141 -24
- package/dist/src/Evolu/Storage.d.ts +158 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +32 -32
- package/dist/src/Evolu/Sync.d.ts +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +29 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +20 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +144 -22
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +5 -2
- package/dist/src/Sqlite.d.ts +20 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +431 -341
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +458 -466
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -13
- 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 +2 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/package.json +7 -7
- package/src/Assert.ts +2 -4
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +78 -91
- package/src/Evolu/Db.ts +455 -947
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +545 -307
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +191 -131
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +536 -653
- package/src/Evolu/Public.ts +7 -9
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Relay.ts +17 -12
- package/src/Evolu/Schema.ts +271 -66
- package/src/Evolu/Storage.ts +263 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +30 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +149 -23
- package/src/Sqlite.ts +59 -24
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +657 -695
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +2 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- package/dist/src/Evolu/Config.d.ts +0 -82
- 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/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -97
- 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,230 @@
|
|
|
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 { decrement } from "../Number.js";
|
|
6
|
+
import { RandomDep } from "../Random.js";
|
|
7
|
+
import { ok, Result } from "../Result.js";
|
|
8
|
+
import { sql, SqliteDep, SqliteError, SqliteValue } from "../Sqlite.js";
|
|
9
|
+
import {
|
|
10
|
+
Id,
|
|
11
|
+
Int64String,
|
|
12
|
+
NonNegativeInt,
|
|
13
|
+
object,
|
|
14
|
+
PositiveInt,
|
|
15
|
+
record,
|
|
16
|
+
String,
|
|
17
|
+
} from "../Type.js";
|
|
18
|
+
import {
|
|
19
|
+
Owner,
|
|
20
|
+
OwnerId,
|
|
21
|
+
OwnerIdBytes,
|
|
22
|
+
ownerIdBytesToOwnerId,
|
|
23
|
+
OwnerWriteKey,
|
|
24
|
+
} from "./Owner.js";
|
|
25
|
+
import { orderTimestampBytes, Timestamp, TimestampBytes } from "./Timestamp.js";
|
|
26
|
+
|
|
1
27
|
/**
|
|
2
|
-
* Evolu Storage
|
|
28
|
+
* Evolu Storage
|
|
29
|
+
*
|
|
30
|
+
* The protocol using Storage is agnostic to storage implementation details—any
|
|
31
|
+
* storage can be plugged in, as long as it implements this interface.
|
|
32
|
+
* Implementations must handle their own errors; return values only indicate
|
|
33
|
+
* overall success or failure.
|
|
34
|
+
*
|
|
35
|
+
* The Storage API is synchronous because SQLite's synchronous API is the
|
|
36
|
+
* fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
|
|
37
|
+
* SQLite's C API directly with no context switching between the event loop and
|
|
38
|
+
* native code, and no promise microtasks or await overhead.
|
|
39
|
+
*
|
|
40
|
+
* The only exception is {@link Storage#writeMessages}, which is async to allow
|
|
41
|
+
* for async validation logic before writing to storage. The write operation
|
|
42
|
+
* itself remains synchronous.
|
|
43
|
+
*/
|
|
44
|
+
export interface Storage {
|
|
45
|
+
readonly getSize: (ownerId: OwnerIdBytes) => NonNegativeInt | null;
|
|
46
|
+
|
|
47
|
+
readonly fingerprint: (
|
|
48
|
+
ownerId: OwnerIdBytes,
|
|
49
|
+
begin: NonNegativeInt,
|
|
50
|
+
end: NonNegativeInt,
|
|
51
|
+
) => Fingerprint | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Computes fingerprints with their upper bounds in one call.
|
|
55
|
+
*
|
|
56
|
+
* This function can be replaced with many fingerprint/findLowerBound calls,
|
|
57
|
+
* but implementations can leverage it for batching and more efficient
|
|
58
|
+
* fingerprint computation.
|
|
59
|
+
*/
|
|
60
|
+
readonly fingerprintRanges: (
|
|
61
|
+
ownerId: OwnerIdBytes,
|
|
62
|
+
buckets: ReadonlyArray<NonNegativeInt>,
|
|
63
|
+
upperBound?: RangeUpperBound,
|
|
64
|
+
) => ReadonlyArray<FingerprintRange> | null;
|
|
65
|
+
|
|
66
|
+
readonly findLowerBound: (
|
|
67
|
+
ownerId: OwnerIdBytes,
|
|
68
|
+
begin: NonNegativeInt,
|
|
69
|
+
end: NonNegativeInt,
|
|
70
|
+
upperBound: RangeUpperBound,
|
|
71
|
+
) => NonNegativeInt | null;
|
|
72
|
+
|
|
73
|
+
readonly iterate: (
|
|
74
|
+
ownerId: OwnerIdBytes,
|
|
75
|
+
begin: NonNegativeInt,
|
|
76
|
+
end: NonNegativeInt,
|
|
77
|
+
callback: (timestamp: TimestampBytes, index: NonNegativeInt) => boolean,
|
|
78
|
+
) => void;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validates the {@link OwnerWriteKey} for the given {@link Owner}.
|
|
82
|
+
*
|
|
83
|
+
* Returns `true` if the write key is valid, `false` otherwise.
|
|
84
|
+
*/
|
|
85
|
+
readonly validateWriteKey: (
|
|
86
|
+
ownerId: OwnerIdBytes,
|
|
87
|
+
writeKey: OwnerWriteKey,
|
|
88
|
+
) => boolean;
|
|
89
|
+
|
|
90
|
+
/** Sets the {@link OwnerWriteKey} for the given {@link Owner}. */
|
|
91
|
+
readonly setWriteKey: (
|
|
92
|
+
ownerId: OwnerIdBytes,
|
|
93
|
+
writeKey: OwnerWriteKey,
|
|
94
|
+
) => boolean;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Write encrypted {@link CrdtMessage}s to storage.
|
|
98
|
+
*
|
|
99
|
+
* Must use a mutex (per ownerId on Relay) to ensure sequential processing and
|
|
100
|
+
* proper protocol logic handling during sync operations.
|
|
101
|
+
*
|
|
102
|
+
* Returns `true` on success, `false` on failure.
|
|
103
|
+
*/
|
|
104
|
+
readonly writeMessages: (
|
|
105
|
+
ownerId: OwnerIdBytes,
|
|
106
|
+
messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>,
|
|
107
|
+
) => Promise<boolean>;
|
|
108
|
+
|
|
109
|
+
/** Read encrypted {@link DbChange}s from storage. */
|
|
110
|
+
readonly readDbChange: (
|
|
111
|
+
ownerId: OwnerIdBytes,
|
|
112
|
+
timestamp: TimestampBytes,
|
|
113
|
+
) => EncryptedDbChange | null;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Delete all data for the given {@link Owner}.
|
|
117
|
+
*
|
|
118
|
+
* Returns `true` on success, `false` on failure.
|
|
119
|
+
*/
|
|
120
|
+
readonly deleteOwner: (ownerId: OwnerIdBytes) => boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface StorageDep {
|
|
124
|
+
readonly storage: Storage;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A cryptographic hash used for efficiently comparing collections of
|
|
129
|
+
* {@link TimestampBytes}s.
|
|
130
|
+
*
|
|
131
|
+
* It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
|
|
132
|
+
* one or more timestamps.
|
|
133
|
+
*/
|
|
134
|
+
export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
|
|
135
|
+
|
|
136
|
+
export const fingerprintSize = 12 as NonNegativeInt;
|
|
137
|
+
|
|
138
|
+
/** A fingerprint of an empty range. */
|
|
139
|
+
export const zeroFingerprint = new Uint8Array(fingerprintSize) as Fingerprint;
|
|
140
|
+
|
|
141
|
+
export interface BaseRange {
|
|
142
|
+
readonly upperBound: RangeUpperBound;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Union type for Range's upperBound: either a {@link TimestampBytes} or
|
|
147
|
+
* {@link InfiniteUpperBound}.
|
|
148
|
+
*/
|
|
149
|
+
export type RangeUpperBound = TimestampBytes | InfiniteUpperBound;
|
|
150
|
+
|
|
151
|
+
export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
|
|
152
|
+
export type InfiniteUpperBound = typeof InfiniteUpperBound;
|
|
153
|
+
|
|
154
|
+
export const RangeType = {
|
|
155
|
+
Fingerprint: 1,
|
|
156
|
+
Skip: 0,
|
|
157
|
+
Timestamps: 2,
|
|
158
|
+
} as const;
|
|
159
|
+
|
|
160
|
+
export type RangeType = (typeof RangeType)[keyof typeof RangeType];
|
|
161
|
+
|
|
162
|
+
export interface SkipRange extends BaseRange {
|
|
163
|
+
readonly type: typeof RangeType.Skip;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface FingerprintRange extends BaseRange {
|
|
167
|
+
readonly type: typeof RangeType.Fingerprint;
|
|
168
|
+
readonly fingerprint: Fingerprint;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface TimestampsRange extends BaseRange {
|
|
172
|
+
readonly type: typeof RangeType.Timestamps;
|
|
173
|
+
readonly timestamps: ReadonlyArray<TimestampBytes>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type Range = SkipRange | FingerprintRange | TimestampsRange;
|
|
177
|
+
|
|
178
|
+
/** An encrypted {@link CrdtMessage}. */
|
|
179
|
+
export interface EncryptedCrdtMessage {
|
|
180
|
+
readonly timestamp: Timestamp;
|
|
181
|
+
readonly change: EncryptedDbChange;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Encrypted DbChange */
|
|
185
|
+
export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A CRDT message combining a unique {@link Timestamp} with a {@link DbChange}.
|
|
189
|
+
*
|
|
190
|
+
* Used in Evolu's sync protocol to replicate data changes across devices. Evolu
|
|
191
|
+
* operates as a durable queue, providing exactly-once delivery guarantees for
|
|
192
|
+
* reliable synchronization across application restarts and network failures.
|
|
193
|
+
*/
|
|
194
|
+
export interface CrdtMessage {
|
|
195
|
+
readonly timestamp: Timestamp;
|
|
196
|
+
readonly change: DbChange;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A DbChange is a change to a table row. Together with a unique
|
|
201
|
+
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
202
|
+
*/
|
|
203
|
+
export const DbChange = object({
|
|
204
|
+
table: String,
|
|
205
|
+
id: Id,
|
|
206
|
+
values: record(String, SqliteValue),
|
|
207
|
+
});
|
|
208
|
+
export type DbChange = typeof DbChange.Type;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Common interface for both client and relay SQLite storages.
|
|
3
212
|
*
|
|
4
213
|
* Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
|
|
5
214
|
* implemented in SQL, so it doesn't have to make roundtrips to the DB.
|
|
6
215
|
*
|
|
7
|
-
*
|
|
216
|
+
* While the SQL implementation may look sophisticated, it's conceptually simple
|
|
217
|
+
* and LLMs can explain how it works. The Skiplist data structure is well
|
|
218
|
+
* explained in [this Stack Overflow
|
|
219
|
+
* answer](https://stackoverflow.com/questions/61944198/what-is-a-zip-tree-and-how-does-it-work).
|
|
220
|
+
* The logic resembles [Negentropy's C++
|
|
221
|
+
* storage](https://github.com/hoytech/negentropy), except we use a Skiplist to
|
|
222
|
+
* leverage SQLite indexes, which makes the code simpler.
|
|
223
|
+
*
|
|
224
|
+
* Note: A paid review by the SQLite team is planned, as they use the same
|
|
225
|
+
* algorithm for their rsync tool.
|
|
226
|
+
*
|
|
227
|
+
* The ideal storage for a Relay should use an architecture like
|
|
8
228
|
* [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
|
|
9
229
|
* to ensure that insertion order doesn't matter (local-first apps can often
|
|
10
230
|
* write in the past.)
|
|
@@ -19,37 +239,18 @@
|
|
|
19
239
|
* each other, if necessary. One relay should handle hundreds of thousands of
|
|
20
240
|
* users, and when it goes down, nothing happens, because it will be
|
|
21
241
|
* synchronized later.
|
|
22
|
-
*
|
|
23
|
-
* @module
|
|
24
242
|
*/
|
|
25
|
-
|
|
26
|
-
import { assert } from "../Assert.js";
|
|
27
|
-
import { decrement } from "../Number.js";
|
|
28
|
-
import { RandomDep } from "../Random.js";
|
|
29
|
-
import { ok, Result } from "../Result.js";
|
|
30
|
-
import { sql, SqliteDep, SqliteError } from "../Sqlite.js";
|
|
31
|
-
import { Int64String, NonNegativeInt, PositiveInt } from "../Type.js";
|
|
32
|
-
import { Brand } from "../Brand.js";
|
|
33
|
-
import { OwnerId } from "./Owner.js";
|
|
34
|
-
import {
|
|
35
|
-
BinaryOwnerId,
|
|
36
|
-
binaryOwnerIdToOwnerId,
|
|
37
|
-
binaryTimestampToFingerprint,
|
|
38
|
-
Fingerprint,
|
|
39
|
-
FingerprintRange,
|
|
40
|
-
InfiniteUpperBound,
|
|
41
|
-
RangeType,
|
|
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
243
|
export interface SqliteStorageBase {
|
|
244
|
+
/**
|
|
245
|
+
* Inserts a timestamp for an owner into the skiplist-based storage.
|
|
246
|
+
*
|
|
247
|
+
* Must be idempotent - inserting the same timestamp multiple times has no
|
|
248
|
+
* effect after the first insertion. This is crucial for sync reliability as
|
|
249
|
+
* messages may be received and processed multiple times.
|
|
250
|
+
*/
|
|
50
251
|
readonly insertTimestamp: (
|
|
51
|
-
ownerId:
|
|
52
|
-
timestamp:
|
|
252
|
+
ownerId: OwnerIdBytes,
|
|
253
|
+
timestamp: TimestampBytes,
|
|
53
254
|
) => Result<void, SqliteError>;
|
|
54
255
|
|
|
55
256
|
readonly getSize: Storage["getSize"];
|
|
@@ -64,7 +265,7 @@ export interface SqliteStorageBaseDep {
|
|
|
64
265
|
readonly storage: SqliteStorageBase;
|
|
65
266
|
}
|
|
66
267
|
|
|
67
|
-
export type SqliteStorageDeps =
|
|
268
|
+
export type SqliteStorageDeps = RandomDep & SqliteDep;
|
|
68
269
|
|
|
69
270
|
export interface CreateSqliteStorageBaseOptions {
|
|
70
271
|
onStorageError: (error: SqliteError) => void;
|
|
@@ -81,22 +282,22 @@ export const createSqliteStorageBase =
|
|
|
81
282
|
const ownerStats = new Map<
|
|
82
283
|
OwnerId,
|
|
83
284
|
{
|
|
84
|
-
minT:
|
|
85
|
-
maxT:
|
|
285
|
+
minT: TimestampBytes;
|
|
286
|
+
maxT: TimestampBytes;
|
|
86
287
|
}
|
|
87
288
|
>();
|
|
88
289
|
|
|
89
290
|
return ok({
|
|
90
|
-
insertTimestamp: (ownerId:
|
|
91
|
-
const ownerIdString =
|
|
291
|
+
insertTimestamp: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => {
|
|
292
|
+
const ownerIdString = ownerIdBytesToOwnerId(ownerId);
|
|
92
293
|
const level = randomSkiplistLevel(deps);
|
|
93
294
|
|
|
94
295
|
let stats = ownerStats.get(ownerIdString);
|
|
95
296
|
|
|
96
297
|
if (!stats) {
|
|
97
298
|
const result = deps.sqlite.exec<{
|
|
98
|
-
maxT:
|
|
99
|
-
minT:
|
|
299
|
+
maxT: TimestampBytes | null;
|
|
300
|
+
minT: TimestampBytes | null;
|
|
100
301
|
}>(sql.prepared`
|
|
101
302
|
select min(t) as minT, max(t) as maxT
|
|
102
303
|
from evolu_timestamp
|
|
@@ -113,10 +314,10 @@ export const createSqliteStorageBase =
|
|
|
113
314
|
|
|
114
315
|
let strategy: InsertTimestampStrategy;
|
|
115
316
|
|
|
116
|
-
if (
|
|
317
|
+
if (orderTimestampBytes(timestamp, stats.maxT) === 1) {
|
|
117
318
|
strategy = "append";
|
|
118
319
|
stats.maxT = timestamp;
|
|
119
|
-
} else if (
|
|
320
|
+
} else if (orderTimestampBytes(timestamp, stats.minT) === -1) {
|
|
120
321
|
strategy = "prepend";
|
|
121
322
|
stats.minT = timestamp;
|
|
122
323
|
} else {
|
|
@@ -193,7 +394,7 @@ export const createSqliteStorageBase =
|
|
|
193
394
|
* implementing chunking, be sure to run performance tests (including
|
|
194
395
|
* fetching one by one).
|
|
195
396
|
*/
|
|
196
|
-
const result = deps.sqlite.exec<{ t:
|
|
397
|
+
const result = deps.sqlite.exec<{ t: TimestampBytes }>(sql`
|
|
197
398
|
select t
|
|
198
399
|
from evolu_timestamp
|
|
199
400
|
where ownerId = ${ownerId} and t > ${first.value}
|
|
@@ -241,7 +442,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
241
442
|
*
|
|
242
443
|
* Columns:
|
|
243
444
|
*
|
|
244
|
-
* - `t` –
|
|
445
|
+
* - `t` – TimestampBytes
|
|
245
446
|
* - `h1`/`h2` – 12-byte fingerprint split into two integers for fast XOR
|
|
246
447
|
* - `c` – incremental count
|
|
247
448
|
* - `l` – Skiplist level (1 to 32)
|
|
@@ -295,13 +496,13 @@ type InsertTimestampStrategy = "append" | "prepend" | "insert";
|
|
|
295
496
|
const insertTimestamp =
|
|
296
497
|
(deps: SqliteDep) =>
|
|
297
498
|
(
|
|
298
|
-
ownerId:
|
|
299
|
-
timestamp:
|
|
499
|
+
ownerId: OwnerIdBytes,
|
|
500
|
+
timestamp: TimestampBytes,
|
|
300
501
|
level: PositiveInt,
|
|
301
502
|
strategy: InsertTimestampStrategy,
|
|
302
503
|
): Result<void, SqliteError> => {
|
|
303
504
|
const [h1, h2] = fingerprintToSqliteFingerprint(
|
|
304
|
-
|
|
505
|
+
timestampBytesToFingerprint(timestamp),
|
|
305
506
|
);
|
|
306
507
|
|
|
307
508
|
let queries: Array<ReturnType<typeof sql.prepared>> = [];
|
|
@@ -819,6 +1020,13 @@ const insertTimestamp =
|
|
|
819
1020
|
return ok();
|
|
820
1021
|
};
|
|
821
1022
|
|
|
1023
|
+
export const timestampBytesToFingerprint = (
|
|
1024
|
+
timestamp: TimestampBytes,
|
|
1025
|
+
): Fingerprint => {
|
|
1026
|
+
const hash = sha256(timestamp).slice(0, fingerprintSize);
|
|
1027
|
+
return hash as Fingerprint;
|
|
1028
|
+
};
|
|
1029
|
+
|
|
822
1030
|
/**
|
|
823
1031
|
* Generates a random skiplist level in the range [1, skiplistMaxLevel].
|
|
824
1032
|
* Probabilistic approach avoids the need for explicit tree balancing.
|
|
@@ -899,7 +1107,7 @@ const sqliteFingerprintToFingerprint = ([
|
|
|
899
1107
|
|
|
900
1108
|
const getSize =
|
|
901
1109
|
(deps: SqliteDep) =>
|
|
902
|
-
(ownerId:
|
|
1110
|
+
(ownerId: OwnerIdBytes): Result<NonNegativeInt, SqliteError> => {
|
|
903
1111
|
const result = deps.sqlite.exec<{ size: NonNegativeInt }>(sql.prepared`
|
|
904
1112
|
with
|
|
905
1113
|
ml(ml) as (
|
|
@@ -942,7 +1150,7 @@ const getSize =
|
|
|
942
1150
|
const findLowerBound =
|
|
943
1151
|
(deps: SqliteDep) =>
|
|
944
1152
|
(
|
|
945
|
-
ownerId:
|
|
1153
|
+
ownerId: OwnerIdBytes,
|
|
946
1154
|
begin: NonNegativeInt,
|
|
947
1155
|
end: NonNegativeInt,
|
|
948
1156
|
upperBound: RangeUpperBound,
|
|
@@ -954,7 +1162,7 @@ const findLowerBound =
|
|
|
954
1162
|
}
|
|
955
1163
|
|
|
956
1164
|
const result = deps.sqlite.exec<{
|
|
957
|
-
t:
|
|
1165
|
+
t: TimestampBytes;
|
|
958
1166
|
}>(sql.prepared`
|
|
959
1167
|
select t
|
|
960
1168
|
from evolu_timestamp
|
|
@@ -978,8 +1186,8 @@ const findLowerBound =
|
|
|
978
1186
|
const getTimestampCount =
|
|
979
1187
|
(deps: SqliteDep) =>
|
|
980
1188
|
(
|
|
981
|
-
ownerId:
|
|
982
|
-
timestamp:
|
|
1189
|
+
ownerId: OwnerIdBytes,
|
|
1190
|
+
timestamp: TimestampBytes,
|
|
983
1191
|
): Result<PositiveInt, SqliteError> => {
|
|
984
1192
|
const result = deps.sqlite.exec<{
|
|
985
1193
|
count: PositiveInt;
|
|
@@ -1036,7 +1244,7 @@ const getTimestampCount =
|
|
|
1036
1244
|
const fingerprint =
|
|
1037
1245
|
(deps: SqliteDep) =>
|
|
1038
1246
|
(
|
|
1039
|
-
ownerId:
|
|
1247
|
+
ownerId: OwnerIdBytes,
|
|
1040
1248
|
begin: NonNegativeInt,
|
|
1041
1249
|
end: NonNegativeInt,
|
|
1042
1250
|
): Result<Fingerprint, SqliteError> => {
|
|
@@ -1068,14 +1276,14 @@ const fingerprint =
|
|
|
1068
1276
|
const fingerprintRanges =
|
|
1069
1277
|
(deps: SqliteDep) =>
|
|
1070
1278
|
(
|
|
1071
|
-
ownerId:
|
|
1279
|
+
ownerId: OwnerIdBytes,
|
|
1072
1280
|
buckets: ReadonlyArray<NonNegativeInt>,
|
|
1073
1281
|
upperBound: RangeUpperBound = InfiniteUpperBound,
|
|
1074
1282
|
): Result<ReadonlyArray<FingerprintRange>, SqliteError> => {
|
|
1075
1283
|
const bucketsJson = JSON.stringify(buckets);
|
|
1076
1284
|
|
|
1077
1285
|
const result = deps.sqlite.exec<{
|
|
1078
|
-
b:
|
|
1286
|
+
b: TimestampBytes | null;
|
|
1079
1287
|
h1: Int64String;
|
|
1080
1288
|
h2: Int64String;
|
|
1081
1289
|
}>(sql.prepared`
|
|
@@ -1211,11 +1419,11 @@ const x = (a: string, b: string) => sql.raw(`(${a} | ${b}) - (${a} & ${b})`);
|
|
|
1211
1419
|
export const getTimestampByIndex =
|
|
1212
1420
|
(deps: SqliteDep) =>
|
|
1213
1421
|
(
|
|
1214
|
-
ownerId:
|
|
1422
|
+
ownerId: OwnerIdBytes,
|
|
1215
1423
|
index: NonNegativeInt,
|
|
1216
|
-
): Result<
|
|
1424
|
+
): Result<TimestampBytes, SqliteError> => {
|
|
1217
1425
|
const result = deps.sqlite.exec<{
|
|
1218
|
-
readonly pt:
|
|
1426
|
+
readonly pt: TimestampBytes;
|
|
1219
1427
|
}>(sql.prepared`
|
|
1220
1428
|
with
|
|
1221
1429
|
fi(b, cl, ic, pt, mt, nt, nc) as (
|