@powersync-bridge/react 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
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ BridgeActivity: () => BridgeActivity,
23
24
  BridgeProvider: () => BridgeProvider,
24
25
  useAutoSync: () => useAutoSync,
25
26
  useBridge: () => useBridge,
@@ -32,6 +33,11 @@ module.exports = __toCommonJS(src_exports);
32
33
  var import_react = require("react");
33
34
  var import_powersync_bridge = require("powersync-bridge");
34
35
  var BridgeContext = (0, import_react.createContext)(null);
36
+ var BridgeActivityContext = (0, import_react.createContext)(true);
37
+ var BridgeActivity = ({ active, children }) => {
38
+ const parentActive = (0, import_react.useContext)(BridgeActivityContext);
39
+ return (0, import_react.createElement)(BridgeActivityContext.Provider, { value: parentActive && active }, children);
40
+ };
35
41
  function BridgeProvider({ bridge, children, online }) {
36
42
  (0, import_react.useEffect)(() => {
37
43
  bridge.setOnline(online);
@@ -40,22 +46,30 @@ function BridgeProvider({ bridge, children, online }) {
40
46
  }
41
47
  var useInfiniteQuery = (options) => {
42
48
  const bridge = useBridge();
49
+ const active = (0, import_react.useContext)(BridgeActivityContext);
43
50
  const epoch = useEpoch(bridge);
44
51
  const key = (0, import_powersync_bridge.infiniteQueryKey)(options);
45
52
  const controller = (0, import_react.useMemo)(() => bridge.query(options), [bridge, epoch, key]);
46
53
  (0, import_react.useEffect)(() => {
47
- controller.retain();
48
- return () => controller.release();
49
- }, [controller]);
50
- const result = (0, import_react.useSyncExternalStore)(controller.store.subscribe, controller.store.getSnapshot, controller.store.getSnapshot);
51
- return (0, import_react.useMemo)(() => ({
52
- ...result,
53
- fetchNextPage: controller.fetchNextPage,
54
- retry: controller.retry,
55
- refresh: controller.refresh,
56
- refetch: controller.refresh,
57
- continueOlder: controller.continueOlder
58
- }), [result, controller]);
54
+ controller.retain(active);
55
+ return () => controller.release(active);
56
+ }, [controller, active]);
57
+ const result = (0, import_react.useSyncExternalStore)(
58
+ controller.store.subscribe,
59
+ controller.store.getSnapshot,
60
+ controller.store.getSnapshot
61
+ );
62
+ return (0, import_react.useMemo)(
63
+ () => ({
64
+ ...result,
65
+ fetchNextPage: controller.fetchNextPage,
66
+ retry: controller.retry,
67
+ refresh: controller.refresh,
68
+ refetch: controller.refresh,
69
+ continueOlder: controller.continueOlder
70
+ }),
71
+ [result, controller]
72
+ );
59
73
  };
60
74
  function useBridge() {
61
75
  const b = (0, import_react.useContext)(BridgeContext);
@@ -63,21 +77,30 @@ function useBridge() {
63
77
  return b;
64
78
  }
65
79
  function useEpoch(bridge) {
66
- return (0, import_react.useSyncExternalStore)(bridge.epochStore.subscribe, bridge.epochStore.getSnapshot, bridge.epochStore.getSnapshot);
80
+ return (0, import_react.useSyncExternalStore)(
81
+ bridge.epochStore.subscribe,
82
+ bridge.epochStore.getSnapshot,
83
+ bridge.epochStore.getSnapshot
84
+ );
67
85
  }
68
- function useRetained(bridge, key, acquire) {
86
+ function useRetained(bridge, key, acquire, active = true) {
69
87
  const epoch = useEpoch(bridge);
70
88
  const holder = (0, import_react.useMemo)(() => acquire(), [bridge, epoch, key]);
71
89
  (0, import_react.useEffect)(() => {
72
- holder.retain();
73
- return () => holder.release();
74
- }, [holder]);
75
- return (0, import_react.useSyncExternalStore)(holder.store.subscribe, holder.store.getSnapshot, holder.store.getSnapshot);
90
+ holder.retain(active);
91
+ return () => holder.release(active);
92
+ }, [holder, active]);
93
+ return (0, import_react.useSyncExternalStore)(
94
+ holder.store.subscribe,
95
+ holder.store.getSnapshot,
96
+ holder.store.getSnapshot
97
+ );
76
98
  }
77
99
  function useRacedList(def, scope) {
78
100
  const bridge = useBridge();
101
+ const active = (0, import_react.useContext)(BridgeActivityContext);
79
102
  const key = `${def.id}:${JSON.stringify(scope ?? null)}`;
80
- return useRetained(bridge, key, () => bridge.raced(def, scope));
103
+ return useRetained(bridge, key, () => bridge.raced(def, scope), active);
81
104
  }
82
105
  function useInfiniteList(def, scope) {
83
106
  const bridge = useBridge();
@@ -88,7 +111,11 @@ function useInfiniteList(def, scope) {
88
111
  store.retain();
89
112
  return () => store.release();
90
113
  }, [store]);
91
- const result = (0, import_react.useSyncExternalStore)(store.store.subscribe, store.store.getSnapshot, store.store.getSnapshot);
114
+ const result = (0, import_react.useSyncExternalStore)(
115
+ store.store.subscribe,
116
+ store.store.getSnapshot,
117
+ store.store.getSnapshot
118
+ );
92
119
  return (0, import_react.useMemo)(
93
120
  () => ({ ...result, loadMore: () => void store.loadMore(), retry: () => void store.retry() }),
94
121
  [result, store]
@@ -106,6 +133,7 @@ function useAutoSync(config, ids) {
106
133
  }
107
134
  // Annotate the CommonJS export names for ESM import in node:
108
135
  0 && (module.exports = {
136
+ BridgeActivity,
109
137
  BridgeProvider,
110
138
  useAutoSync,
111
139
  useBridge,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { createContext, createElement, useContext, useEffect, useMemo, useSyncExternalStore, type ReactNode } from 'react';\nimport { infiniteQueryKey } from 'powersync-bridge';\nimport type {\n Bridge,\n FlaggerConfig,\n FlagResult,\n InfiniteListDefinition,\n InfiniteListResult,\n InfiniteQueryOptions,\n InfiniteQueryResult,\n OnDemandOptions,\n OnDemandResult,\n OrderKey,\n Params,\n RacedListDefinition,\n RacedListResult,\n Store,\n} from 'powersync-bridge';\n\n// NOTE (R11, static assertion): this package never imports `useQuery` from TanStack.\n// It renders from the engine store via useSyncExternalStore only.\n\nconst BridgeContext = createContext<Bridge | null>(null);\n\nexport interface BridgeProviderProps {\n bridge: Bridge;\n children?: ReactNode;\n /** Native apps supply their network hook's result once, at the provider. */\n online?: boolean | undefined;\n}\n\n/** Mount once at app root. Crud-delta observation started at createBridge(), not here (F6-001). */\nexport function BridgeProvider({ bridge, children, online }: BridgeProviderProps) {\n useEffect(() => { bridge.setOnline(online); }, [bridge, online]);\n return createElement(BridgeContext.Provider, { value: bridge }, children);\n}\n\nexport type UseInfiniteQueryResult<T> = InfiniteQueryResult<T> & {\n fetchNextPage(): void;\n retry(): void;\n refresh(): void;\n refetch(): void;\n continueOlder(): void;\n};\n\n/** Define local SQL and an API page; the bridge owns all paging and reconciliation. */\nexport const useInfiniteQuery = <T extends { id: string }>(\n options: InfiniteQueryOptions<T>,\n): UseInfiniteQueryResult<T> => {\n const bridge = useBridge();\n const epoch = useEpoch(bridge);\n const key = infiniteQueryKey(options);\n // Definitions are closures; every changing predicate belongs in queryKey.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const controller = useMemo(() => bridge.query(options), [bridge, epoch, key]);\n useEffect(() => {\n controller.retain();\n return () => controller.release();\n }, [controller]);\n const result = useSyncExternalStore(controller.store.subscribe, controller.store.getSnapshot, controller.store.getSnapshot);\n return useMemo(() => ({\n ...result, fetchNextPage: controller.fetchNextPage, retry: controller.retry,\n refresh: controller.refresh, refetch: controller.refresh, continueOlder: controller.continueOlder,\n }), [result, controller]);\n};\n\nexport function useBridge(): Bridge {\n const b = useContext(BridgeContext);\n if (!b) throw new Error('powersync-bridge: no <BridgeProvider> above this component');\n return b;\n}\n\nfunction useEpoch(bridge: Bridge): number {\n return useSyncExternalStore(bridge.epochStore.subscribe, bridge.epochStore.getSnapshot, bridge.epochStore.getSnapshot);\n}\n\ninterface Retainable<S> {\n readonly store: Store<S>;\n retain(): void;\n release(): void;\n}\n\n/**\n * Acquire a ref-counted store for the current epoch and subscribe to it. Retain/release run in\n * an effect so StrictMode's double-invoke is a no-op pair (R10); the store is shared across\n * components that ask for the same definition + scope.\n */\nfunction useRetained<S>(bridge: Bridge, key: string, acquire: () => Retainable<S>): S {\n const epoch = useEpoch(bridge);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const holder = useMemo(() => acquire(), [bridge, epoch, key]);\n useEffect(() => {\n holder.retain();\n return () => holder.release();\n }, [holder]);\n return useSyncExternalStore(holder.store.subscribe, holder.store.getSnapshot, holder.store.getSnapshot);\n}\n\nexport function useRacedList<TItem, TScope, TApi, K extends OrderKey<TItem>>(\n def: RacedListDefinition<TItem, TScope, TApi, K>,\n scope: TScope,\n): RacedListResult<TItem> {\n const bridge = useBridge();\n const key = `${def.id}:${JSON.stringify(scope ?? null)}`;\n return useRetained(bridge, key, () => bridge.raced(def, scope));\n}\n\nexport interface UseInfiniteListResult<TItem> extends InfiniteListResult<TItem> {\n loadMore(): void;\n /** Clears API backoff and loads (R16). */\n retry(): void;\n}\n\nexport function useInfiniteList<TItem, TScope, K extends OrderKey<TItem>, TCursor, TPartition>(\n def: InfiniteListDefinition<TItem, TScope, K, TCursor, TPartition>,\n scope: TScope,\n): UseInfiniteListResult<TItem> {\n const bridge = useBridge();\n const epoch = useEpoch(bridge);\n const key = `${def.id}:${JSON.stringify(scope ?? null)}`;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const store = useMemo(() => bridge.infinite(def, scope), [bridge, epoch, key]);\n useEffect(() => {\n store.retain();\n return () => store.release();\n }, [store]);\n const result = useSyncExternalStore(store.store.subscribe, store.store.getSnapshot, store.store.getSnapshot);\n return useMemo(\n () => ({ ...result, loadMore: () => void store.loadMore(), retry: () => void store.retry() }),\n [result, store],\n );\n}\n\n/** The default \"pull one row into the offline DB\" primitive (§7). */\nexport function useOnDemandStream(name: string, params: Params | null, opts: OnDemandOptions = {}): OnDemandResult {\n const bridge = useBridge();\n const key = `${name}:${JSON.stringify(params)}:${JSON.stringify(opts)}`;\n return useRetained(bridge, key, () => bridge.onDemand(name, params, opts));\n}\n\n/** OPTIONAL: server-side flagging (§7). Prefer useOnDemandStream. */\nexport function useAutoSync(config: FlaggerConfig, ids: readonly string[]): FlagResult {\n const bridge = useBridge();\n const key = `${config.stream}:${JSON.stringify([...ids].sort())}`;\n return useRetained(bridge, key, () => bridge.autoSync(config, ids));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n createContext,\n createElement,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from 'react';\nimport { infiniteQueryKey } from 'powersync-bridge';\nimport type {\n Bridge,\n FlaggerConfig,\n FlagResult,\n InfiniteListDefinition,\n InfiniteListResult,\n InfiniteQueryOptions,\n InfiniteQueryResult,\n OnDemandOptions,\n OnDemandResult,\n OrderKey,\n Params,\n RacedListDefinition,\n RacedListResult,\n Store,\n} from 'powersync-bridge';\n\n// NOTE (R11, static assertion): this package never imports `useQuery` from TanStack.\n// It renders from the engine store via useSyncExternalStore only.\n\nconst BridgeContext = createContext<Bridge | null>(null);\nconst BridgeActivityContext = createContext(true);\n\n/** Pause read observers in a hidden subtree while retaining their scoped snapshots. */\nexport const BridgeActivity = ({ active, children }: { active: boolean; children?: ReactNode }) => {\n const parentActive = useContext(BridgeActivityContext);\n return createElement(BridgeActivityContext.Provider, { value: parentActive && active }, children);\n};\n\nexport interface BridgeProviderProps {\n bridge: Bridge;\n children?: ReactNode;\n /** Native apps supply their network hook's result once, at the provider. */\n online?: boolean | undefined;\n}\n\n/** Mount once at app root. Crud-delta observation started at createBridge(), not here (F6-001). */\nexport function BridgeProvider({ bridge, children, online }: BridgeProviderProps) {\n useEffect(() => {\n bridge.setOnline(online);\n }, [bridge, online]);\n return createElement(BridgeContext.Provider, { value: bridge }, children);\n}\n\nexport type UseInfiniteQueryResult<T> = InfiniteQueryResult<T> & {\n fetchNextPage(): void;\n retry(): void;\n refresh(): void;\n refetch(): void;\n continueOlder(): void;\n};\n\n/** Define local SQL and an API page; the bridge owns all paging and reconciliation. */\nexport const useInfiniteQuery = <T extends { id: string }>(\n options: InfiniteQueryOptions<T>,\n): UseInfiniteQueryResult<T> => {\n const bridge = useBridge();\n const active = useContext(BridgeActivityContext);\n const epoch = useEpoch(bridge);\n const key = infiniteQueryKey(options);\n // Definitions are closures; every changing predicate belongs in queryKey.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const controller = useMemo(() => bridge.query(options), [bridge, epoch, key]);\n useEffect(() => {\n controller.retain(active);\n return () => controller.release(active);\n }, [controller, active]);\n const result = useSyncExternalStore(\n controller.store.subscribe,\n controller.store.getSnapshot,\n controller.store.getSnapshot,\n );\n return useMemo(\n () => ({\n ...result,\n fetchNextPage: controller.fetchNextPage,\n retry: controller.retry,\n refresh: controller.refresh,\n refetch: controller.refresh,\n continueOlder: controller.continueOlder,\n }),\n [result, controller],\n );\n};\n\nexport function useBridge(): Bridge {\n const b = useContext(BridgeContext);\n if (!b) throw new Error('powersync-bridge: no <BridgeProvider> above this component');\n return b;\n}\n\nfunction useEpoch(bridge: Bridge): number {\n return useSyncExternalStore(\n bridge.epochStore.subscribe,\n bridge.epochStore.getSnapshot,\n bridge.epochStore.getSnapshot,\n );\n}\n\ninterface Retainable<S> {\n readonly store: Store<S>;\n retain(active?: boolean): void;\n release(active?: boolean): void;\n}\n\n/**\n * Acquire a ref-counted store for the current epoch and subscribe to it. Retain/release run in\n * an effect so StrictMode's double-invoke is a no-op pair (R10); the store is shared across\n * components that ask for the same definition + scope.\n */\nfunction useRetained<S>(\n bridge: Bridge,\n key: string,\n acquire: () => Retainable<S>,\n active = true,\n): S {\n const epoch = useEpoch(bridge);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const holder = useMemo(() => acquire(), [bridge, epoch, key]);\n useEffect(() => {\n holder.retain(active);\n return () => holder.release(active);\n }, [holder, active]);\n return useSyncExternalStore(\n holder.store.subscribe,\n holder.store.getSnapshot,\n holder.store.getSnapshot,\n );\n}\n\nexport function useRacedList<TItem, TScope, TApi, K extends OrderKey<TItem>>(\n def: RacedListDefinition<TItem, TScope, TApi, K>,\n scope: TScope,\n): RacedListResult<TItem> {\n const bridge = useBridge();\n const active = useContext(BridgeActivityContext);\n const key = `${def.id}:${JSON.stringify(scope ?? null)}`;\n return useRetained(bridge, key, () => bridge.raced(def, scope), active);\n}\n\nexport interface UseInfiniteListResult<TItem> extends InfiniteListResult<TItem> {\n loadMore(): void;\n /** Clears API backoff and loads (R16). */\n retry(): void;\n}\n\nexport function useInfiniteList<TItem, TScope, K extends OrderKey<TItem>, TCursor, TPartition>(\n def: InfiniteListDefinition<TItem, TScope, K, TCursor, TPartition>,\n scope: TScope,\n): UseInfiniteListResult<TItem> {\n const bridge = useBridge();\n const epoch = useEpoch(bridge);\n const key = `${def.id}:${JSON.stringify(scope ?? null)}`;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const store = useMemo(() => bridge.infinite(def, scope), [bridge, epoch, key]);\n useEffect(() => {\n store.retain();\n return () => store.release();\n }, [store]);\n const result = useSyncExternalStore(\n store.store.subscribe,\n store.store.getSnapshot,\n store.store.getSnapshot,\n );\n return useMemo(\n () => ({ ...result, loadMore: () => void store.loadMore(), retry: () => void store.retry() }),\n [result, store],\n );\n}\n\n/** The default \"pull one row into the offline DB\" primitive (§7). */\nexport function useOnDemandStream(\n name: string,\n params: Params | null,\n opts: OnDemandOptions = {},\n): OnDemandResult {\n const bridge = useBridge();\n const key = `${name}:${JSON.stringify(params)}:${JSON.stringify(opts)}`;\n return useRetained(bridge, key, () => bridge.onDemand(name, params, opts));\n}\n\n/** OPTIONAL: server-side flagging (§7). Prefer useOnDemandStream. */\nexport function useAutoSync(config: FlaggerConfig, ids: readonly string[]): FlagResult {\n const bridge = useBridge();\n const key = `${config.stream}:${JSON.stringify([...ids].sort())}`;\n return useRetained(bridge, key, () => bridge.autoSync(config, ids));\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -2,6 +2,11 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { Bridge, InfiniteListResult, InfiniteQueryResult, FlaggerConfig, FlagResult, OrderKey, InfiniteListDefinition, InfiniteQueryOptions, Params, OnDemandOptions, OnDemandResult, RacedListDefinition, RacedListResult } from 'powersync-bridge';
4
4
 
5
+ /** Pause read observers in a hidden subtree while retaining their scoped snapshots. */
6
+ declare const BridgeActivity: ({ active, children }: {
7
+ active: boolean;
8
+ children?: ReactNode;
9
+ }) => react.FunctionComponentElement<react.ProviderProps<boolean>>;
5
10
  interface BridgeProviderProps {
6
11
  bridge: Bridge;
7
12
  children?: ReactNode;
@@ -34,4 +39,4 @@ declare function useOnDemandStream(name: string, params: Params | null, opts?: O
34
39
  /** OPTIONAL: server-side flagging (§7). Prefer useOnDemandStream. */
35
40
  declare function useAutoSync(config: FlaggerConfig, ids: readonly string[]): FlagResult;
36
41
 
37
- export { BridgeProvider, type BridgeProviderProps, type UseInfiniteListResult, type UseInfiniteQueryResult, useAutoSync, useBridge, useInfiniteList, useInfiniteQuery, useOnDemandStream, useRacedList };
42
+ export { BridgeActivity, BridgeProvider, type BridgeProviderProps, type UseInfiniteListResult, type UseInfiniteQueryResult, useAutoSync, useBridge, useInfiniteList, useInfiniteQuery, useOnDemandStream, useRacedList };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,11 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { Bridge, InfiniteListResult, InfiniteQueryResult, FlaggerConfig, FlagResult, OrderKey, InfiniteListDefinition, InfiniteQueryOptions, Params, OnDemandOptions, OnDemandResult, RacedListDefinition, RacedListResult } from 'powersync-bridge';
4
4
 
5
+ /** Pause read observers in a hidden subtree while retaining their scoped snapshots. */
6
+ declare const BridgeActivity: ({ active, children }: {
7
+ active: boolean;
8
+ children?: ReactNode;
9
+ }) => react.FunctionComponentElement<react.ProviderProps<boolean>>;
5
10
  interface BridgeProviderProps {
6
11
  bridge: Bridge;
7
12
  children?: ReactNode;
@@ -34,4 +39,4 @@ declare function useOnDemandStream(name: string, params: Params | null, opts?: O
34
39
  /** OPTIONAL: server-side flagging (§7). Prefer useOnDemandStream. */
35
40
  declare function useAutoSync(config: FlaggerConfig, ids: readonly string[]): FlagResult;
36
41
 
37
- export { BridgeProvider, type BridgeProviderProps, type UseInfiniteListResult, type UseInfiniteQueryResult, useAutoSync, useBridge, useInfiniteList, useInfiniteQuery, useOnDemandStream, useRacedList };
42
+ export { BridgeActivity, BridgeProvider, type BridgeProviderProps, type UseInfiniteListResult, type UseInfiniteQueryResult, useAutoSync, useBridge, useInfiniteList, useInfiniteQuery, useOnDemandStream, useRacedList };
package/dist/index.js CHANGED
@@ -1,7 +1,19 @@
1
1
  // src/index.ts
2
- import { createContext, createElement, useContext, useEffect, useMemo, useSyncExternalStore } from "react";
2
+ import {
3
+ createContext,
4
+ createElement,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useSyncExternalStore
9
+ } from "react";
3
10
  import { infiniteQueryKey } from "powersync-bridge";
4
11
  var BridgeContext = createContext(null);
12
+ var BridgeActivityContext = createContext(true);
13
+ var BridgeActivity = ({ active, children }) => {
14
+ const parentActive = useContext(BridgeActivityContext);
15
+ return createElement(BridgeActivityContext.Provider, { value: parentActive && active }, children);
16
+ };
5
17
  function BridgeProvider({ bridge, children, online }) {
6
18
  useEffect(() => {
7
19
  bridge.setOnline(online);
@@ -10,22 +22,30 @@ function BridgeProvider({ bridge, children, online }) {
10
22
  }
11
23
  var useInfiniteQuery = (options) => {
12
24
  const bridge = useBridge();
25
+ const active = useContext(BridgeActivityContext);
13
26
  const epoch = useEpoch(bridge);
14
27
  const key = infiniteQueryKey(options);
15
28
  const controller = useMemo(() => bridge.query(options), [bridge, epoch, key]);
16
29
  useEffect(() => {
17
- controller.retain();
18
- return () => controller.release();
19
- }, [controller]);
20
- const result = useSyncExternalStore(controller.store.subscribe, controller.store.getSnapshot, controller.store.getSnapshot);
21
- return useMemo(() => ({
22
- ...result,
23
- fetchNextPage: controller.fetchNextPage,
24
- retry: controller.retry,
25
- refresh: controller.refresh,
26
- refetch: controller.refresh,
27
- continueOlder: controller.continueOlder
28
- }), [result, controller]);
30
+ controller.retain(active);
31
+ return () => controller.release(active);
32
+ }, [controller, active]);
33
+ const result = useSyncExternalStore(
34
+ controller.store.subscribe,
35
+ controller.store.getSnapshot,
36
+ controller.store.getSnapshot
37
+ );
38
+ return useMemo(
39
+ () => ({
40
+ ...result,
41
+ fetchNextPage: controller.fetchNextPage,
42
+ retry: controller.retry,
43
+ refresh: controller.refresh,
44
+ refetch: controller.refresh,
45
+ continueOlder: controller.continueOlder
46
+ }),
47
+ [result, controller]
48
+ );
29
49
  };
30
50
  function useBridge() {
31
51
  const b = useContext(BridgeContext);
@@ -33,21 +53,30 @@ function useBridge() {
33
53
  return b;
34
54
  }
35
55
  function useEpoch(bridge) {
36
- return useSyncExternalStore(bridge.epochStore.subscribe, bridge.epochStore.getSnapshot, bridge.epochStore.getSnapshot);
56
+ return useSyncExternalStore(
57
+ bridge.epochStore.subscribe,
58
+ bridge.epochStore.getSnapshot,
59
+ bridge.epochStore.getSnapshot
60
+ );
37
61
  }
38
- function useRetained(bridge, key, acquire) {
62
+ function useRetained(bridge, key, acquire, active = true) {
39
63
  const epoch = useEpoch(bridge);
40
64
  const holder = useMemo(() => acquire(), [bridge, epoch, key]);
41
65
  useEffect(() => {
42
- holder.retain();
43
- return () => holder.release();
44
- }, [holder]);
45
- return useSyncExternalStore(holder.store.subscribe, holder.store.getSnapshot, holder.store.getSnapshot);
66
+ holder.retain(active);
67
+ return () => holder.release(active);
68
+ }, [holder, active]);
69
+ return useSyncExternalStore(
70
+ holder.store.subscribe,
71
+ holder.store.getSnapshot,
72
+ holder.store.getSnapshot
73
+ );
46
74
  }
47
75
  function useRacedList(def, scope) {
48
76
  const bridge = useBridge();
77
+ const active = useContext(BridgeActivityContext);
49
78
  const key = `${def.id}:${JSON.stringify(scope ?? null)}`;
50
- return useRetained(bridge, key, () => bridge.raced(def, scope));
79
+ return useRetained(bridge, key, () => bridge.raced(def, scope), active);
51
80
  }
52
81
  function useInfiniteList(def, scope) {
53
82
  const bridge = useBridge();
@@ -58,7 +87,11 @@ function useInfiniteList(def, scope) {
58
87
  store.retain();
59
88
  return () => store.release();
60
89
  }, [store]);
61
- const result = useSyncExternalStore(store.store.subscribe, store.store.getSnapshot, store.store.getSnapshot);
90
+ const result = useSyncExternalStore(
91
+ store.store.subscribe,
92
+ store.store.getSnapshot,
93
+ store.store.getSnapshot
94
+ );
62
95
  return useMemo(
63
96
  () => ({ ...result, loadMore: () => void store.loadMore(), retry: () => void store.retry() }),
64
97
  [result, store]
@@ -75,6 +108,7 @@ function useAutoSync(config, ids) {
75
108
  return useRetained(bridge, key, () => bridge.autoSync(config, ids));
76
109
  }
77
110
  export {
111
+ BridgeActivity,
78
112
  BridgeProvider,
79
113
  useAutoSync,
80
114
  useBridge,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { createContext, createElement, useContext, useEffect, useMemo, useSyncExternalStore, type ReactNode } from 'react';\nimport { infiniteQueryKey } from 'powersync-bridge';\nimport type {\n Bridge,\n FlaggerConfig,\n FlagResult,\n InfiniteListDefinition,\n InfiniteListResult,\n InfiniteQueryOptions,\n InfiniteQueryResult,\n OnDemandOptions,\n OnDemandResult,\n OrderKey,\n Params,\n RacedListDefinition,\n RacedListResult,\n Store,\n} from 'powersync-bridge';\n\n// NOTE (R11, static assertion): this package never imports `useQuery` from TanStack.\n// It renders from the engine store via useSyncExternalStore only.\n\nconst BridgeContext = createContext<Bridge | null>(null);\n\nexport interface BridgeProviderProps {\n bridge: Bridge;\n children?: ReactNode;\n /** Native apps supply their network hook's result once, at the provider. */\n online?: boolean | undefined;\n}\n\n/** Mount once at app root. Crud-delta observation started at createBridge(), not here (F6-001). */\nexport function BridgeProvider({ bridge, children, online }: BridgeProviderProps) {\n useEffect(() => { bridge.setOnline(online); }, [bridge, online]);\n return createElement(BridgeContext.Provider, { value: bridge }, children);\n}\n\nexport type UseInfiniteQueryResult<T> = InfiniteQueryResult<T> & {\n fetchNextPage(): void;\n retry(): void;\n refresh(): void;\n refetch(): void;\n continueOlder(): void;\n};\n\n/** Define local SQL and an API page; the bridge owns all paging and reconciliation. */\nexport const useInfiniteQuery = <T extends { id: string }>(\n options: InfiniteQueryOptions<T>,\n): UseInfiniteQueryResult<T> => {\n const bridge = useBridge();\n const epoch = useEpoch(bridge);\n const key = infiniteQueryKey(options);\n // Definitions are closures; every changing predicate belongs in queryKey.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const controller = useMemo(() => bridge.query(options), [bridge, epoch, key]);\n useEffect(() => {\n controller.retain();\n return () => controller.release();\n }, [controller]);\n const result = useSyncExternalStore(controller.store.subscribe, controller.store.getSnapshot, controller.store.getSnapshot);\n return useMemo(() => ({\n ...result, fetchNextPage: controller.fetchNextPage, retry: controller.retry,\n refresh: controller.refresh, refetch: controller.refresh, continueOlder: controller.continueOlder,\n }), [result, controller]);\n};\n\nexport function useBridge(): Bridge {\n const b = useContext(BridgeContext);\n if (!b) throw new Error('powersync-bridge: no <BridgeProvider> above this component');\n return b;\n}\n\nfunction useEpoch(bridge: Bridge): number {\n return useSyncExternalStore(bridge.epochStore.subscribe, bridge.epochStore.getSnapshot, bridge.epochStore.getSnapshot);\n}\n\ninterface Retainable<S> {\n readonly store: Store<S>;\n retain(): void;\n release(): void;\n}\n\n/**\n * Acquire a ref-counted store for the current epoch and subscribe to it. Retain/release run in\n * an effect so StrictMode's double-invoke is a no-op pair (R10); the store is shared across\n * components that ask for the same definition + scope.\n */\nfunction useRetained<S>(bridge: Bridge, key: string, acquire: () => Retainable<S>): S {\n const epoch = useEpoch(bridge);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const holder = useMemo(() => acquire(), [bridge, epoch, key]);\n useEffect(() => {\n holder.retain();\n return () => holder.release();\n }, [holder]);\n return useSyncExternalStore(holder.store.subscribe, holder.store.getSnapshot, holder.store.getSnapshot);\n}\n\nexport function useRacedList<TItem, TScope, TApi, K extends OrderKey<TItem>>(\n def: RacedListDefinition<TItem, TScope, TApi, K>,\n scope: TScope,\n): RacedListResult<TItem> {\n const bridge = useBridge();\n const key = `${def.id}:${JSON.stringify(scope ?? null)}`;\n return useRetained(bridge, key, () => bridge.raced(def, scope));\n}\n\nexport interface UseInfiniteListResult<TItem> extends InfiniteListResult<TItem> {\n loadMore(): void;\n /** Clears API backoff and loads (R16). */\n retry(): void;\n}\n\nexport function useInfiniteList<TItem, TScope, K extends OrderKey<TItem>, TCursor, TPartition>(\n def: InfiniteListDefinition<TItem, TScope, K, TCursor, TPartition>,\n scope: TScope,\n): UseInfiniteListResult<TItem> {\n const bridge = useBridge();\n const epoch = useEpoch(bridge);\n const key = `${def.id}:${JSON.stringify(scope ?? null)}`;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const store = useMemo(() => bridge.infinite(def, scope), [bridge, epoch, key]);\n useEffect(() => {\n store.retain();\n return () => store.release();\n }, [store]);\n const result = useSyncExternalStore(store.store.subscribe, store.store.getSnapshot, store.store.getSnapshot);\n return useMemo(\n () => ({ ...result, loadMore: () => void store.loadMore(), retry: () => void store.retry() }),\n [result, store],\n );\n}\n\n/** The default \"pull one row into the offline DB\" primitive (§7). */\nexport function useOnDemandStream(name: string, params: Params | null, opts: OnDemandOptions = {}): OnDemandResult {\n const bridge = useBridge();\n const key = `${name}:${JSON.stringify(params)}:${JSON.stringify(opts)}`;\n return useRetained(bridge, key, () => bridge.onDemand(name, params, opts));\n}\n\n/** OPTIONAL: server-side flagging (§7). Prefer useOnDemandStream. */\nexport function useAutoSync(config: FlaggerConfig, ids: readonly string[]): FlagResult {\n const bridge = useBridge();\n const key = `${config.stream}:${JSON.stringify([...ids].sort())}`;\n return useRetained(bridge, key, () => bridge.autoSync(config, ids));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n createContext,\n createElement,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from 'react';\nimport { infiniteQueryKey } from 'powersync-bridge';\nimport type {\n Bridge,\n FlaggerConfig,\n FlagResult,\n InfiniteListDefinition,\n InfiniteListResult,\n InfiniteQueryOptions,\n InfiniteQueryResult,\n OnDemandOptions,\n OnDemandResult,\n OrderKey,\n Params,\n RacedListDefinition,\n RacedListResult,\n Store,\n} from 'powersync-bridge';\n\n// NOTE (R11, static assertion): this package never imports `useQuery` from TanStack.\n// It renders from the engine store via useSyncExternalStore only.\n\nconst BridgeContext = createContext<Bridge | null>(null);\nconst BridgeActivityContext = createContext(true);\n\n/** Pause read observers in a hidden subtree while retaining their scoped snapshots. */\nexport const BridgeActivity = ({ active, children }: { active: boolean; children?: ReactNode }) => {\n const parentActive = useContext(BridgeActivityContext);\n return createElement(BridgeActivityContext.Provider, { value: parentActive && active }, children);\n};\n\nexport interface BridgeProviderProps {\n bridge: Bridge;\n children?: ReactNode;\n /** Native apps supply their network hook's result once, at the provider. */\n online?: boolean | undefined;\n}\n\n/** Mount once at app root. Crud-delta observation started at createBridge(), not here (F6-001). */\nexport function BridgeProvider({ bridge, children, online }: BridgeProviderProps) {\n useEffect(() => {\n bridge.setOnline(online);\n }, [bridge, online]);\n return createElement(BridgeContext.Provider, { value: bridge }, children);\n}\n\nexport type UseInfiniteQueryResult<T> = InfiniteQueryResult<T> & {\n fetchNextPage(): void;\n retry(): void;\n refresh(): void;\n refetch(): void;\n continueOlder(): void;\n};\n\n/** Define local SQL and an API page; the bridge owns all paging and reconciliation. */\nexport const useInfiniteQuery = <T extends { id: string }>(\n options: InfiniteQueryOptions<T>,\n): UseInfiniteQueryResult<T> => {\n const bridge = useBridge();\n const active = useContext(BridgeActivityContext);\n const epoch = useEpoch(bridge);\n const key = infiniteQueryKey(options);\n // Definitions are closures; every changing predicate belongs in queryKey.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const controller = useMemo(() => bridge.query(options), [bridge, epoch, key]);\n useEffect(() => {\n controller.retain(active);\n return () => controller.release(active);\n }, [controller, active]);\n const result = useSyncExternalStore(\n controller.store.subscribe,\n controller.store.getSnapshot,\n controller.store.getSnapshot,\n );\n return useMemo(\n () => ({\n ...result,\n fetchNextPage: controller.fetchNextPage,\n retry: controller.retry,\n refresh: controller.refresh,\n refetch: controller.refresh,\n continueOlder: controller.continueOlder,\n }),\n [result, controller],\n );\n};\n\nexport function useBridge(): Bridge {\n const b = useContext(BridgeContext);\n if (!b) throw new Error('powersync-bridge: no <BridgeProvider> above this component');\n return b;\n}\n\nfunction useEpoch(bridge: Bridge): number {\n return useSyncExternalStore(\n bridge.epochStore.subscribe,\n bridge.epochStore.getSnapshot,\n bridge.epochStore.getSnapshot,\n );\n}\n\ninterface Retainable<S> {\n readonly store: Store<S>;\n retain(active?: boolean): void;\n release(active?: boolean): void;\n}\n\n/**\n * Acquire a ref-counted store for the current epoch and subscribe to it. Retain/release run in\n * an effect so StrictMode's double-invoke is a no-op pair (R10); the store is shared across\n * components that ask for the same definition + scope.\n */\nfunction useRetained<S>(\n bridge: Bridge,\n key: string,\n acquire: () => Retainable<S>,\n active = true,\n): S {\n const epoch = useEpoch(bridge);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const holder = useMemo(() => acquire(), [bridge, epoch, key]);\n useEffect(() => {\n holder.retain(active);\n return () => holder.release(active);\n }, [holder, active]);\n return useSyncExternalStore(\n holder.store.subscribe,\n holder.store.getSnapshot,\n holder.store.getSnapshot,\n );\n}\n\nexport function useRacedList<TItem, TScope, TApi, K extends OrderKey<TItem>>(\n def: RacedListDefinition<TItem, TScope, TApi, K>,\n scope: TScope,\n): RacedListResult<TItem> {\n const bridge = useBridge();\n const active = useContext(BridgeActivityContext);\n const key = `${def.id}:${JSON.stringify(scope ?? null)}`;\n return useRetained(bridge, key, () => bridge.raced(def, scope), active);\n}\n\nexport interface UseInfiniteListResult<TItem> extends InfiniteListResult<TItem> {\n loadMore(): void;\n /** Clears API backoff and loads (R16). */\n retry(): void;\n}\n\nexport function useInfiniteList<TItem, TScope, K extends OrderKey<TItem>, TCursor, TPartition>(\n def: InfiniteListDefinition<TItem, TScope, K, TCursor, TPartition>,\n scope: TScope,\n): UseInfiniteListResult<TItem> {\n const bridge = useBridge();\n const epoch = useEpoch(bridge);\n const key = `${def.id}:${JSON.stringify(scope ?? null)}`;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const store = useMemo(() => bridge.infinite(def, scope), [bridge, epoch, key]);\n useEffect(() => {\n store.retain();\n return () => store.release();\n }, [store]);\n const result = useSyncExternalStore(\n store.store.subscribe,\n store.store.getSnapshot,\n store.store.getSnapshot,\n );\n return useMemo(\n () => ({ ...result, loadMore: () => void store.loadMore(), retry: () => void store.retry() }),\n [result, store],\n );\n}\n\n/** The default \"pull one row into the offline DB\" primitive (§7). */\nexport function useOnDemandStream(\n name: string,\n params: Params | null,\n opts: OnDemandOptions = {},\n): OnDemandResult {\n const bridge = useBridge();\n const key = `${name}:${JSON.stringify(params)}:${JSON.stringify(opts)}`;\n return useRetained(bridge, key, () => bridge.onDemand(name, params, opts));\n}\n\n/** OPTIONAL: server-side flagging (§7). Prefer useOnDemandStream. */\nexport function useAutoSync(config: FlaggerConfig, ids: readonly string[]): FlagResult {\n const bridge = useBridge();\n const key = `${config.stream}:${JSON.stringify([...ids].sort())}`;\n return useRetained(bridge, key, () => bridge.autoSync(config, ids));\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync-bridge/react",
3
- "version": "0.0.1-alpha.1",
3
+ "version": "0.0.1-alpha.2",
4
4
  "description": "React bindings for powersync-bridge: BridgeProvider, useRacedList, useInfiniteList, useOnDemandStream, useAutoSync (useSyncExternalStore-based).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,14 +41,14 @@
41
41
  ],
42
42
  "peerDependencies": {
43
43
  "react": "^18.2.0 || ^19.0.0",
44
- "powersync-bridge": "0.0.1-alpha.1"
44
+ "powersync-bridge": "0.0.1-alpha.2"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/react": "^19.1.0",
48
48
  "react": "^19.1.0",
49
49
  "tsup": "^8.5.1",
50
50
  "typescript": "^5.9.0",
51
- "powersync-bridge": "0.0.1-alpha.1"
51
+ "powersync-bridge": "0.0.1-alpha.2"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "tsup",