@evolu/common 8.10.0 → 8.11.0

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.
Files changed (144) hide show
  1. package/dist/src/Config.d.ts +22 -22
  2. package/dist/src/Config.d.ts.map +1 -1
  3. package/dist/src/Console.d.ts +62 -7
  4. package/dist/src/Console.d.ts.map +1 -1
  5. package/dist/src/Console.js +20 -4
  6. package/dist/src/Crypto.d.ts +76 -4
  7. package/dist/src/Crypto.d.ts.map +1 -1
  8. package/dist/src/Crypto.js +55 -4
  9. package/dist/src/Error.d.ts +45 -0
  10. package/dist/src/Error.d.ts.map +1 -1
  11. package/dist/src/Error.js +69 -0
  12. package/dist/src/Fs.d.ts +92 -18
  13. package/dist/src/Fs.d.ts.map +1 -1
  14. package/dist/src/Fs.js +2 -0
  15. package/dist/src/Identicon.d.ts +2 -2
  16. package/dist/src/Identicon.js +2 -2
  17. package/dist/src/LeakDetector.d.ts +22 -3
  18. package/dist/src/LeakDetector.d.ts.map +1 -1
  19. package/dist/src/LeakDetector.js +12 -2
  20. package/dist/src/LockManager.d.ts +8 -0
  21. package/dist/src/LockManager.d.ts.map +1 -1
  22. package/dist/src/LockManager.js +6 -0
  23. package/dist/src/Object.d.ts.map +1 -1
  24. package/dist/src/Object.js +5 -0
  25. package/dist/src/Platform.d.ts +47 -7
  26. package/dist/src/Platform.d.ts.map +1 -1
  27. package/dist/src/Platform.js +24 -5
  28. package/dist/src/Random.d.ts +25 -2
  29. package/dist/src/Random.d.ts.map +1 -1
  30. package/dist/src/Random.js +14 -2
  31. package/dist/src/Resource.d.ts +156 -1
  32. package/dist/src/Resource.d.ts.map +1 -1
  33. package/dist/src/Resource.js +201 -72
  34. package/dist/src/Schedule.d.ts +11 -10
  35. package/dist/src/Schedule.d.ts.map +1 -1
  36. package/dist/src/Schedule.js +1 -1
  37. package/dist/src/Sqlite.d.ts +132 -16
  38. package/dist/src/Sqlite.d.ts.map +1 -1
  39. package/dist/src/Sqlite.js +63 -9
  40. package/dist/src/Task.d.ts +15 -4
  41. package/dist/src/Task.d.ts.map +1 -1
  42. package/dist/src/Task.js +41 -15
  43. package/dist/src/Test.d.ts +9 -0
  44. package/dist/src/Test.d.ts.map +1 -1
  45. package/dist/src/Test.js +4 -0
  46. package/dist/src/Time.d.ts +106 -9
  47. package/dist/src/Time.d.ts.map +1 -1
  48. package/dist/src/Time.js +55 -4
  49. package/dist/src/Type.d.ts +1455 -1310
  50. package/dist/src/Type.d.ts.map +1 -1
  51. package/dist/src/Type.js +1274 -517
  52. package/dist/src/WebSocket.d.ts +164 -13
  53. package/dist/src/WebSocket.d.ts.map +1 -1
  54. package/dist/src/WebSocket.js +133 -24
  55. package/dist/src/Worker.d.ts +90 -8
  56. package/dist/src/Worker.d.ts.map +1 -1
  57. package/dist/src/Worker.js +28 -2
  58. package/dist/src/index.d.ts +6 -7
  59. package/dist/src/index.d.ts.map +1 -1
  60. package/dist/src/index.js +2 -3
  61. package/dist/src/local-first/Db.d.ts +52 -3
  62. package/dist/src/local-first/Db.d.ts.map +1 -1
  63. package/dist/src/local-first/Db.js +412 -137
  64. package/dist/src/local-first/Evolu.d.ts +336 -211
  65. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  66. package/dist/src/local-first/Evolu.js +102 -15
  67. package/dist/src/local-first/Owner.d.ts +13 -30
  68. package/dist/src/local-first/Owner.d.ts.map +1 -1
  69. package/dist/src/local-first/Owner.js +13 -30
  70. package/dist/src/local-first/Protocol.d.ts +94 -16
  71. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  72. package/dist/src/local-first/Protocol.js +118 -38
  73. package/dist/src/local-first/Query.d.ts +8 -15
  74. package/dist/src/local-first/Query.d.ts.map +1 -1
  75. package/dist/src/local-first/Schema.d.ts +335 -21
  76. package/dist/src/local-first/Schema.d.ts.map +1 -1
  77. package/dist/src/local-first/Schema.js +214 -17
  78. package/dist/src/local-first/Shared.d.ts +537 -22
  79. package/dist/src/local-first/Shared.d.ts.map +1 -1
  80. package/dist/src/local-first/Shared.js +1437 -234
  81. package/dist/src/local-first/Storage.d.ts +192 -14
  82. package/dist/src/local-first/Storage.d.ts.map +1 -1
  83. package/dist/src/local-first/Storage.js +81 -20
  84. package/dist/src/local-first/Timestamp.d.ts +392 -41
  85. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  86. package/dist/src/local-first/Timestamp.js +403 -81
  87. package/dist/src/local-first/index.d.ts +0 -1
  88. package/dist/src/local-first/index.d.ts.map +1 -1
  89. package/dist/src/local-first/index.js +0 -1
  90. package/package.json +1 -1
  91. package/src/Assert.test.ts +2 -5
  92. package/src/Config.test.ts +2 -6
  93. package/src/Config.ts +133 -133
  94. package/src/Console.ts +62 -7
  95. package/src/Crypto.ts +76 -4
  96. package/src/Eq.test.ts +2 -3
  97. package/src/Error.test.ts +76 -3
  98. package/src/Error.ts +71 -0
  99. package/src/Fs.ts +92 -18
  100. package/src/Identicon.ts +2 -2
  101. package/src/LeakDetector.ts +22 -3
  102. package/src/LockManager.ts +8 -0
  103. package/src/Object.test.ts +27 -12
  104. package/src/Object.ts +5 -0
  105. package/src/Platform.ts +50 -8
  106. package/src/Random.ts +25 -2
  107. package/src/Resource.test.ts +837 -0
  108. package/src/Resource.ts +235 -15
  109. package/src/Schedule.test.ts +50 -12
  110. package/src/Schedule.ts +24 -14
  111. package/src/Sqlite.ts +137 -17
  112. package/src/Task.test.ts +189 -8
  113. package/src/Task.ts +56 -17
  114. package/src/Test.ts +9 -0
  115. package/src/Time.ts +106 -9
  116. package/src/Type.test.ts +946 -1028
  117. package/src/Type.ts +4195 -3136
  118. package/src/Types.test.ts +4 -14
  119. package/src/WebSocket.ts +313 -40
  120. package/src/Worker.ts +90 -8
  121. package/src/index.ts +15 -6
  122. package/src/local-first/Db.ts +644 -339
  123. package/src/local-first/Evolu.test.ts +686 -21
  124. package/src/local-first/Evolu.ts +450 -228
  125. package/src/local-first/Owner.ts +13 -30
  126. package/src/local-first/Protocol.test.ts +617 -10
  127. package/src/local-first/Protocol.ts +196 -72
  128. package/src/local-first/Query.ts +8 -15
  129. package/src/local-first/Schema.test.ts +143 -0
  130. package/src/local-first/Schema.ts +363 -24
  131. package/src/local-first/Shared.test.ts +7731 -559
  132. package/src/local-first/Shared.ts +2036 -267
  133. package/src/local-first/Storage.ts +218 -32
  134. package/src/local-first/Timestamp.test.ts +344 -70
  135. package/src/local-first/Timestamp.ts +434 -118
  136. package/src/local-first/index.ts +0 -1
  137. package/dist/src/local-first/Error.d.ts +0 -12
  138. package/dist/src/local-first/Error.d.ts.map +0 -1
  139. package/dist/src/local-first/Error.js +0 -6
  140. package/dist/src/local-first/LocalAuth.d.ts +0 -150
  141. package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
  142. package/dist/src/local-first/LocalAuth.js +0 -179
  143. package/src/local-first/Error.ts +0 -17
  144. package/src/local-first/LocalAuth.ts +0 -457
