@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
- static SETTLED_WRITE_GRACE_MS = 1e4;
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
- this.listRefIdleStreak = changed ? 0 : this.listRefIdleStreak + 1;
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.222";
7747
- const WASM_VERSION = "0.0.1-canary.222";
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.222",
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.222",
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.222",
64
- "@spooky-sync/ssp-wasm": "0.0.1-canary.222",
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 = 10_000;
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
+ });
@@ -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
- this.listRefIdleStreak = changed ? 0 : this.listRefIdleStreak + 1;
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,