@spooky-sync/core 0.0.1-canary.280 → 0.0.1-canary.282

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.
@@ -24,7 +24,7 @@ export function fakeServiceBundle<S extends SchemaStructure>(over: Partial<Servi
24
24
  streamProcessor: { addReceiver: (r: unknown) => void receivers.push(r), checkpoint: async () => undefined, dispose: noop } as any,
25
25
  migrator: {} as any,
26
26
  crdt: { closeAll: noop, dispose: noop, open: async () => 'field', close: noop } as any,
27
- auth: { subscribe: (cb: (u: string | null) => void) => (authListeners.push(cb), noop), subscribeImpersonation: () => noop, currentUser: null, isAuthenticated: false, impersonation: null, eventSystem: { subscribe: noop } } as any,
27
+ auth: { subscribe: (cb: (u: string | null) => void) => (authListeners.push(cb), noop), subscribeImpersonation: () => noop, onBeforeSignOut: () => noop, currentUser: null, isAuthenticated: false, token: null, impersonation: null, eventSystem: { subscribe: noop } } as any,
28
28
  tabs: null,
29
29
  tabsUnsupportedReason: 'test',
30
30
  tabId: 'tab-test',
package/src/types.ts CHANGED
@@ -3,6 +3,7 @@ import type { RecordId, SchemaStructure, QueryPlan } from '@spooky-sync/query-bu
3
3
  import type { Level, LoggerOptions } from 'pino';
4
4
  import type { PushEventOptions } from './events/index';
5
5
  import type { LocalEngineChoice } from './services/database/cache-engine';
6
+ import type { WebPushConfig } from './modules/web-push/index';
6
7
 
7
8
  export type { Level };
8
9
 
@@ -384,6 +385,16 @@ export interface Sp00kyConfig<S extends SchemaStructure> {
384
385
  * Defaults to 30000; `0` disables the timeout.
385
386
  */
386
387
  downTimeoutMs?: number;
388
+ /**
389
+ * Web Push on this page (`client.webPush`, `db.webPush`). Everything is
390
+ * optional and the defaults need no service worker configuration here:
391
+ * `autoSync` (repair this browser's subscription after sign-in),
392
+ * `unsubscribeOnSignOut` (drop this device's subscription on `signOut()`),
393
+ * `bridge` (post the session to the service worker for live rendering) are
394
+ * on; `serviceWorkerUrl` registers a worker when the app does not.
395
+ * See the `push-notifications` docs page.
396
+ */
397
+ webPush?: WebPushConfig;
387
398
  }
388
399
 
389
400
  /** Tunables for sync-health reporting. See {@link Sp00kyConfig.syncHealth}. */
@@ -0,0 +1,39 @@
1
+ import { RecordId } from 'surrealdb';
2
+ import type { InlineRow } from '../types';
3
+ import { encodeRecordId } from './index';
4
+
5
+ /**
6
+ * Reading `_00_list_ref` edges as a LIVE notification delivers them. Pure, so
7
+ * both the full client and the wasm-free live feed (`@spooky-sync/core/live`)
8
+ * decode the same wire shape the same way.
9
+ */
10
+
11
+ /** The hash a `_00_list_ref` edge belongs to: the id part of its `in` (`_00_query:<hash>`). */
12
+ export function hashOfEdge(value: unknown): string | null {
13
+ const inId = (value as { in?: unknown } | null)?.in;
14
+ if (!inId) return null;
15
+ const str = inId instanceof RecordId ? String(inId.id) : String(inId).replace(/^_00_query:/, '');
16
+ return str.length > 0 ? str : null;
17
+ }
18
+
19
+ /**
20
+ * The row an edge notification carries, when the subscription joined it on.
21
+ *
22
+ * Without `FETCH out` the edge's `out` is a record id and there is nothing to
23
+ * land, so this returns null and the caller falls back to fetching the body.
24
+ * It also returns null for a row the session may not read (the join yields
25
+ * `out: null`) and for an edge whose target has been deleted.
26
+ */
27
+ export function rowOfEdge(value: unknown): InlineRow | null {
28
+ const edge = value as { out?: unknown; version?: unknown } | null;
29
+ const out = edge?.out;
30
+ if (!out || typeof out !== 'object' || Array.isArray(out) || out instanceof RecordId) return null;
31
+ // Must be a real RecordId: the landing path keys off `row.id` being one, and
32
+ // would otherwise record the version for a body it never wrote.
33
+ const rid = (out as { id?: unknown }).id;
34
+ if (!(rid instanceof RecordId)) return null;
35
+ const id = encodeRecordId(rid);
36
+ const version = typeof edge?.version === 'number' ? edge.version : null;
37
+ if (version === null) return null;
38
+ return { id, version, record: out as Record<string, unknown> };
39
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Lowercase hex SHA-256 of a UTF-8 string, through WebCrypto.
3
+ *
4
+ * Pure and dependency free so it can live in `@spooky-sync/core/pure` and run
5
+ * in a service worker, a dedicated worker or Node (`globalThis.crypto` is
6
+ * there on Node 19+). Query keys are `sha256Hex(JSON.stringify(..))`, see
7
+ * `query/hash.ts`.
8
+ */
9
+ export async function sha256Hex(input: string): Promise<string> {
10
+ const bytes = new TextEncoder().encode(input);
11
+ const digest = await crypto.subtle.digest('SHA-256', bytes);
12
+ return Array.from(new Uint8Array(digest))
13
+ .map((b) => b.toString(16).padStart(2, '0'))
14
+ .join('');
15
+ }
package/tsdown.config.ts CHANGED
@@ -70,7 +70,8 @@ const workerUrlPlugin = {
70
70
  },
71
71
  };
72
72
 
73
- export default defineConfig({
73
+ export default defineConfig([
74
+ {
74
75
  // `sqlite-worker` is emitted at `dist/sqlite-worker.js` (top level, NOT under
75
76
  // services/database) so the `new URL('./sqlite-worker.js', import.meta.url)`
76
77
  // in `SqliteCacheEngine` — which gets bundled into the flat `dist/index.js` —
@@ -91,4 +92,32 @@ export default defineConfig({
91
92
  clean: true,
92
93
  hash: false,
93
94
  plugins: [versionDefinePlugin, workerUrlPlugin],
94
- });
95
+ },
96
+ // The wasm-free entries a service worker, a dedicated worker or Node can
97
+ // import: `/pure` (protocol builders), `/live` (the live feed over the
98
+ // pure-JS `surrealdb` SDK) and `/sw` (push handlers). Built separately so
99
+ // they never share a chunk with `index` (whose graph holds the wasm engines)
100
+ // and so `dist/index.js` stays one flat file. They may share chunks among
101
+ // themselves; scripts/check-worker-entries.mjs walks those too and fails
102
+ // the build on any wasm/pino import or top-level window/document access.
103
+ {
104
+ entry: {
105
+ pure: 'src/pure.ts',
106
+ live: 'src/live/index.ts',
107
+ sw: 'src/sw/index.ts',
108
+ },
109
+ format: ['esm'],
110
+ dts: true,
111
+ clean: false,
112
+ hash: false,
113
+ // Own prefix so a chunk of this build never overwrites one of the first
114
+ // (both write `dist/`; `types.d.ts` collided).
115
+ // The entries' own declaration files arrive as chunks named `<entry>.d`
116
+ // and must keep their plain name.
117
+ outputOptions: {
118
+ chunkFileNames: (chunk: { name: string }) =>
119
+ ['pure.d', 'live.d', 'sw.d'].includes(chunk.name) ? '[name].js' : 'worker-[name].js',
120
+ },
121
+ plugins: [versionDefinePlugin],
122
+ },
123
+ ]);