powersync-bridge 0.0.1-alpha.1 → 0.0.1-alpha.2

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/README.md CHANGED
@@ -83,6 +83,34 @@ require a connection. Reconnection resumes the pending API range without clearin
83
83
  The lower-level `useRacedList`, `useInfiniteList`, `useOnDemandStream` and `useAutoSync` APIs
84
84
  remain available for specialized workflows. Ordinary paginated lists need only `useInfiniteQuery`.
85
85
 
86
+ ## Hidden screens and checkpoint bursts
87
+
88
+ Wrap a retained screen or tab in `BridgeActivity` and pass its existing visibility state:
89
+
90
+ ```tsx
91
+ import { BridgeActivity } from '@powersync-bridge/react';
92
+
93
+ <BridgeActivity active={isVisible}>
94
+ <ServicesScreen />
95
+ </BridgeActivity>
96
+ ```
97
+
98
+ This applies to `useInfiniteQuery` and `useRacedList`. Pausing stops their SQLite watches,
99
+ checkpoint reads and pending API requests while keeping the last result and loaded page window.
100
+ Returning refreshes the local baseline, including rows changed or deleted while hidden.
101
+ An active consumer elsewhere keeps a shared query running. Nested boundaries cannot reactivate
102
+ an inactive parent. Identity, permissions and query-key changes still clear the outgoing scope.
103
+ Use `enabled` for query eligibility and `BridgeActivity` for temporary screen visibility.
104
+ On-demand sync streams and uploads continue independently.
105
+
106
+ Checkpoint refreshes are single-flight per observer. A synchronous burst produces one read;
107
+ changes during that read produce one follow-up, and superseded results cannot claim freshness.
108
+ Checkpoint maintenance shares one background-read slot across the bridge; first reads and
109
+ pagination bypass that queue. Queued maintenance is cancelled when its observer pauses.
110
+ Unscoped queries refresh only for global checkpoints. A query declaring `stream` also refreshes
111
+ for its own stream, rather than every unrelated stream. Closing a watch cancels its queued
112
+ follow-up. Applications need no checkpoint debounce, refresh queue or cache workaround.
113
+
86
114
  ## What your backend must change
87
115
 
88
116
  **Nothing.** No new tables, no new columns, no change to existing sync streams. The one rule is an