@@ -3,34 +3,36 @@ import {
3
3
  assertEqual,
4
4
  assertErr,
5
5
  assertFalse,
6
+ assertNotSame,
6
7
  assertOk,
8
+ assertThrowsInstanceOf,
7
9
  assertTrue,
8
10
  } from "../Assert.ts";
9
11
  import { increment } from "../Number.ts";
10
12
  import { orderNumber } from "../Order.ts";
11
13
  import type { Result } from "../Result.ts";
12
- import { ok } from "../Result.ts";
13
14
  import { testCreateDeps } from "../Task.ts";
14
- import type { Time, TimeDep } from "../Time.ts";
15
- import { maxMillis, Millis, minMillis, testCreateTime } from "../Time.ts";
15
+ import { maxMillis, Millis, minMillis } from "../Time.ts";
16
+ import { assertType } from "../Type.ts";
16
17
  import type {
17
18
  Timestamp,
18
19
  TimestampBytes,
19
20
  TimestampConfigDep,
20
- TimestampCounterOverflowError,
21
21
  TimestampDriftError,
22
- TimestampTimeOutOfRangeError,
23
22
  } from "./Timestamp.ts";
24
23
  import {
25
24
  Counter,
26
25
  createInitialTimestamp,
27
26
  createTimestamp,
28
27
  defaultTimestampMaxDrift,
28
+ isTimestampBeyondMaxDrift,
29
29
  maxCounter,
30
+ maxNodeId,
30
31
  minCounter,
31
32
  NodeId,
32
33
  nodeIdBytesToNodeId,
33
34
  nodeIdToNodeIdBytes,
35
+ orderTimestamp,
34
36
  orderTimestampBytes,
35
37
  receiveTimestamp,
36
38
  sendTimestamp,
@@ -93,21 +95,90 @@ test("creates an initial Timestamp", () => {
93
95
  });
94
96
  });
