@spooky-sync/core 0.0.1-canary.224 → 0.0.1-canary.226
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
|
@@ -814,7 +814,7 @@ declare class DataModule<S extends SchemaStructure> {
|
|
|
814
814
|
* No-op when nothing is pending. The pending entry is removed before the
|
|
815
815
|
* await so a concurrently-firing timer can't process it twice.
|
|
816
816
|
*/
|
|
817
|
-
flushPendingStreamUpdate(queryHash: string): Promise<
|
|
817
|
+
flushPendingStreamUpdate(queryHash: string): Promise<boolean>;
|
|
818
818
|
/**
|
|
819
819
|
* Materialize a query's result rows from the local store.
|
|
820
820
|
*
|
package/dist/index.js
CHANGED
|
@@ -3753,9 +3753,10 @@ var DataModule = class DataModule {
|
|
|
3753
3753
|
this.debounceTimers.delete(queryHash);
|
|
3754
3754
|
}
|
|
3755
3755
|
const pending = this.pendingStreamUpdates.get(queryHash);
|
|
3756
|
-
if (!pending) return;
|
|
3756
|
+
if (!pending) return false;
|
|
3757
3757
|
this.pendingStreamUpdates.delete(queryHash);
|
|
3758
3758
|
await this.processStreamUpdate(pending);
|
|
3759
|
+
return true;
|
|
3759
3760
|
}
|
|
3760
3761
|
/**
|
|
3761
3762
|
* Materialize a query's result rows from the local store.
|
|
@@ -7244,7 +7245,10 @@ var Sp00kySync = class Sp00kySync {
|
|
|
7244
7245
|
} finally {
|
|
7245
7246
|
if (fetching) {
|
|
7246
7247
|
try {
|
|
7247
|
-
await this.dataModule.flushPendingStreamUpdate(hash)
|
|
7248
|
+
if (!await this.dataModule.flushPendingStreamUpdate(hash)) {
|
|
7249
|
+
this.dataModule.scheduleRematerialize(hash);
|
|
7250
|
+
await this.dataModule.flushPendingStreamUpdate(hash);
|
|
7251
|
+
}
|
|
7248
7252
|
} catch (err) {
|
|
7249
7253
|
this.logger.warn({
|
|
7250
7254
|
err,
|
|
@@ -7760,8 +7764,8 @@ function selfAllowlistedVariant(flag, userId) {
|
|
|
7760
7764
|
|
|
7761
7765
|
//#endregion
|
|
7762
7766
|
//#region src/modules/devtools/index.ts
|
|
7763
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
7764
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
7767
|
+
const CORE_VERSION = "0.0.1-canary.226";
|
|
7768
|
+
const WASM_VERSION = "0.0.1-canary.226";
|
|
7765
7769
|
const SURREAL_VERSION = "3.0.3";
|
|
7766
7770
|
var DevToolsService = class DevToolsService {
|
|
7767
7771
|
eventsHistory = [];
|
|
@@ -12776,7 +12780,7 @@ var Sp00kyClient = class {
|
|
|
12776
12780
|
return new TabsCoordinator({
|
|
12777
12781
|
tabId,
|
|
12778
12782
|
fingerprint: computeTabsFingerprint({
|
|
12779
|
-
coreVersion: "0.0.1-canary.
|
|
12783
|
+
coreVersion: "0.0.1-canary.226",
|
|
12780
12784
|
schemaHash: hash53(this.config.schemaSurql),
|
|
12781
12785
|
endpoint: this.config.database.endpoint ?? "",
|
|
12782
12786
|
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.226",
|
|
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.226",
|
|
64
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.226",
|
|
65
65
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
66
66
|
"@surrealdb/wasm": "^3.0.3",
|
|
67
67
|
"blurhash": "^2.0.5",
|
|
@@ -492,16 +492,17 @@ export class DataModule<S extends SchemaStructure> {
|
|
|
492
492
|
* No-op when nothing is pending. The pending entry is removed before the
|
|
493
493
|
* await so a concurrently-firing timer can't process it twice.
|
|
494
494
|
*/
|
|
495
|
-
async flushPendingStreamUpdate(queryHash: string): Promise<
|
|
495
|
+
async flushPendingStreamUpdate(queryHash: string): Promise<boolean> {
|
|
496
496
|
const timer = this.debounceTimers.get(queryHash);
|
|
497
497
|
if (timer) {
|
|
498
498
|
clearTimeout(timer);
|
|
499
499
|
this.debounceTimers.delete(queryHash);
|
|
500
500
|
}
|
|
501
501
|
const pending = this.pendingStreamUpdates.get(queryHash);
|
|
502
|
-
if (!pending) return;
|
|
502
|
+
if (!pending) return false;
|
|
503
503
|
this.pendingStreamUpdates.delete(queryHash);
|
|
504
504
|
await this.processStreamUpdate(pending);
|
|
505
|
+
return true;
|
|
505
506
|
}
|
|
506
507
|
|
|
507
508
|
/**
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { RecordId } from 'surrealdb';
|
|
3
|
+
import { Sp00kySync } from './sync';
|
|
4
|
+
|
|
5
|
+
// A LIVE (or poll) UPDATE for a row already in membership fetches the new body
|
|
6
|
+
// into the local store — and that can be the end of it: the circuit sees the
|
|
7
|
+
// same id-set and emits no stream update, and nothing else re-reads the query.
|
|
8
|
+
// Measured on staging: a `conversation` row reached the store at `_00_rv` 8
|
|
9
|
+
// while the query kept rendering version 2 for minutes. `runSyncForQuery`
|
|
10
|
+
// must therefore land a synthetic re-materialize whenever a fetch produced
|
|
11
|
+
// no stream update of its own.
|
|
12
|
+
|
|
13
|
+
function makeSync(opts: { streamUpdatePending: boolean }) {
|
|
14
|
+
const logger: any = {
|
|
15
|
+
child: () => logger,
|
|
16
|
+
debug: () => {},
|
|
17
|
+
info: () => {},
|
|
18
|
+
warn: () => {},
|
|
19
|
+
error: () => {},
|
|
20
|
+
trace: () => {},
|
|
21
|
+
};
|
|
22
|
+
const queryState: any = {
|
|
23
|
+
config: {
|
|
24
|
+
id: new RecordId('_00_query', 'h1'),
|
|
25
|
+
// Membership already knows the newer version; the circuit still holds 1.
|
|
26
|
+
localArray: [['conversation:c1', 1]],
|
|
27
|
+
remoteArray: [['conversation:c1', 8]],
|
|
28
|
+
membershipKnown: true,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
const calls: string[] = [];
|
|
32
|
+
const dataModule: any = {
|
|
33
|
+
getQueryByHash: vi.fn().mockReturnValue(queryState),
|
|
34
|
+
getPendingRecordIds: async () => ({ writes: new Set(), deletes: new Set() }),
|
|
35
|
+
beginFetching: vi.fn(() => calls.push('beginFetching')),
|
|
36
|
+
endFetching: vi.fn(() => calls.push('endFetching')),
|
|
37
|
+
recordRemoteFetch: vi.fn(),
|
|
38
|
+
updateQueryLocalArray: vi.fn(),
|
|
39
|
+
scheduleRematerialize: vi.fn(() => calls.push('scheduleRematerialize')),
|
|
40
|
+
// First flush reports whether a real stream update was pending; a second
|
|
41
|
+
// flush (after the synthetic schedule) always lands something.
|
|
42
|
+
flushPendingStreamUpdate: vi
|
|
43
|
+
.fn()
|
|
44
|
+
.mockImplementationOnce(async () => {
|
|
45
|
+
calls.push('flush');
|
|
46
|
+
return opts.streamUpdatePending;
|
|
47
|
+
})
|
|
48
|
+
.mockImplementation(async () => {
|
|
49
|
+
calls.push('flush');
|
|
50
|
+
return true;
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
const sync = new Sp00kySync({} as any, { query: vi.fn() } as any, {} as any, dataModule, {} as any, logger);
|
|
54
|
+
(sync as any).syncEngine = {
|
|
55
|
+
syncRecords: vi.fn(async () => {
|
|
56
|
+
calls.push('syncRecords');
|
|
57
|
+
return { remoteFetchMs: 3, stillRemoteIds: [] };
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
const run = () =>
|
|
61
|
+
(sync as any).runSyncForQuery('h1', {
|
|
62
|
+
added: [],
|
|
63
|
+
updated: [{ id: 'conversation:c1', version: 8 }],
|
|
64
|
+
removed: [],
|
|
65
|
+
}) as Promise<void>;
|
|
66
|
+
return { run, dataModule, calls };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('runSyncForQuery re-materializes a fetched row the circuit stayed silent about', () => {
|
|
70
|
+
it('schedules and lands a synthetic re-materialize before going idle', async () => {
|
|
71
|
+
const { run, dataModule, calls } = makeSync({ streamUpdatePending: false });
|
|
72
|
+
await run();
|
|
73
|
+
expect(dataModule.scheduleRematerialize).toHaveBeenCalledWith('h1');
|
|
74
|
+
expect(dataModule.flushPendingStreamUpdate).toHaveBeenCalledTimes(2);
|
|
75
|
+
// The re-render lands BEFORE `endFetching`, so `idle` never races ahead
|
|
76
|
+
// of the rows it describes.
|
|
77
|
+
expect(calls).toEqual([
|
|
78
|
+
'beginFetching',
|
|
79
|
+
'syncRecords',
|
|
80
|
+
'flush',
|
|
81
|
+
'scheduleRematerialize',
|
|
82
|
+
'flush',
|
|
83
|
+
'endFetching',
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('leaves a real stream update alone', async () => {
|
|
88
|
+
const { run, dataModule } = makeSync({ streamUpdatePending: true });
|
|
89
|
+
await run();
|
|
90
|
+
expect(dataModule.scheduleRematerialize).not.toHaveBeenCalled();
|
|
91
|
+
expect(dataModule.flushPendingStreamUpdate).toHaveBeenCalledTimes(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/src/modules/sync/sync.ts
CHANGED
|
@@ -1803,8 +1803,21 @@ export class Sp00kySync<S extends SchemaStructure> {
|
|
|
1803
1803
|
// Land the coalesced result BEFORE flipping to idle: the final stream
|
|
1804
1804
|
// update sits on a debounce timer, and an `idle` that races ahead of it
|
|
1805
1805
|
// would let consumers treat a partially-filled window as authoritative.
|
|
1806
|
+
//
|
|
1807
|
+
// A fetch that changed a row already in membership can come with no
|
|
1808
|
+
// stream update at all: the body lands in the local store, but the
|
|
1809
|
+
// circuit sees the same id-set and stays silent (measured: a peer's
|
|
1810
|
+
// UPDATE to a `conversation` row reached the store at `_00_rv` 8 while
|
|
1811
|
+
// the query kept rendering version 2 indefinitely). Nothing else
|
|
1812
|
+
// re-reads the query then, so ask for a synthetic re-materialize and
|
|
1813
|
+
// land it here too. It is one local select, and a no-op when a real
|
|
1814
|
+
// update was pending, which the first flush already processed.
|
|
1806
1815
|
try {
|
|
1807
|
-
await this.dataModule.flushPendingStreamUpdate(hash);
|
|
1816
|
+
const landed = await this.dataModule.flushPendingStreamUpdate(hash);
|
|
1817
|
+
if (!landed) {
|
|
1818
|
+
this.dataModule.scheduleRematerialize(hash);
|
|
1819
|
+
await this.dataModule.flushPendingStreamUpdate(hash);
|
|
1820
|
+
}
|
|
1808
1821
|
} catch (err) {
|
|
1809
1822
|
this.logger.warn(
|
|
1810
1823
|
{ err, hash, Category: 'sp00ky-client::Sp00kySync::runSyncForQuery' },
|