@spooky-sync/core 0.0.1-canary.340 → 0.0.1-canary.341

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
@@ -1236,6 +1236,8 @@ interface ServiceCalls {
1236
1236
  'auth.currentUser': () => Record<string, unknown> | null;
1237
1237
  'auth.consumeEndedImpersonation': () => string | null;
1238
1238
  'remote.connect': () => Promise<void>;
1239
+ /** The platform reports no network at all (a browser's `navigator.onLine === false`). */
1240
+ 'net.offline': () => boolean;
1239
1241
  'remote.releaseViews': (ids: unknown[]) => void;
1240
1242
  'supervisor.start': () => void;
1241
1243
  'tabs.start': (bucketId: string) => Promise<'solo' | 'leader' | 'follower'>;
package/dist/index.js CHANGED
@@ -2286,8 +2286,8 @@ function trimHistory(history, cap) {
2286
2286
 
2287
2287
  //#endregion
2288
2288
  //#region src/modules/devtools/index.ts
2289
- const CORE_VERSION = "0.0.1-canary.340";
2290
- const WASM_VERSION = "0.0.1-canary.340";
2289
+ const CORE_VERSION = "0.0.1-canary.341";
2290
+ const WASM_VERSION = "0.0.1-canary.341";
2291
2291
  const SURREAL_VERSION = "3.1.0";
2292
2292
  var DevToolsService = class DevToolsService {
2293
2293
  version = CORE_VERSION;
@@ -6327,7 +6327,11 @@ var PreloadFailedError = class extends Error {
6327
6327
  * A session that cannot be sent membership (signed out, without anonymous
6328
6328
  * live queries) does not wait either: a view with rows would never settle,
6329
6329
  * and an app that gates its first paint on a preload showed its loading
6330
- * screen forever, signed-out landing page included.
6330
+ * screen forever, signed-out landing page included. Nor does an offline
6331
+ * device: the membership it waits for cannot arrive before the network does,
6332
+ * and an app reloaded offline before its preload ever settled stayed on its
6333
+ * loading screen. It paints from what is local instead, and the query fills in
6334
+ * once the registration lands.
6331
6335
  */
6332
6336
  function* preload(env, input) {
6333
6337
  const hash = yield* registerLocal(env, input);
@@ -6336,6 +6340,10 @@ function* preload(env, input) {
6336
6340
  hash,
6337
6341
  waited: false
6338
6342
  };
6343
+ if (yield fx.service("net.offline")) return {
6344
+ hash,
6345
+ waited: false
6346
+ };
6339
6347
  yield fx.state.wait((s) => settled(s, hash) || settleFailed(s, hash));
6340
6348
  if (yield fx.state.read((s) => settleFailed(s, hash))) throw new PreloadFailedError(hash);
6341
6349
  return {
@@ -15889,7 +15897,7 @@ function createTabsCoordinator(config, s, host) {
15889
15897
  return new TabsCoordinator({
15890
15898
  tabId: s.tabId,
15891
15899
  fingerprint: computeTabsFingerprint({
15892
- coreVersion: "0.0.1-canary.340",
15900
+ coreVersion: "0.0.1-canary.341",
15893
15901
  schemaHash: hash53(config.schemaSurql),
15894
15902
  endpoint: config.database.endpoint ?? "",
15895
15903
  namespace: config.database.namespace,
@@ -16054,6 +16062,7 @@ function createAdapters(config, s, host, lateModules) {
16054
16062
  if (sql) s.remote.beaconSql(sql);
16055
16063
  },
16056
16064
  "supervisor.start": () => s.connectionSupervisor.start(),
16065
+ "net.offline": () => typeof navigator !== "undefined" && navigator.onLine === false,
16057
16066
  "tabs.start": (bucketId) => {
16058
16067
  if (!s.tabs) throw new Error("shared tabs unavailable");
16059
16068
  return s.tabs.start(bucketId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/core",
3
- "version": "0.0.1-canary.340",
3
+ "version": "0.0.1-canary.341",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -75,8 +75,8 @@
75
75
  }
76
76
  },
77
77
  "dependencies": {
78
- "@spooky-sync/query-builder": "0.0.1-canary.340",
79
- "@spooky-sync/ssp-wasm": "0.0.1-canary.340",
78
+ "@spooky-sync/query-builder": "0.0.1-canary.341",
79
+ "@spooky-sync/ssp-wasm": "0.0.1-canary.341",
80
80
  "@sqlite.org/sqlite-wasm": "3.53.0-build1",
81
81
  "@surrealdb/wasm": "^3.1.0",
82
82
  "@surrealdb/wasm-native": "^3.3.4",
@@ -9,6 +9,11 @@ import { preload, PreloadFailedError } from './preload.saga';
9
9
  const env = defaultEnv({ tables: [{ name: 'thing', columns: {} }] } as any);
10
10
  const input = { tableName: 'thing', surql: 'SELECT * FROM thing', params: {}, ttl: '10m' as const };
11
11
  const sspOk = () => ({ localArray: [], timings: { parseMs: 0, planMs: 0, snapshotMs: 0, wallMs: 0 } });
12
+ /** The `service` handler for an online device: `net.offline` answers false. */
13
+ const online = (offline = false) => (e: any) => {
14
+ if (e.name === 'net.offline') return offline;
15
+ throw new Error(`unexpected service ${e.name}`);
16
+ };
12
17
 
13
18
  describe('preload', () => {
14
19
  it('resolved before: returns without waiting and with no remote effect', async () => {
@@ -31,7 +36,7 @@ describe('preload', () => {
31
36
  };
32
37
  const out = await runPure(preload(env, input), {
33
38
  state: signedIn,
34
- handlers: { 'local.getById': () => null, 'ssp.register': sspOk, 'state.wait': (_e, ctx) => settleIt(ctx) },
39
+ handlers: { 'local.getById': () => null, 'ssp.register': sspOk, service: online(), 'state.wait': (_e, ctx) => settleIt(ctx) },
35
40
  });
36
41
  expect(out.result).toEqual({ hash, waited: true });
37
42
  const failed = runPure(preload(env, input), {
@@ -39,13 +44,14 @@ describe('preload', () => {
39
44
  handlers: {
40
45
  'local.getById': () => null,
41
46
  'ssp.register': sspOk,
47
+ service: online(),
42
48
  'state.wait': (_e, ctx) => void (ctx.state = R.applyLifecycle(hash, { type: 'remote-failed' })(ctx.state)),
43
49
  },
44
50
  });
45
51
  await expect(failed).rejects.toBeInstanceOf(PreloadFailedError);
46
52
  const gone = runPure(preload(env, input), {
47
53
  state: signedIn,
48
- handlers: { 'local.getById': () => null, 'ssp.register': sspOk, 'state.wait': (_e, ctx) => void (ctx.state = R.removeQuery(hash)(ctx.state)) },
54
+ handlers: { 'local.getById': () => null, 'ssp.register': sspOk, service: online(), 'state.wait': (_e, ctx) => void (ctx.state = R.removeQuery(hash)(ctx.state)) },
49
55
  });
50
56
  await expect(gone).rejects.toThrow(/could not settle/);
51
57
  const already = buildState([buildEntry({ def: { hash }, lifecycle: { phase: 'cached' } })]);
@@ -67,6 +73,7 @@ describe('preload', () => {
67
73
  handlers: {
68
74
  'local.getById': () => null,
69
75
  'ssp.register': sspOk,
76
+ service: online(),
70
77
  'state.wait': (_e, ctx) =>
71
78
  void (ctx.state = R.compose(
72
79
  R.commitMembership(hash, [['thing:1', 1]], true),
@@ -77,4 +84,13 @@ describe('preload', () => {
77
84
  });
78
85
  expect(anon.result).toEqual({ hash, waited: true });
79
86
  });
87
+ it('offline: registers but never waits, so a reload without a network paints from what is local', async () => {
88
+ const signedIn = buildState([], R.setIdentity({ userId: 'user:alice' }));
89
+ const out = await runPure(preload(env, input), {
90
+ state: signedIn,
91
+ handlers: { 'local.getById': () => null, 'ssp.register': sspOk, service: online(true) },
92
+ });
93
+ expect(out.result.waited).toBe(false);
94
+ expect(out.log.filter((e) => e.kind === 'state.wait')).toHaveLength(0);
95
+ });
80
96
  });
@@ -24,7 +24,11 @@ export class PreloadFailedError extends Error {
24
24
  * A session that cannot be sent membership (signed out, without anonymous
25
25
  * live queries) does not wait either: a view with rows would never settle,
26
26
  * and an app that gates its first paint on a preload showed its loading
27
- * screen forever, signed-out landing page included.
27
+ * screen forever, signed-out landing page included. Nor does an offline
28
+ * device: the membership it waits for cannot arrive before the network does,
29
+ * and an app reloaded offline before its preload ever settled stayed on its
30
+ * loading screen. It paints from what is local instead, and the query fills in
31
+ * once the registration lands.
28
32
  */
29
33
  export function* preload(env: SagaEnv, input: RegisterInput): Saga<{ hash: QueryHash; waited: boolean }> {
30
34
  const hash = yield* registerLocal(env, input);
@@ -33,6 +37,7 @@ export function* preload(env: SagaEnv, input: RegisterInput): Saga<{ hash: Query
33
37
  boolean,
34
38
  ];
35
39
  if (phase !== 'cold' || !reachable) return { hash, waited: false };
40
+ if ((yield fx.service('net.offline')) as boolean) return { hash, waited: false };
36
41
  yield fx.state.wait((s) => settled(s, hash) || settleFailed(s, hash));
37
42
  const failed = (yield fx.state.read((s) => settleFailed(s, hash))) as boolean;
38
43
  if (failed) throw new PreloadFailedError(hash);
@@ -331,6 +331,7 @@ export function createAdapters<S extends SchemaStructure>(config: Sp00kyConfig<S
331
331
  if (sql) s.remote.beaconSql(sql);
332
332
  },
333
333
  'supervisor.start': () => s.connectionSupervisor.start(),
334
+ 'net.offline': () => typeof navigator !== 'undefined' && navigator.onLine === false,
334
335
  'tabs.start': (bucketId) => {
335
336
  if (!s.tabs) throw new Error('shared tabs unavailable');
336
337
  return s.tabs.start(bucketId);
@@ -100,6 +100,8 @@ export interface ServiceCalls {
100
100
  'auth.currentUser': () => Record<string, unknown> | null;
101
101
  'auth.consumeEndedImpersonation': () => string | null;
102
102
  'remote.connect': () => Promise<void>;
103
+ /** The platform reports no network at all (a browser's `navigator.onLine === false`). */
104
+ 'net.offline': () => boolean;
103
105
  'remote.releaseViews': (ids: unknown[]) => void;
104
106
  'supervisor.start': () => void;
105
107
  'tabs.start': (bucketId: string) => Promise<'solo' | 'leader' | 'follower'>;