95
97
 
98
+ describe("orderTimestamp", () => {
99
+ it("compares equal timestamps regardless of object identity", () => {
100
+ const timestamp = createTimestamp({
101
+ millis: maxMillis,
102
+ counter: maxCounter,
103
+ nodeId: maxNodeId,
104
+ });
105
+ const copy = { ...timestamp };
106
+
107
+ assertNotSame(timestamp, copy);
108
+ assertEqual(orderTimestamp(timestamp, timestamp), 0);
109
+ assertEqual(orderTimestamp(timestamp, copy), 0);
110
+ assertEqual(orderTimestamp(copy, timestamp), 0);
111
+ });
112
+
113
+ it("orders millis, counter, then nodeId consistently with timestamp bytes", () => {
114
+ const timestamps = [
115
+ createTimestamp(),
116
+ createTimestamp({ nodeId: NodeId.orThrow("9999999999999999") }),
117
+ createTimestamp({ nodeId: NodeId.orThrow("aaaaaaaaaaaaaaaa") }),
118
+ createTimestamp({ nodeId: maxNodeId }),
119
+ createTimestamp({ counter: Counter.orThrow(255), nodeId: maxNodeId }),
120
+ createTimestamp({ counter: Counter.orThrow(256) }),
121
+ createTimestamp({ counter: maxCounter, nodeId: maxNodeId }),
122
+ createTimestamp({ millis: Millis.orThrow(1) }),
123
+ createTimestamp({
124
+ millis: Millis.orThrow(2 ** 32 - 1),
125
+ counter: maxCounter,
126
+ nodeId: maxNodeId,
127
+ }),
128
+ createTimestamp({ millis: Millis.orThrow(2 ** 32) }),
129
+ createTimestamp({
130
+ millis: maxMillis,
131
+ counter: maxCounter,
132
+ nodeId: maxNodeId,
133
+ }),
134
+ ];
135
+ const bytes = timestamps.map(timestampToTimestampBytes);
136
+
137
+ for (const [i, a] of timestamps.entries()) {
138
+ for (const [j, b] of timestamps.entries()) {
139
+ const order = orderTimestamp(a, b);
140
+ assertEqual(order, orderNumber(i, j));
141
+ assertEqual(order, orderTimestampBytes(bytes[i], bytes[j]));
142
+ }
143
+ }
144
+ });
145
+ });
146
+
96
147
  const makeMillis = (millis: number): Millis => Millis.orThrow(millis);