@@ -81,7 +81,6 @@ var FreshnessTracker = class {
81
81
  lastHasSynced = void 0;
82
82
  streamFreshAt = /* @__PURE__ */ new Map();
83
83
  streamSeenSyncedAt = /* @__PURE__ */ new Map();
84
- completions = [];
85
84
  completionListeners = /* @__PURE__ */ new Set();
86
85
  clearListeners = /* @__PURE__ */ new Set();
87
86
  unsubscribe = null;
@@ -115,7 +114,6 @@ var FreshnessTracker = class {
115
114
  this.lastSeenSyncedAt = null;
116
115
  this.streamFreshAt.clear();
117
116
  this.streamSeenSyncedAt.clear();
118
- this.completions.length = 0;
119
117
  }
120
118
  get sessionConnectAtMs() {
121
119
  return this.sessionConnectAt;
@@ -188,11 +186,10 @@ var FreshnessTracker = class {
188
186
  this.prevCompletionAt = now;
189
187
  this.lastDownloadStart = null;
190
188
  const c = { stamp: this.localFreshAt, completedAt: now, stream };
191
- this.completions.push(c);
192
189
  for (const l of this.completionListeners) l(c);
193
190
  }
194
191
  };
195
192
 
196
193
  export { FreshnessTracker, cursorOfRow, decodeCursor, encodeCursor, idOf, isAfterCursor, keyColumns, rowComparator, streamKey };
197
- //# sourceMappingURL=chunk-KAR4QIIU.js.map
198
- //# sourceMappingURL=chunk-KAR4QIIU.js.map
194
+ //# sourceMappingURL=chunk-WJDQWVO5.js.map
195
+ //# sourceMappingURL=chunk-WJDQWVO5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/keyset.ts","../src/freshness.ts"],"names":[],"mappings":"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","file":"chunk-WJDQWVO5.js","sourcesContent":["import type { CursorOf, KeyOf, OpaqueCursor, OrderKey, SortDirection } from './types';\n\n/** `id` is always the implicit tiebreaker (§8.3): a scalar key K is treated as [K, 'id']. */\nexport function keyColumns<T>(orderKey: OrderKey<T>): [KeyOf<T>, KeyOf<T>] {\n if (typeof orderKey === 'string') return [orderKey, 'id' as KeyOf<T>];\n return [orderKey[0], orderKey[1]];\n}\n\nexport function idOf(row: unknown): string {\n const r = row as { id?: unknown };\n if (r == null || r.id === undefined || r.id === null) {\n throw new Error('powersync-bridge: every row must have an `id`');\n }\n return String(r.id);\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a === b) return 0;\n if (a === null || a === undefined) return 1; // nulls last\n if (b === null || b === undefined) return -1;\n if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();\n if (typeof a === 'number' && typeof b === 'number') return a - b;\n if (typeof a === 'boolean' && typeof b === 'boolean') return Number(a) - Number(b);\n const sa = String(a);\n const sb = String(b);\n return sa < sb ? -1 : sa > sb ? 1 : 0;\n}\n\n/** Comparator over rows for the declared order key + direction. Byte order for strings (SQLite BINARY). */\nexport function rowComparator<T>(orderKey: OrderKey<T>, direction: SortDirection = 'asc'): (a: T, b: T) => number {\n const [k1, k2] = keyColumns(orderKey);\n const sign = direction === 'desc' ? -1 : 1;\n return (a, b) => {\n const c1 = compareValues((a as Record<string, unknown>)[k1], (b as Record<string, unknown>)[k1]);\n if (c1 !== 0) return c1 * sign;\n return compareValues((a as Record<string, unknown>)[k2], (b as Record<string, unknown>)[k2]) * sign;\n };\n}\n\n/** The cursor a row yields, in the definition's cursor shape. */\nexport function cursorOfRow<T, K extends OrderKey<T>>(row: T, orderKey: K): CursorOf<T, K> {\n const r = row as Record<string, unknown>;\n if (typeof orderKey === 'string') return r[orderKey] as CursorOf<T, K>;\n return [r[orderKey[0]], r[orderKey[1]]] as CursorOf<T, K>;\n}\n\n/** Whether `row` sorts strictly after `cursor` in the list's order (i.e. is \"beyond\" it). */\nexport function isAfterCursor<T, K extends OrderKey<T>>(\n row: T,\n cursor: CursorOf<T, K>,\n orderKey: K,\n direction: SortDirection = 'asc',\n): boolean {\n const [k1, k2] = keyColumns(orderKey);\n const r = row as Record<string, unknown>;\n const sign = direction === 'desc' ? -1 : 1;\n const [c1, c2] = typeof orderKey === 'string' ? [cursor, undefined] : (cursor as [unknown, unknown]);\n const d1 = compareValues(r[k1], c1) * sign;\n if (d1 !== 0) return d1 > 0;\n if (c2 === undefined) return false;\n return compareValues(r[k2], c2) * sign > 0;\n}\n\nfunction toBase64(s: string): string {\n if (typeof btoa === 'function') return btoa(unescape(encodeURIComponent(s)));\n return Buffer.from(s, 'utf8').toString('base64');\n}\n\nfunction fromBase64(s: string): string {\n if (typeof atob === 'function') return decodeURIComponent(escape(atob(s)));\n return Buffer.from(s, 'base64').toString('utf8');\n}\n\nexport function encodeCursor(cursor: unknown): OpaqueCursor {\n return toBase64(JSON.stringify(cursor)) as OpaqueCursor;\n}\n\nexport function decodeCursor<C>(opaque: OpaqueCursor): C {\n return JSON.parse(fromBase64(opaque)) as C;\n}\n","import type { BridgeDriver, StreamDescription, SyncStatusView } from './types';\n\nexport type Clock = () => number;\n\nexport interface CheckpointCompletion {\n /** Lower-bound stamp of the checkpoint's data position (download start), ms. */\n readonly stamp: number;\n readonly completedAt: number;\n /** Stream this completion was observed on; null for the global status. */\n readonly stream: string | null;\n}\n\nexport function streamKey(desc: StreamDescription): string {\n return `${desc.name}:${desc.params ? JSON.stringify(desc.params) : ''}`;\n}\n\n/**\n * Session-scoped, monotonic, transition-independent freshness (§5.2, R3/R5).\n *\n * stamp(checkpoint_n) = max(observed download start ≤ its completion,\n * completion time of checkpoint n−1,\n * sessionConnectAt when no other bound exists)\n * localFreshAt = max(previous, stamp) — a null observation never lowers it.\n */\nexport class FreshnessTracker {\n private sessionConnectAt: number | null = null;\n private localFreshAt = Number.NEGATIVE_INFINITY;\n private prevCompletionAt: number | null = null;\n private lastDownloadStart: number | null = null;\n private lastSeenSyncedAt: number | null = null;\n private lastDownloading = false;\n private lastConnected = false;\n private lastHasSynced: boolean | undefined = undefined;\n private readonly streamFreshAt = new Map<string, number>();\n private readonly streamSeenSyncedAt = new Map<string, number>();\n private readonly completionListeners = new Set<(c: CheckpointCompletion) => void>();\n private readonly clearListeners = new Set<() => void>();\n private unsubscribe: (() => void) | null = null;\n\n constructor(\n private readonly driver: BridgeDriver,\n private readonly clock: Clock,\n ) {}\n\n /** Call once at engine init and again after every identity swap. */\n start(epochStartedAt: number): void {\n this.stop();\n this.reset();\n const status = this.driver.syncStatus();\n this.lastConnected = status.connected;\n this.lastDownloading = status.downloading;\n this.lastHasSynced = status.hasSynced;\n this.lastSeenSyncedAt = status.lastSyncedAt?.getTime() ?? null;\n if (status.connected) this.sessionConnectAt = epochStartedAt;\n // Boot mid-download: provisional bound for the FIRST completion, never applied to localFreshAt now.\n if (status.downloading) {\n this.prevCompletionAt = Math.max(epochStartedAt, this.lastSeenSyncedAt ?? Number.NEGATIVE_INFINITY);\n }\n for (const s of status.syncStreams) {\n if (s.lastSyncedAt) this.streamSeenSyncedAt.set(streamKey(s), s.lastSyncedAt.getTime());\n }\n this.unsubscribe = this.driver.onStatusChanged(() => this.onStatus(this.driver.syncStatus()));\n }\n\n stop(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n\n private reset(): void {\n this.sessionConnectAt = null;\n this.localFreshAt = Number.NEGATIVE_INFINITY;\n this.prevCompletionAt = null;\n this.lastDownloadStart = null;\n this.lastSeenSyncedAt = null;\n this.streamFreshAt.clear();\n this.streamSeenSyncedAt.clear();\n }\n\n get sessionConnectAtMs(): number | null {\n return this.sessionConnectAt;\n }\n\n get localFreshAtMs(): number {\n return this.localFreshAt;\n }\n\n streamFreshAtMs(desc: StreamDescription): number {\n return this.streamFreshAt.get(streamKey(desc)) ?? Number.NEGATIVE_INFINITY;\n }\n\n /** Freshness for a list: the owning stream's stamp when declared, else the global stamp. */\n freshAtFor(stream: StreamDescription | undefined): number {\n if (!stream) return this.localFreshAt;\n return Math.max(this.streamFreshAtMs(stream), this.localFreshAt);\n }\n\n /** \"Answered\" = in-session freshness evidence (R5). */\n isFresh(stream: StreamDescription | undefined): boolean {\n if (this.sessionConnectAt === null) return false;\n return this.freshAtFor(stream) >= this.sessionConnectAt;\n }\n\n onCompletion(l: (c: CheckpointCompletion) => void): () => void {\n this.completionListeners.add(l);\n return () => this.completionListeners.delete(l);\n }\n\n onClear(l: () => void): () => void {\n this.clearListeners.add(l);\n return () => this.clearListeners.delete(l);\n }\n\n private onStatus(status: SyncStatusView): void {\n const now = this.clock();\n\n if (status.connected && !this.lastConnected && this.sessionConnectAt === null) {\n this.sessionConnectAt = now; // FIRST connect of the epoch — never per reconnect (R5)\n }\n this.lastConnected = status.connected;\n\n if (status.downloading && !this.lastDownloading) this.lastDownloadStart = now;\n this.lastDownloading = status.downloading;\n\n const syncedAt = status.lastSyncedAt?.getTime() ?? null;\n if (syncedAt !== null && syncedAt !== this.lastSeenSyncedAt) {\n this.recordCompletion(now, null);\n }\n if (syncedAt !== null) this.lastSeenSyncedAt = syncedAt; // null never lowers anything (R5)\n\n for (const s of status.syncStreams) {\n const key = streamKey(s);\n const at = s.lastSyncedAt?.getTime() ?? null;\n if (at !== null && at !== this.streamSeenSyncedAt.get(key)) {\n this.streamSeenSyncedAt.set(key, at);\n const stamp = this.stampFor(now);\n const prev = this.streamFreshAt.get(key) ?? Number.NEGATIVE_INFINITY;\n this.streamFreshAt.set(key, Math.max(prev, stamp));\n const c: CheckpointCompletion = { stamp: Math.max(prev, stamp), completedAt: now, stream: key };\n for (const l of this.completionListeners) l(c);\n }\n }\n\n if (this.lastHasSynced === true && status.hasSynced !== true) {\n for (const l of this.clearListeners) l();\n }\n this.lastHasSynced = status.hasSynced;\n }\n\n private stampFor(now: number): number {\n const candidates: number[] = [];\n if (this.lastDownloadStart !== null && this.lastDownloadStart <= now) candidates.push(this.lastDownloadStart);\n if (this.prevCompletionAt !== null) candidates.push(this.prevCompletionAt);\n if (candidates.length === 0 && this.sessionConnectAt !== null) candidates.push(this.sessionConnectAt);\n if (candidates.length === 0) return Number.NEGATIVE_INFINITY;\n return Math.max(...candidates);\n }\n\n private recordCompletion(now: number, stream: string | null): void {\n const stamp = this.stampFor(now);\n this.localFreshAt = Math.max(this.localFreshAt, stamp);\n this.prevCompletionAt = now;\n this.lastDownloadStart = null; // consumed; the next stamp derives from this completion\n const c: CheckpointCompletion = { stamp: this.localFreshAt, completedAt: now, stream };\n for (const l of this.completionListeners) l(c);\n }\n}\n"]}