@spooky-sync/core 0.0.1-canary.222 → 0.0.1-canary.223
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/index.d.ts
CHANGED
|
@@ -882,8 +882,21 @@ declare class DataModule<S extends SchemaStructure> {
|
|
|
882
882
|
* rolls the mutation back and never reports it settled, so it vanishes at
|
|
883
883
|
* once. This deadline only bounds the case where the write succeeded and its
|
|
884
884
|
* membership never arrived at all.
|
|
885
|
+
*
|
|
886
|
+
* 30 s, up from 10 s. At 10 s every server hiccup longer than a scheduler
|
|
887
|
+
* drain plus one slow database commit showed up as the user's own chat
|
|
888
|
+
* message vanishing and coming back a minute later. 30 s covers a drain and
|
|
889
|
+
* a stalled commit end to end; a membership that takes longer than that is
|
|
890
|
+
* a server fault the admin heartbeat reports, not something to paper over.
|
|
885
891
|
*/
|
|
886
892
|
private static readonly SETTLED_WRITE_GRACE_MS;
|
|
893
|
+
/**
|
|
894
|
+
* Whether any settled write or delete is still waiting for membership to
|
|
895
|
+
* catch up. The sync poll keeps its fast cadence while this is true, so a
|
|
896
|
+
* missed LIVE notification for the edge is picked up on the next poll tick
|
|
897
|
+
* rather than after the idle backoff.
|
|
898
|
+
*/
|
|
899
|
+
hasSettledWritesPending(): boolean;
|
|
887
900
|
/**
|
|
888
901
|
* Report that a mutation was accepted by the server and its outbox row
|
|
889
902
|
* removed. Called only on the SUCCESS path — a rolled-back mutation must
|
package/dist/index.js
CHANGED
|
@@ -3863,8 +3863,24 @@ var DataModule = class DataModule {
|
|
|
3863
3863
|
* rolls the mutation back and never reports it settled, so it vanishes at
|
|
3864
3864
|
* once. This deadline only bounds the case where the write succeeded and its
|
|
3865
3865
|
* membership never arrived at all.
|
|
3866
|
+
*
|
|
3867
|
+
* 30 s, up from 10 s. At 10 s every server hiccup longer than a scheduler
|
|
3868
|
+
* drain plus one slow database commit showed up as the user's own chat
|
|
3869
|
+
* message vanishing and coming back a minute later. 30 s covers a drain and
|
|
3870
|
+
* a stalled commit end to end; a membership that takes longer than that is
|
|
3871
|
+
* a server fault the admin heartbeat reports, not something to paper over.
|
|
3872
|
+
*/
|
|
3873
|
+
static SETTLED_WRITE_GRACE_MS = 3e4;
|
|
3874
|
+
/**
|
|
3875
|
+
* Whether any settled write or delete is still waiting for membership to
|
|
3876
|
+
* catch up. The sync poll keeps its fast cadence while this is true, so a
|
|
3877
|
+
* missed LIVE notification for the edge is picked up on the next poll tick
|
|
3878
|
+
* rather than after the idle backoff.
|
|
3866
3879
|
*/
|
|
3867
|
-
|
|
3880
|
+
hasSettledWritesPending() {
|
|
3881
|
+
this.pruneSettled(Date.now());
|
|
3882
|
+
return this.settledWrites.size > 0 || this.settledDeletes.size > 0;
|
|
3883
|
+
}
|
|
3868
3884
|
/**
|
|
3869
3885
|
* Report that a mutation was accepted by the server and its outbox row
|
|
3870
3886
|
* removed. Called only on the SUCCESS path — a rolled-back mutation must
|
|
@@ -6628,7 +6644,8 @@ var Sp00kySync = class Sp00kySync {
|
|
|
6628
6644
|
} finally {
|
|
6629
6645
|
this.listRefPollInFlight = null;
|
|
6630
6646
|
if (!this.listRefPollRunning) return;
|
|
6631
|
-
|
|
6647
|
+
const awaitingMembership = this.dataModule.hasSettledWritesPending();
|
|
6648
|
+
this.listRefIdleStreak = changed || awaitingMembership ? 0 : this.listRefIdleStreak + 1;
|
|
6632
6649
|
schedule(listRefPollDelayMs({
|
|
6633
6650
|
idleStreak: this.listRefIdleStreak,
|
|
6634
6651
|
baseIntervalMs: this.refSyncIntervalMs
|
|
@@ -7743,8 +7760,8 @@ function selfAllowlistedVariant(flag, userId) {
|
|
|
7743
7760
|
|
|
7744
7761
|
//#endregion
|
|
7745
7762
|
//#region src/modules/devtools/index.ts
|
|
7746
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
7747
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
7763
|
+
const CORE_VERSION = "0.0.1-canary.223";
|
|
7764
|
+
const WASM_VERSION = "0.0.1-canary.223";
|
|
7748
7765
|
const SURREAL_VERSION = "3.0.3";
|
|
7749
7766
|
var DevToolsService = class DevToolsService {
|
|
7750
7767
|
eventsHistory = [];
|
|
@@ -12759,7 +12776,7 @@ var Sp00kyClient = class {
|
|
|
12759
12776
|
return new TabsCoordinator({
|
|
12760
12777
|
tabId,
|
|
12761
12778
|
fingerprint: computeTabsFingerprint({
|
|
12762
|
-
coreVersion: "0.0.1-canary.
|
|
12779
|
+
coreVersion: "0.0.1-canary.223",
|
|
12763
12780
|
schemaHash: hash53(this.config.schemaSurql),
|
|
12764
12781
|
endpoint: this.config.database.endpoint ?? "",
|
|
12765
12782
|
namespace: this.config.database.namespace,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spooky-sync/core",
|
|
3
|
-
"version": "0.0.1-canary.
|
|
3
|
+
"version": "0.0.1-canary.223",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@spooky-sync/query-builder": "0.0.1-canary.
|
|
64
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
63
|
+
"@spooky-sync/query-builder": "0.0.1-canary.223",
|
|
64
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.223",
|
|
65
65
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
66
66
|
"@surrealdb/wasm": "^3.0.3",
|
|
67
67
|
"blurhash": "^2.0.5",
|
|
@@ -169,9 +169,38 @@ describe('settled-write grace window', () => {
|
|
|
169
169
|
});
|
|
170
170
|
dm.noteWriteSettled('comment:new', 'create');
|
|
171
171
|
expect(ids(await materialize(dm, state))).toContain('comment:new');
|
|
172
|
+
expect(dm.hasSettledWritesPending()).toBe(true);
|
|
172
173
|
|
|
174
|
+
// A scheduler drain plus one stalled database commit fit inside the
|
|
175
|
+
// window: at 11 s the row is still the user's own message, not a ghost.
|
|
173
176
|
vi.advanceTimersByTime(11_000);
|
|
177
|
+
expect(ids(await materialize(dm, state))).toContain('comment:new');
|
|
178
|
+
expect(dm.hasSettledWritesPending()).toBe(true);
|
|
179
|
+
|
|
180
|
+
vi.advanceTimersByTime(20_000);
|
|
174
181
|
expect(ids(await materialize(dm, state))).toEqual(['comment:old']);
|
|
182
|
+
expect(dm.hasSettledWritesPending()).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('reports no pending settled write once membership has caught up', async () => {
|
|
186
|
+
const { dm, state } = setup({
|
|
187
|
+
remoteArray: [['comment:old', 1]],
|
|
188
|
+
localArray: [
|
|
189
|
+
['comment:old', 1],
|
|
190
|
+
['comment:new', 1],
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
expect(dm.hasSettledWritesPending()).toBe(false);
|
|
194
|
+
|
|
195
|
+
dm.noteWriteSettled('comment:new', 'create');
|
|
196
|
+
expect(dm.hasSettledWritesPending()).toBe(true);
|
|
197
|
+
|
|
198
|
+
state.config.remoteArray = [
|
|
199
|
+
['comment:old', 1],
|
|
200
|
+
['comment:new', 1],
|
|
201
|
+
];
|
|
202
|
+
await materialize(dm, state);
|
|
203
|
+
expect(dm.hasSettledWritesPending()).toBe(false);
|
|
175
204
|
});
|
|
176
205
|
|
|
177
206
|
it('keeps an accepted delete subtracted until membership drops the row', async () => {
|
|
@@ -684,8 +684,25 @@ export class DataModule<S extends SchemaStructure> {
|
|
|
684
684
|
* rolls the mutation back and never reports it settled, so it vanishes at
|
|
685
685
|
* once. This deadline only bounds the case where the write succeeded and its
|
|
686
686
|
* membership never arrived at all.
|
|
687
|
+
*
|
|
688
|
+
* 30 s, up from 10 s. At 10 s every server hiccup longer than a scheduler
|
|
689
|
+
* drain plus one slow database commit showed up as the user's own chat
|
|
690
|
+
* message vanishing and coming back a minute later. 30 s covers a drain and
|
|
691
|
+
* a stalled commit end to end; a membership that takes longer than that is
|
|
692
|
+
* a server fault the admin heartbeat reports, not something to paper over.
|
|
687
693
|
*/
|
|
688
|
-
private static readonly SETTLED_WRITE_GRACE_MS =
|
|
694
|
+
private static readonly SETTLED_WRITE_GRACE_MS = 30_000;
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Whether any settled write or delete is still waiting for membership to
|
|
698
|
+
* catch up. The sync poll keeps its fast cadence while this is true, so a
|
|
699
|
+
* missed LIVE notification for the edge is picked up on the next poll tick
|
|
700
|
+
* rather than after the idle backoff.
|
|
701
|
+
*/
|
|
702
|
+
hasSettledWritesPending(): boolean {
|
|
703
|
+
this.pruneSettled(Date.now());
|
|
704
|
+
return this.settledWrites.size > 0 || this.settledDeletes.size > 0;
|
|
705
|
+
}
|
|
689
706
|
|
|
690
707
|
/**
|
|
691
708
|
* Report that a mutation was accepted by the server and its outbox row
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { Sp00kySync } from './sync';
|
|
3
|
+
|
|
4
|
+
// The list_ref poll backs off toward a 5s cap on a quiet page. A settled write
|
|
5
|
+
// that is still waiting for its membership edge must hold the poll at its base
|
|
6
|
+
// cadence instead: the row is rendered on the settled-write grace alone, so
|
|
7
|
+
// the edge has to be found on the first poll after it lands, not after the
|
|
8
|
+
// backoff has grown. Otherwise a missed LIVE notification during a server
|
|
9
|
+
// stall turns into "my message vanished" even though the edge arrived.
|
|
10
|
+
|
|
11
|
+
function makeSync(pending: () => boolean) {
|
|
12
|
+
const logger: any = {
|
|
13
|
+
child: () => logger,
|
|
14
|
+
debug: () => {},
|
|
15
|
+
info: () => {},
|
|
16
|
+
warn: () => {},
|
|
17
|
+
error: () => {},
|
|
18
|
+
trace: () => {},
|
|
19
|
+
};
|
|
20
|
+
const remote: any = { query: vi.fn().mockResolvedValue([true]) };
|
|
21
|
+
const dataModule: any = {
|
|
22
|
+
getActiveQueryHashes: () => ['h1'],
|
|
23
|
+
hasSettledWritesPending: pending,
|
|
24
|
+
};
|
|
25
|
+
const sync = new Sp00kySync({} as any, remote, {} as any, dataModule, {} as any, logger);
|
|
26
|
+
// A poll that never observes a change: the only thing driving the streak is
|
|
27
|
+
// the settled-write signal.
|
|
28
|
+
(sync as any).pollListRefForActiveQueries = vi.fn().mockResolvedValue(false);
|
|
29
|
+
return sync;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('list_ref poll cadence while a settled write awaits membership', () => {
|
|
33
|
+
afterEach(() => vi.useRealTimers());
|
|
34
|
+
|
|
35
|
+
it('stays at the base cadence while a write is pending, then backs off', async () => {
|
|
36
|
+
vi.useFakeTimers();
|
|
37
|
+
let pending = true;
|
|
38
|
+
const sync = makeSync(() => pending);
|
|
39
|
+
const base = sync.refSyncIntervalMs;
|
|
40
|
+
|
|
41
|
+
(sync as any).startListRefPoll();
|
|
42
|
+
|
|
43
|
+
// Three quiet ticks with a write outstanding: no backoff at all.
|
|
44
|
+
for (let i = 0; i < 3; i++) {
|
|
45
|
+
await vi.advanceTimersByTimeAsync(base);
|
|
46
|
+
expect((sync as any).listRefIdleStreak).toBe(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Membership caught up: the ordinary idle backoff resumes.
|
|
50
|
+
pending = false;
|
|
51
|
+
await vi.advanceTimersByTimeAsync(base);
|
|
52
|
+
expect((sync as any).listRefIdleStreak).toBe(1);
|
|
53
|
+
await vi.advanceTimersByTimeAsync(base * 2);
|
|
54
|
+
expect((sync as any).listRefIdleStreak).toBe(2);
|
|
55
|
+
|
|
56
|
+
(sync as any).stopListRefPoll();
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/modules/sync/sync.ts
CHANGED
|
@@ -877,8 +877,12 @@ export class Sp00kySync<S extends SchemaStructure> {
|
|
|
877
877
|
// Reset the idle streak on any observed change so the poll snaps
|
|
878
878
|
// back to the fast base cadence; otherwise grow it so a quiet page
|
|
879
879
|
// backs off toward the cap. (`handleRemoteListRefChange` also resets
|
|
880
|
-
// it when a LIVE event lands.)
|
|
881
|
-
|
|
880
|
+
// it when a LIVE event lands.) A settled write still waiting for its
|
|
881
|
+
// membership also pins the fast cadence: the row is on borrowed time
|
|
882
|
+
// (the settled-write grace), so the edge must be found on the first
|
|
883
|
+
// poll after it lands, not after the backoff has grown to the cap.
|
|
884
|
+
const awaitingMembership = this.dataModule.hasSettledWritesPending();
|
|
885
|
+
this.listRefIdleStreak = changed || awaitingMembership ? 0 : this.listRefIdleStreak + 1;
|
|
882
886
|
const next = listRefPollDelayMs({
|
|
883
887
|
idleStreak: this.listRefIdleStreak,
|
|
884
888
|
baseIntervalMs: this.refSyncIntervalMs,
|