97
148
 
98
- const deps0: TimeDep & TimestampConfigDep = {
99
- time: testCreateTime({ startAt: minMillis }),
100
- timestampConfig: { maxDrift: defaultTimestampMaxDrift },
101
- };
149
+ describe("isTimestampBeyondMaxDrift", () => {
150
+ it("accepts past timestamps and the exact future limit", () => {
151
+ const isBeyondMaxDrift = isTimestampBeyondMaxDrift({
152
+ timestampConfig: { maxDrift: 10 },
153
+ });
154
+ const now = makeMillis(100);
155
+
156
+ assertFalse(isBeyondMaxDrift(makeMillis(0), now));
157
+ assertFalse(isBeyondMaxDrift(now, now));
158
+ assertFalse(isBeyondMaxDrift(makeMillis(109), now));
159
+ assertFalse(isBeyondMaxDrift(makeMillis(110), now));
160
+ assertTrue(isBeyondMaxDrift(makeMillis(111), now));
161
+ });
162
+
163
+ it("rejects any future timestamp when the limit is zero", () => {
164
+ const isBeyondMaxDrift = isTimestampBeyondMaxDrift({
165
+ timestampConfig: { maxDrift: 0 },
166
+ });
167
+ const now = makeMillis(100);
168
+
169
+ assertFalse(isBeyondMaxDrift(makeMillis(99), now));
170
+ assertFalse(isBeyondMaxDrift(now, now));
171
+ assertTrue(isBeyondMaxDrift(makeMillis(101), now));
172
+ });
173
+ });
102
174
 
103
- const deps1: TimeDep & TimestampConfigDep = {
104
- time: testCreateTime({ startAt: (minMillis + 1) as Millis }),
175
+ const config: TimestampConfigDep = {
105
176
  timestampConfig: { maxDrift: defaultTimestampMaxDrift },
106
177
  };
107
178
 
108
179
  describe("sendTimestamp", () => {
109
180
  it("sends monotonically with a monotonic clock", () => {
110
- assertOk(sendTimestamp(deps1)(createTimestamp()), {
181
+ assertOk(sendTimestamp(config)(createTimestamp(), makeMillis(1)), {
111
182
  counter: 0,
112
183
  millis: 1,
113
184
  nodeId: "0000000000000000",
@@ -115,7 +186,7 @@ describe("sendTimestamp", () => {
115
186
  });
116
187
 
117
188
  it("sends monotonically with a stuttering clock", () => {
118
- assertOk(sendTimestamp(deps0)(createTimestamp()), {
189
+ assertOk(sendTimestamp(config)(createTimestamp(), minMillis), {
119
190
  counter: 1,
120
191
  millis: 0,
121
192
  nodeId: "0000000000000000",
@@ -124,8 +195,9 @@ describe("sendTimestamp", () => {
124
195
 
125
196
  it("sends monotonically with a regressing clock", () => {
126
197
  assertOk(
127
- sendTimestamp(deps0)(
198
+ sendTimestamp(config)(
128
199
  createTimestamp({ millis: makeMillis(minMillis + 1) }),
200
+ minMillis,
129
201
  ),
130
202
  {
131
203
  counter: 1,
@@ -135,52 +207,64 @@ describe("sendTimestamp", () => {
135
207
  );
136
208
  });
137
209
 
138
- it("returns TimestampCounterOverflowError for counter overflow", () => {
139
- let timestamp: Result<
140
- Timestamp,
141
- | TimestampDriftError
142
- | TimestampCounterOverflowError
143
- | TimestampTimeOutOfRangeError
144
- > = ok(createTimestamp());
145
-
146
- // Note +1 in 65536.
147
- for (let i = 0; i < 65536; i++) {
148
- if (timestamp.ok) timestamp = sendTimestamp(deps0)(timestamp.value);
210
+ it("continues past a full counter with fixed wall time", () => {
211
+ let timestamp = createTimestamp();
212
+ for (let i = 0; i <= maxCounter; i++) {
213
+ const next = sendTimestamp(config)(timestamp, minMillis);
214
+ assertOk(next);
215
+ timestamp = next.value;
149
216
  }
217
+ assertEqual(timestamp, createTimestamp({ millis: makeMillis(1) }));
218
+ assertOk(
219
+ sendTimestamp(config)(timestamp, minMillis),
220
+ createTimestamp({ millis: makeMillis(1), counter: Counter.orThrow(1) }),
221
+ );
222
+ });
150
223
 
151
- assertErr(timestamp, { type: "TimestampCounterOverflowError" });
224
+ it("continues past a full counter while the clock is ahead of advancing wall time", () => {
225
+ // Wall time advances by one millisecond per send, while the clock stays two
226
+ // minutes ahead, as after receiving a timestamp from a fast peer. Every
227
+ // send keeps the pinned millis, so the counter alone must carry ordering.
228
+ const pinnedMillis = makeMillis(2 * 60 * 1000);
229
+ let timestamp = createTimestamp({ millis: pinnedMillis });
230
+ for (let i = 0; i <= maxCounter; i++) {
231
+ const next = sendTimestamp(config)(timestamp, makeMillis(i));
232
+ assertOk(next);
233
+ assertEqual(
234
+ orderTimestampBytes(
235
+ timestampToTimestampBytes(timestamp),
236
+ timestampToTimestampBytes(next.value),
237
+ ),
238
+ -1,
239
+ );
240
+ if (i < maxCounter) assertEqual(next.value.millis, pinnedMillis);
241
+ timestamp = next.value;
242
+ }
243
+ assertEqual(
244
+ timestamp,
245
+ createTimestamp({ millis: makeMillis(pinnedMillis + 1) }),
246
+ );
152
247
  });
153
248
 
154
- it("returns TimestampDriftError for excessive clock drift", () => {
249
+ it("returns TimestampDriftError for an ahead local clock", () => {
155
250
  assertErr(
156
- sendTimestamp(deps0)(
251
+ sendTimestamp(config)(
157
252
  createTimestamp({
158
253
  millis: makeMillis(minMillis + defaultTimestampMaxDrift + 1),
159
254
  }),
255
+ minMillis,
160
256
  ),
161
257
  {
162
- next: 300001,
163
- now: 0,
164
258
  type: "TimestampDriftError",
259
+ now: minMillis,
260
+ timestamp: createTimestamp({
261
+ millis: makeMillis(300001),
262
+ counter: Counter.orThrow(1),
263
+ }),
264
+ cause: "local",
165
265
  },
166
266
  );
167
267
  });
168
-
169
- it("returns TimestampTimeOutOfRangeError for an invalid clock", () => {
170
- const time = testCreateTime();
171
- const invalidTime: Time = {
172
- ...time,
173
- now: (() => -1) as Time["now"],
174
- };
175
-
176
- assertErr(
177
- sendTimestamp({
178
- time: invalidTime,
179
- timestampConfig: { maxDrift: defaultTimestampMaxDrift },
180
- })(createTimestamp()),
181
- { type: "TimestampTimeOutOfRangeError" },
182
- );
183
- });
184
268
  });
185
269
 
186
270
  describe("receiveTimestamp", () => {
@@ -200,7 +284,11 @@ describe("receiveTimestamp", () => {
200
284
 
201
285
  it("uses a wall clock later than both timestamps", () => {
202
286
  assertOk(
203
- receiveTimestamp(deps1)(makeNode1Timestamp(), makeNode2Timestamp()),
287
+ receiveTimestamp(config)(
288
+ makeNode1Timestamp(),
289
+ makeNode2Timestamp(),
290
+ makeMillis(1),
291
+ ),
204
292
  {
205
293
  counter: 0,
206
294
  millis: 1,
@@ -212,9 +300,10 @@ describe("receiveTimestamp", () => {
212
300
  describe("wall clock is behind", () => {
213
301
  it("increments the maximum counter when millis are equal", () => {
214
302
  assertOk(
215
- receiveTimestamp(deps1)(
303
+ receiveTimestamp(config)(
216
304
  makeNode1Timestamp(1, 0),
217
305
  makeNode2Timestamp(1, 1),
306
+ makeMillis(1),
218
307
  ),
219
308
  {
220
309
  counter: 2,
@@ -224,9 +313,10 @@ describe("receiveTimestamp", () => {
224
313
  );
225
314
 
226
315
  assertOk(
227
- receiveTimestamp(deps0)(
316
+ receiveTimestamp(config)(
228
317
  makeNode1Timestamp(1, 1),
229
318
  makeNode2Timestamp(1, 0),
319
+ minMillis,
230
320
  ),
231
321
  {
232
322
  counter: 2,
@@ -238,7 +328,11 @@ describe("receiveTimestamp", () => {
238
328
 
239
329
  it("increments the counter when local millis is later", () => {
240
330
  assertOk(
241
- receiveTimestamp(deps0)(makeNode1Timestamp(2), makeNode2Timestamp(1)),
331
+ receiveTimestamp(config)(
332
+ makeNode1Timestamp(2),
333
+ makeNode2Timestamp(1),
334
+ minMillis,
335
+ ),
242
336
  {
243
337
  counter: 1,
244
338
  millis: 2,
@@ -249,7 +343,11 @@ describe("receiveTimestamp", () => {
249
343
 
250
344
  it("increments the counter when remote millis is later", () => {
251
345
  assertOk(
252
- receiveTimestamp(deps0)(makeNode1Timestamp(1), makeNode2Timestamp(2)),
346
+ receiveTimestamp(config)(
347
+ makeNode1Timestamp(1),
348
+ makeNode2Timestamp(2),
349
+ minMillis,
350
+ ),
253
351
  {
254
352
  counter: 1,
255
353
  millis: 2,
@@ -258,43 +356,219 @@ describe("receiveTimestamp", () => {
258
356
  );
259
357
  });
260
358
 
261
- it("returns TimestampDriftError for excessive clock drift", () => {
262
- const expected = {
263
- next: 300001,
264
- now: 0,
265
- type: "TimestampDriftError",
266
- } as const;
267
-
359
+ it("returns local drift when an accepted remote timestamp leaves the clock ahead", () => {
268
360
  assertErr(
269
- receiveTimestamp(deps0)(
361
+ receiveTimestamp(config)(
270
362
  createTimestamp({
271
363
  millis: makeMillis(minMillis + defaultTimestampMaxDrift + 1),
272
364
  }),
273
365
  makeNode2Timestamp(),
366
+ minMillis,
367
+ ),
368
+ {
369
+ type: "TimestampDriftError",
370
+ now: minMillis,
371
+ timestamp: createTimestamp({
372
+ millis: makeMillis(300001),
373
+ counter: Counter.orThrow(1),
374
+ }),
375
+ cause: "local",
376
+ },
377
+ );
378
+ });
379
+
380
+ it("rejects remote drift without a separate caller check", () => {
381
+ const remote = createTimestamp({
382
+ millis: makeMillis(minMillis + defaultTimestampMaxDrift + 1),
383
+ });
384
+ const local = makeNode2Timestamp();
385
+ const before = { ...local };
386
+ assertErr(receiveTimestamp(config)(local, remote, minMillis), {
387
+ type: "TimestampDriftError",
388
+ timestamp: remote,
389
+ cause: "remote",
390
+ now: minMillis,
391
+ });
392
+ assertEqual(local, before);
393
+ });
394
+
395
+ it("rejects remote drift before arithmetic at the range ceiling", () => {
396
+ const remote = createTimestamp({
397
+ millis: maxMillis,
398
+ counter: maxCounter,
399
+ });
400
+ assertErr(
401
+ receiveTimestamp(config)(createTimestamp(), remote, minMillis),
402
+ {
403
+ type: "TimestampDriftError",
404
+ timestamp: remote,
405
+ cause: "remote",
406
+ now: minMillis,
407
+ },
408
+ );
409
+ });
410
+
411
+ it("uses the configured drift boundary, including a zero limit", () => {
412
+ for (const maxDrift of [0, 10]) {
413
+ const deps = { timestampConfig: { maxDrift } };
414
+ const now = makeMillis(100);
415
+ const local = makeNode1Timestamp();
416
+ const accepted = makeNode2Timestamp(100 + maxDrift);
417
+ assertOk(
418
+ receiveTimestamp(deps)(local, accepted, now),
419
+ makeNode1Timestamp(100 + maxDrift, 1),
420
+ );
421
+ const rejected = makeNode2Timestamp(101 + maxDrift);
422
+ assertErr(receiveTimestamp(deps)(local, rejected, now), {
423
+ type: "TimestampDriftError",
424
+ timestamp: rejected,
425
+ cause: "remote",
426
+ now: makeMillis(100),
427
+ });
428
+ }
429
+ });
430
+
431
+ for (const [
432
+ label,
433
+ localMillis,
434
+ remoteMillis,
435
+ localCounter,
436
+ remoteCounter,
437
+ ] of [
438
+ ["equal millis with the local counter exhausted", 1, 1, maxCounter, 0],
439
+ ["equal millis with the remote counter exhausted", 1, 1, 0, maxCounter],
440
+ [
441
+ "equal millis with both counters exhausted",
442
+ 1,
443
+ 1,
444
+ maxCounter,
445
+ maxCounter,
446
+ ],
447
+ ["later local millis", 1, 0, maxCounter, 0],
448
+ ["later remote millis", 0, 1, 0, maxCounter],
449
+ ] as const) {
450
+ it(`rolls over for ${label}`, () => {
451
+ const local = makeNode1Timestamp(localMillis, localCounter);
452
+ const remote = makeNode2Timestamp(remoteMillis, remoteCounter);
453
+ const result = receiveTimestamp(config)(local, remote, minMillis);
454
+ assertOk(result, makeNode1Timestamp(2, 0));
455
+ const bytes = timestampToTimestampBytes(result.value);
456
+ assertEqual(
457
+ orderTimestampBytes(timestampToTimestampBytes(local), bytes),
458
+ -1,
459
+ );
460
+ assertEqual(
461
+ orderTimestampBytes(timestampToTimestampBytes(remote), bytes),
462
+ -1,
463
+ );
464
+ });
465
+ }
466
+ it("resets exhausted counters when wall time is newer", () => {
467
+ assertOk(
468
+ receiveTimestamp(config)(
469
+ makeNode1Timestamp(1, maxCounter),
470
+ makeNode2Timestamp(1, maxCounter),
471
+ makeMillis(2),
274
472
  ),
275
- expected,
473
+ makeNode1Timestamp(2, 0),
276
474
  );
475
+ });
476
+ });
477
+ });
277
478
 
479
+ describe("timestamp rollover boundaries", () => {
480
+ for (const operation of ["send", "receive"] as const) {
481
+ const nextTimestamp = (timestamp: Timestamp, now = minMillis) =>
482
+ operation === "send"
483
+ ? sendTimestamp(config)(timestamp, now)
484
+ : receiveTimestamp(config)(timestamp, timestamp, now);
485
+
486
+ it(`${operation} uses the last counter before rolling over`, () => {
487
+ const lastCounter = createTimestamp({ counter: maxCounter });
488
+ assertOk(
489
+ nextTimestamp(
490
+ createTimestamp({ counter: Counter.orThrow(maxCounter - 1) }),
491
+ ),
492
+ lastCounter,
493
+ );
494
+ const result = nextTimestamp(lastCounter);
495
+ assertOk(result, createTimestamp({ millis: makeMillis(1) }));
496
+ assertEqual(
497
+ timestampBytesToTimestamp(timestampToTimestampBytes(result.value)),
498
+ result.value,
499
+ );
500
+ });
501
+
502
+ it(`${operation} permits rollover at the drift limit and rejects the next millisecond`, () => {
503
+ assertOk(
504
+ nextTimestamp(
505
+ createTimestamp({
506
+ millis: makeMillis(defaultTimestampMaxDrift - 1),
507
+ counter: maxCounter,
508
+ }),
509
+ ),
510
+ createTimestamp({ millis: makeMillis(defaultTimestampMaxDrift) }),
511
+ );
278
512
  assertErr(
279
- receiveTimestamp(deps0)(
280
- makeNode2Timestamp(),
513
+ nextTimestamp(
281
514
  createTimestamp({
282
- millis: makeMillis(minMillis + defaultTimestampMaxDrift + 1),
515
+ millis: makeMillis(defaultTimestampMaxDrift),
516
+ counter: maxCounter,
283
517
  }),
284
518
  ),
285
- expected,
519
+ {
520
+ type: "TimestampDriftError",
521
+ now: minMillis,
522
+ timestamp: createTimestamp({
523
+ millis: makeMillis(defaultTimestampMaxDrift + 1),
524
+ }),
525
+ cause: "local",
526
+ },
286
527
  );
287
528
  });
288
529
 
289
- it("returns TimestampCounterOverflowError for counter overflow", () => {
290
- assertErr(
291
- receiveTimestamp(deps0)(
292
- makeNode1Timestamp(0, maxCounter),
293
- makeNode2Timestamp(0, maxCounter),
530
+ it(`${operation} permits the maximum millis and throws on rollover beyond it`, () => {
531
+ const now = makeMillis(maxMillis - 1);
532
+ assertOk(
533
+ nextTimestamp(
534
+ createTimestamp({
535
+ millis: makeMillis(maxMillis - 1),
536
+ counter: maxCounter,
537
+ }),
538
+ now,
294
539
  ),
295
- { type: "TimestampCounterOverflowError" },
540
+ createTimestamp({ millis: maxMillis }),
296
541
  );
542
+ assertThrowsInstanceOf(() => {
543
+ nextTimestamp(
544
+ createTimestamp({ millis: maxMillis, counter: maxCounter }),
545
+ now,
546
+ );
547
+ }, Error);
297
548
  });
549
+ }
550
+
551
+ it("exposes the failures of each operation", () => {
552
+ assertType<
553
+ ReturnType<ReturnType<typeof sendTimestamp>>,
554
+ Result<Timestamp, TimestampDriftError>
555
+ >();
556
+ assertType<
557
+ ReturnType<ReturnType<typeof receiveTimestamp>>,
558
+ Result<Timestamp, TimestampDriftError>
559
+ >();
560
+ assertType<Parameters<typeof sendTimestamp>, [TimestampConfigDep]>();
561
+ assertType<
562
+ Parameters<ReturnType<typeof sendTimestamp>>,
563
+ [Timestamp, Millis]
564
+ >();
565
+ assertType<Parameters<typeof receiveTimestamp>, [TimestampConfigDep]>();
566
+ assertType<
567
+ Parameters<ReturnType<typeof receiveTimestamp>>,
568
+ [Timestamp, Timestamp, Millis]
569
+ >();
570
+ // @ts-expect-error TimestampCounterOverflowError is no longer exported; counters roll over.
571
+ type _RemovedError = import("../index.ts").TimestampCounterOverflowError;
298
572
  });
299
573
  });
300
574