@doync/react 0.1.0 → 0.1.1
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 +3 -3
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +6 -11
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +6 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mobile.cjs +1 -1
- package/dist/mobile.d.cts +2 -1
- package/dist/mobile.d.cts.map +1 -1
- package/dist/mobile.d.ts +2 -1
- package/dist/mobile.d.ts.map +1 -1
- package/dist/mobile.js +1 -1
- package/dist/mobile.js.map +1 -1
- package/dist/use-client-D1Y1EDVp.js +2 -0
- package/dist/use-client-D1Y1EDVp.js.map +1 -0
- package/dist/{use-client-D1I3o0t5.cjs → use-client-Dif9u1-9.cjs} +1 -1
- package/dist/web.cjs +1 -1
- package/dist/web.d.cts +2 -1
- package/dist/web.d.cts.map +1 -1
- package/dist/web.d.ts +2 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +1 -1
- package/dist/web.js.map +1 -1
- package/package.json +5 -5
- package/src/hooks.tsx +14 -14
- package/src/mobile.ts +9 -15
- package/src/use-client.ts +34 -69
- package/src/web.ts +10 -15
- package/dist/use-client-DanwzoBF.js +0 -2
- package/dist/use-client-DanwzoBF.js.map +0 -1
package/README.md
CHANGED
|
@@ -18,14 +18,14 @@ Peer: `react` ≥ 18.
|
|
|
18
18
|
|
|
19
19
|
## Provider
|
|
20
20
|
|
|
21
|
-
Boot the environment client once, then provide it to the tree. Keep the same client object for the life of the app (or tab); drive login/logout through `client.
|
|
21
|
+
Boot the environment client once, then provide it to the tree. Keep the same client object for the life of the app (or tab); drive login/logout through `client.updateAuth`, not by reconstructing the client.
|
|
22
22
|
|
|
23
23
|
```tsx
|
|
24
24
|
import { DoyncProvider } from '@doync/react'
|
|
25
25
|
import { createWebClient } from '@doync/web'
|
|
26
26
|
// or: import { createMobileClient } from '@doync/mobile'
|
|
27
27
|
|
|
28
|
-
const client = createWebClient({/* worker,
|
|
28
|
+
const client = createWebClient({/* worker, authData, … — see @doync/web */})
|
|
29
29
|
|
|
30
30
|
export function App() {
|
|
31
31
|
return (
|
|
@@ -40,7 +40,7 @@ export function App() {
|
|
|
40
40
|
|
|
41
41
|
## Shared definition module
|
|
42
42
|
|
|
43
|
-
Hooks take the same `queries` / `mutations` trees every client uses — define them once
|
|
43
|
+
Hooks take the same `queries` / `mutations` trees every client uses — define them once through `createDoync` / `createDoyncDrizzle` and import that module from web, mobile, and the DB-worker entry:
|
|
44
44
|
|
|
45
45
|
```ts
|
|
46
46
|
// shared/data.ts — one data layer for every client
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./use-client-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./use-client-Dif9u1-9.cjs");let t=require("react"),n=require("react/jsx-runtime"),r=require("@doync/core/internal");const i=(0,t.createContext)(null);function a({client:e,children:t}){return(0,n.jsx)(i.Provider,{value:e,children:t})}function o(){let e=(0,t.useContext)(i);if(e===null)throw Error(`doync: a doync hook was used outside a <DoyncProvider>`);return e}function s(e,n){let r=o(),i=n?.skip??!1,a=x(e),s=a?`|ttl=${n?.ttl??``}|skip=${i}|falsy=1`:`${b(e)}|ttl=${n?.ttl??``}|skip=${i}|falsy=0`,l=(0,t.useMemo)(()=>{if(a)return g;let t=S(e,`useQuery`);return r.subscribe(t,{ttl:n?.ttl,skip:i})},[r,s]);(0,t.useEffect)(()=>(l.retain(),()=>l.release()),[l]);let u=v(l),d=y(l),f=(0,t.useMemo)(()=>a?!1:c(e.query,e.args),[s]);if(a)return[void 0,d];if(l.one!==void 0&&l.one!==f)throw Error(l.one?`doync: the subscribed statement is a one-query (sql.one / findFirst) but this client resolved it as multi-row — the query definition disagrees across the resolve boundary`:`doync: the subscribed statement is multi-row but this client resolved it as a one-query — the query definition disagrees across the resolve boundary`);return[l.one??f?u.length>0?u[0]:void 0:u,d]}function c(e,t){try{return e.resolve({ctx:null,args:t})?.one??!1}catch{return!1}}function l(e){let n=o(),r=x(e),i=(0,t.useMemo)(()=>{if(r)return _;let t=S(e,`useQueryOnce`);return n.once(t)},[n,r?`falsy=1`:`${b(e)}|falsy=0`]),[a,s]=(0,t.useState)(`loading`);(0,t.useEffect)(()=>{if(r)return;let e=!0;return s(`loading`),i.server.then(()=>{e&&s(`success`)},()=>{e&&s(`error`)}),()=>{e=!1,i.dispose()}},[i,r]);let c=r?`skipped`:a,l=v(i);return[r?void 0:l,{status:c}]}function u(e,n=[]){let r=o(),i=x(e),a=i?null:typeof e==`function`?e():e,s=a===null?``:typeof a==`string`?a:a.sql,c=a===null||typeof a==`string`?n:a.params??[],l=(0,t.useMemo)(()=>i?g:r.local(s,...c),[r,i?`falsy=1`:`${s}|${JSON.stringify(c)}|falsy=0`]);(0,t.useEffect)(()=>(l.retain(),()=>l.release()),[l]);let u=v(l),d=y(l);return i?[void 0,d]:[u,d]}function d(){let e=o();return(0,t.useSyncExternalStore)((0,t.useCallback)(t=>e.onConnectionChange(t),[e]),()=>e.connectionStatus,()=>e.connectionStatus)}function f(e){let n=o();return(0,t.useCallback)((t,r)=>n.mutate(e,t,r),[n,e])}function p(){let e=o();return(0,t.useSyncExternalStore)((0,t.useCallback)(t=>e.onSchemaChange(t),[e]),()=>e.schemaStatus,()=>e.schemaStatus)}const m=Object.freeze([]),h=Object.freeze({status:`unknown`}),g={current:()=>m,status:()=>h,onChange:()=>()=>{},retain:()=>{},release:()=>{}},_={current:()=>m,onChange:()=>()=>{},dispose:()=>{},server:new Promise(()=>{})};function v(e){let n=(0,t.useCallback)(t=>e.onChange(t),[e]),r=(0,t.useCallback)(()=>e.current(),[e]);return(0,t.useSyncExternalStore)(n,r,r)}function y(e){let n=(0,t.useCallback)(t=>e.onChange(t),[e]),r=(0,t.useCallback)(()=>e.status(),[e]);return(0,t.useSyncExternalStore)(n,r,r)}function b(e){return`${e.query.name??``}|${JSON.stringify(e.args??null)}`}function x(e){return e===!1||e==null}function S(e,t){if((0,r.isBoundQuery)(e))return e;throw Error(typeof e==`function`?`doync: ${t} expected a BoundQuery — received a function; did you forget to call it?`:`doync: ${t} expected a BoundQuery (produced by queries.…(args)), got ${typeof e==`object`&&e?`an object`:String(e)}`)}exports.DoyncProvider=a,exports.useClient=e.t,exports.useConnectionStatus=d,exports.useDoyncClient=o,exports.useLocalQuery=u,exports.useMutation=f,exports.useQuery=s,exports.useQueryOnce=l,exports.useSchemaStatus=p;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ConnectionStatus, ConnectionStatus as ConnectionStatus$1, DoyncClient, DoyncClient as DoyncClient$1, FalsyQuery, FalsyQuery as FalsyQuery$1, LogoutBehavior, LogoutBehavior as LogoutBehavior$1, MutationOptions, MutationOptions as MutationOptions$1, MutationResult, MutationResult as MutationResult$1, OnceView, PreloadHandle, PreloadOptions, QueryStatus, SchemaEvent, SchemaEvent as SchemaEvent$1, SchemaEventKind, SubscribeOptions, SubscribeOptions as SubscribeOptions$1, View, ViewStatus, ViewStatus as ViewStatus$1 } from "@doync/client";
|
|
1
|
+
import { AuthData, ConnectionStatus, ConnectionStatus as ConnectionStatus$1, DoyncClient, DoyncClient as DoyncClient$1, FalsyQuery, FalsyQuery as FalsyQuery$1, LogoutBehavior, LogoutBehavior as LogoutBehavior$1, MutationOptions, MutationOptions as MutationOptions$1, MutationResult, MutationResult as MutationResult$1, OnceView, PreloadHandle, PreloadOptions, QueryStatus, SchemaEvent, SchemaEvent as SchemaEvent$1, SchemaEventKind, SubscribeOptions, SubscribeOptions as SubscribeOptions$1, View, ViewStatus, ViewStatus as ViewStatus$1 } from "@doync/client";
|
|
2
2
|
import { ReactElement, ReactNode } from "react";
|
|
3
|
-
import { BoundQuery, BoundQuery as BoundQuery$1, MutationDefinition, SqlValue } from "@doync/core";
|
|
3
|
+
import { AuthContext, BoundQuery, BoundQuery as BoundQuery$1, MutationDefinition, SqlValue } from "@doync/core";
|
|
4
4
|
|
|
5
5
|
//#region src/provider.d.ts
|
|
6
6
|
/** Props for {@link DoyncProvider}. */
|
|
@@ -90,7 +90,7 @@ declare function useConnectionStatus(): ConnectionStatus$1;
|
|
|
90
90
|
* and push; returns `{ client, server }` — paint from `client`, await `server`
|
|
91
91
|
* for Origin confirmation.
|
|
92
92
|
*/
|
|
93
|
-
declare function useMutation<Input = unknown>(mutation: MutationDefinition<Input>): (args: Input, options?: MutationOptions$1) => MutationResult$1;
|
|
93
|
+
declare function useMutation<Input = unknown>(mutation: MutationDefinition<Input, any>): (args: Input, options?: MutationOptions$1) => MutationResult$1;
|
|
94
94
|
/**
|
|
95
95
|
* Current schema/recovery state, or `null` when nominal. Use for a deploy-skew
|
|
96
96
|
* / recovery banner — re-renders when the state clears too (a one-shot
|
|
@@ -103,13 +103,9 @@ declare function useSchemaStatus(): SchemaEvent$1 | null;
|
|
|
103
103
|
* Minimum call surface the hook routes onto. Platform clients (`MobileClient`,
|
|
104
104
|
* `WebClient`) both satisfy this; tests inject a fake.
|
|
105
105
|
*/
|
|
106
|
-
interface ClientLifecycle {
|
|
106
|
+
interface ClientLifecycle<TAuthContext extends AuthContext = AuthContext> {
|
|
107
107
|
close(): void;
|
|
108
|
-
|
|
109
|
-
token: string | null;
|
|
110
|
-
ctx?: unknown;
|
|
111
|
-
userId: string | null;
|
|
112
|
-
}): void;
|
|
108
|
+
updateAuth(authData: AuthData<TAuthContext> | null): void;
|
|
113
109
|
setLogoutBehavior(behavior: LogoutBehavior$1): void;
|
|
114
110
|
}
|
|
115
111
|
/**
|
|
@@ -119,8 +115,7 @@ interface ClientLifecycle {
|
|
|
119
115
|
*
|
|
120
116
|
* - `identity` — close the old client and create a new one (e.g. a different
|
|
121
117
|
* database `name` is a different client; no in-place path).
|
|
122
|
-
* - `auth` — `
|
|
123
|
-
* compared by JSON).
|
|
118
|
+
* - `auth` — `authData` feeds a single `updateAuth` call (compared by JSON).
|
|
124
119
|
* - `logoutBehavior` — call `setLogoutBehavior`.
|
|
125
120
|
* - `throw` — value change throws (e.g. `url`). Checked before `identity`, so a
|
|
126
121
|
* same-render name+url swap cannot silently reconnect elsewhere.
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/provider.tsx","../src/hooks.tsx","../src/use-client.ts"],"mappings":";;;;;;UAQiB,kBAAA;;WAEN,MAAA,EAAQ,aAAA;EAAA,SACR,QAAA,EAAU,SAAA;AAAA;;iBAIL,aAAA;EACd,MAAA;EACA;AAAA,GACC,kBAAA,GAAqB,YAAA;;;;;iBAUR,cAAA,IAAkB,aAAA;;;;;;AApBlC;;KC4BY,eAAA,GAAkB,kBAAA;;;;;;;ADzBT;AAIrB;;;;;;iBCsCgB,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,SAClB,OAAA,GAAU,eAAA,IACR,GAAA,cAAiB,YAAA;AAAA,iBAEL,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,UAClB,OAAA,GAAU,eAAA,aACC,GAAA,IAAO,YAAA;AAAA,iBAEJ,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,UAAa,YAAA,EAC/B,OAAA,GAAU,eAAA,IACR,GAAA,cAAiB,YAAA;AAAA,iBAEL,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,WAAc,YAAA,EAChC,OAAA,GAAU,eAAA,aACC,GAAA,gBAAmB,YAAA;;;;;KAoFpB,UAAA;;ADjJY;AAUxB;;;;iBCgJgB,YAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,uBACP,GAAA;EAAA,SAAkB,MAAA,EAAQ,UAAA;AAAA;AAAA,iBAEvB,YAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,aAAgB,YAAA,aACvB,GAAA;EAAA,SAA8B,MAAA,EAAQ,UAAA;AAAA;;;AAlJrB;AAiB9B;KAmLY,WAAA;EAAA,SAEG,GAAA;EAAA,SAAsB,MAAA,YAAkB,QAAA;AAAA;EAAA,SAGpC,GAAA;EAAA,SAAsB,MAAA,YAAkB,QAAA;AAAA;;;;;;iBAO3C,aAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,MAAA,EAAQ,WAAA,EACR,MAAA,YAAkB,QAAA,eACP,GAAA,IAAO,YAAA;AAAA,iBACJ,aAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,MAAA,EAAQ,WAAA,GAAc,YAAA,EACtB,MAAA,YAAkB,QAAA,eACP,GAAA,gBAAmB,YAAA;;;;;;iBAiDhB,mBAAA,IAAuB,kBAAA;;;;;AAtPlB;iBAyQL,WAAA,kBACd,QAAA,EAAU,kBAAA,CAAmB,KAAA,
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/provider.tsx","../src/hooks.tsx","../src/use-client.ts"],"mappings":";;;;;;UAQiB,kBAAA;;WAEN,MAAA,EAAQ,aAAA;EAAA,SACR,QAAA,EAAU,SAAA;AAAA;;iBAIL,aAAA;EACd,MAAA;EACA;AAAA,GACC,kBAAA,GAAqB,YAAA;;;;;iBAUR,cAAA,IAAkB,aAAA;;;;;;AApBlC;;KC4BY,eAAA,GAAkB,kBAAA;;;;;;;ADzBT;AAIrB;;;;;;iBCsCgB,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,SAClB,OAAA,GAAU,eAAA,IACR,GAAA,cAAiB,YAAA;AAAA,iBAEL,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,UAClB,OAAA,GAAU,eAAA,aACC,GAAA,IAAO,YAAA;AAAA,iBAEJ,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,UAAa,YAAA,EAC/B,OAAA,GAAU,eAAA,IACR,GAAA,cAAiB,YAAA;AAAA,iBAEL,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,WAAc,YAAA,EAChC,OAAA,GAAU,eAAA,aACC,GAAA,gBAAmB,YAAA;;;;;KAoFpB,UAAA;;ADjJY;AAUxB;;;;iBCgJgB,YAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,uBACP,GAAA;EAAA,SAAkB,MAAA,EAAQ,UAAA;AAAA;AAAA,iBAEvB,YAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,aAAgB,YAAA,aACvB,GAAA;EAAA,SAA8B,MAAA,EAAQ,UAAA;AAAA;;;AAlJrB;AAiB9B;KAmLY,WAAA;EAAA,SAEG,GAAA;EAAA,SAAsB,MAAA,YAAkB,QAAA;AAAA;EAAA,SAGpC,GAAA;EAAA,SAAsB,MAAA,YAAkB,QAAA;AAAA;;;;;;iBAO3C,aAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,MAAA,EAAQ,WAAA,EACR,MAAA,YAAkB,QAAA,eACP,GAAA,IAAO,YAAA;AAAA,iBACJ,aAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,MAAA,EAAQ,WAAA,GAAc,YAAA,EACtB,MAAA,YAAkB,QAAA,eACP,GAAA,gBAAmB,YAAA;;;;;;iBAiDhB,mBAAA,IAAuB,kBAAA;;;;;AAtPlB;iBAyQL,WAAA,kBACd,QAAA,EAAU,kBAAA,CAAmB,KAAA,UAC3B,IAAA,EAAM,KAAA,EAAO,OAAA,GAAU,iBAAA,KAAoB,gBAAA;;;;;;iBAgB/B,eAAA,IAAmB,aAAA;;;ADtUnC;;;;AAAA,UEQiB,eAAA,sBACM,WAAA,GAAc,WAAA;EAEnC,KAAA;EACA,UAAA,CAAW,QAAA,EAAU,QAAA,CAAS,YAAA;EAC9B,iBAAA,CAAkB,QAAA,EAAU,gBAAA;AAAA;;;;;;;;AFVN;AAUxB;;;;AAAkC;;;KEkBtB,iBAAA;ADVZ;;;;AAAA,KCqBY,kBAAA,mDACW,QAAA,IAAY,iBAAA;ADLnC;;;;;;AAAA,iBCsBgB,SAAA,0CAEE,eAAA,EAEhB,OAAA,EAAS,QAAA,SACT,YAAA,GAAe,OAAA,EAAS,QAAA,KAAa,OAAA,EACrC,MAAA,EAAQ,kBAAA,CAAmB,QAAA,IAC1B,OAAA"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReactElement, ReactNode } from "react";
|
|
2
|
-
import { ConnectionStatus, ConnectionStatus as ConnectionStatus$1, DoyncClient, DoyncClient as DoyncClient$1, FalsyQuery, FalsyQuery as FalsyQuery$1, LogoutBehavior, LogoutBehavior as LogoutBehavior$1, MutationOptions, MutationOptions as MutationOptions$1, MutationResult, MutationResult as MutationResult$1, OnceView, PreloadHandle, PreloadOptions, QueryStatus, SchemaEvent, SchemaEvent as SchemaEvent$1, SchemaEventKind, SubscribeOptions, SubscribeOptions as SubscribeOptions$1, View, ViewStatus, ViewStatus as ViewStatus$1 } from "@doync/client";
|
|
3
|
-
import { BoundQuery, BoundQuery as BoundQuery$1, MutationDefinition, SqlValue } from "@doync/core";
|
|
2
|
+
import { AuthData, ConnectionStatus, ConnectionStatus as ConnectionStatus$1, DoyncClient, DoyncClient as DoyncClient$1, FalsyQuery, FalsyQuery as FalsyQuery$1, LogoutBehavior, LogoutBehavior as LogoutBehavior$1, MutationOptions, MutationOptions as MutationOptions$1, MutationResult, MutationResult as MutationResult$1, OnceView, PreloadHandle, PreloadOptions, QueryStatus, SchemaEvent, SchemaEvent as SchemaEvent$1, SchemaEventKind, SubscribeOptions, SubscribeOptions as SubscribeOptions$1, View, ViewStatus, ViewStatus as ViewStatus$1 } from "@doync/client";
|
|
3
|
+
import { AuthContext, BoundQuery, BoundQuery as BoundQuery$1, MutationDefinition, SqlValue } from "@doync/core";
|
|
4
4
|
|
|
5
5
|
//#region src/provider.d.ts
|
|
6
6
|
/** Props for {@link DoyncProvider}. */
|
|
@@ -90,7 +90,7 @@ declare function useConnectionStatus(): ConnectionStatus$1;
|
|
|
90
90
|
* and push; returns `{ client, server }` — paint from `client`, await `server`
|
|
91
91
|
* for Origin confirmation.
|
|
92
92
|
*/
|
|
93
|
-
declare function useMutation<Input = unknown>(mutation: MutationDefinition<Input>): (args: Input, options?: MutationOptions$1) => MutationResult$1;
|
|
93
|
+
declare function useMutation<Input = unknown>(mutation: MutationDefinition<Input, any>): (args: Input, options?: MutationOptions$1) => MutationResult$1;
|
|
94
94
|
/**
|
|
95
95
|
* Current schema/recovery state, or `null` when nominal. Use for a deploy-skew
|
|
96
96
|
* / recovery banner — re-renders when the state clears too (a one-shot
|
|
@@ -103,13 +103,9 @@ declare function useSchemaStatus(): SchemaEvent$1 | null;
|
|
|
103
103
|
* Minimum call surface the hook routes onto. Platform clients (`MobileClient`,
|
|
104
104
|
* `WebClient`) both satisfy this; tests inject a fake.
|
|
105
105
|
*/
|
|
106
|
-
interface ClientLifecycle {
|
|
106
|
+
interface ClientLifecycle<TAuthContext extends AuthContext = AuthContext> {
|
|
107
107
|
close(): void;
|
|
108
|
-
|
|
109
|
-
token: string | null;
|
|
110
|
-
ctx?: unknown;
|
|
111
|
-
userId: string | null;
|
|
112
|
-
}): void;
|
|
108
|
+
updateAuth(authData: AuthData<TAuthContext> | null): void;
|
|
113
109
|
setLogoutBehavior(behavior: LogoutBehavior$1): void;
|
|
114
110
|
}
|
|
115
111
|
/**
|
|
@@ -119,8 +115,7 @@ interface ClientLifecycle {
|
|
|
119
115
|
*
|
|
120
116
|
* - `identity` — close the old client and create a new one (e.g. a different
|
|
121
117
|
* database `name` is a different client; no in-place path).
|
|
122
|
-
* - `auth` — `
|
|
123
|
-
* compared by JSON).
|
|
118
|
+
* - `auth` — `authData` feeds a single `updateAuth` call (compared by JSON).
|
|
124
119
|
* - `logoutBehavior` — call `setLogoutBehavior`.
|
|
125
120
|
* - `throw` — value change throws (e.g. `url`). Checked before `identity`, so a
|
|
126
121
|
* same-render name+url swap cannot silently reconnect elsewhere.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/provider.tsx","../src/hooks.tsx","../src/use-client.ts"],"mappings":";;;;;;UAQiB,kBAAA;;WAEN,MAAA,EAAQ,aAAA;EAAA,SACR,QAAA,EAAU,SAAA;AAAA;;iBAIL,aAAA;EACd,MAAA;EACA;AAAA,GACC,kBAAA,GAAqB,YAAA;;;;;iBAUR,cAAA,IAAkB,aAAA;;;;;;AApBlC;;KC4BY,eAAA,GAAkB,kBAAA;;;;;;;ADzBT;AAIrB;;;;;;iBCsCgB,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,SAClB,OAAA,GAAU,eAAA,IACR,GAAA,cAAiB,YAAA;AAAA,iBAEL,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,UAClB,OAAA,GAAU,eAAA,aACC,GAAA,IAAO,YAAA;AAAA,iBAEJ,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,UAAa,YAAA,EAC/B,OAAA,GAAU,eAAA,IACR,GAAA,cAAiB,YAAA;AAAA,iBAEL,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,WAAc,YAAA,EAChC,OAAA,GAAU,eAAA,aACC,GAAA,gBAAmB,YAAA;;;;;KAoFpB,UAAA;;ADjJY;AAUxB;;;;iBCgJgB,YAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,uBACP,GAAA;EAAA,SAAkB,MAAA,EAAQ,UAAA;AAAA;AAAA,iBAEvB,YAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,aAAgB,YAAA,aACvB,GAAA;EAAA,SAA8B,MAAA,EAAQ,UAAA;AAAA;;;AAlJrB;AAiB9B;KAmLY,WAAA;EAAA,SAEG,GAAA;EAAA,SAAsB,MAAA,YAAkB,QAAA;AAAA;EAAA,SAGpC,GAAA;EAAA,SAAsB,MAAA,YAAkB,QAAA;AAAA;;;;;;iBAO3C,aAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,MAAA,EAAQ,WAAA,EACR,MAAA,YAAkB,QAAA,eACP,GAAA,IAAO,YAAA;AAAA,iBACJ,aAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,MAAA,EAAQ,WAAA,GAAc,YAAA,EACtB,MAAA,YAAkB,QAAA,eACP,GAAA,gBAAmB,YAAA;;;;;;iBAiDhB,mBAAA,IAAuB,kBAAA;;;;;AAtPlB;iBAyQL,WAAA,kBACd,QAAA,EAAU,kBAAA,CAAmB,KAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/provider.tsx","../src/hooks.tsx","../src/use-client.ts"],"mappings":";;;;;;UAQiB,kBAAA;;WAEN,MAAA,EAAQ,aAAA;EAAA,SACR,QAAA,EAAU,SAAA;AAAA;;iBAIL,aAAA;EACd,MAAA;EACA;AAAA,GACC,kBAAA,GAAqB,YAAA;;;;;iBAUR,cAAA,IAAkB,aAAA;;;;;;AApBlC;;KC4BY,eAAA,GAAkB,kBAAA;;;;;;;ADzBT;AAIrB;;;;;;iBCsCgB,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,SAClB,OAAA,GAAU,eAAA,IACR,GAAA,cAAiB,YAAA;AAAA,iBAEL,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,UAClB,OAAA,GAAU,eAAA,aACC,GAAA,IAAO,YAAA;AAAA,iBAEJ,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,UAAa,YAAA,EAC/B,OAAA,GAAU,eAAA,IACR,GAAA,cAAiB,YAAA;AAAA,iBAEL,QAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,WAAc,YAAA,EAChC,OAAA,GAAU,eAAA,aACC,GAAA,gBAAmB,YAAA;;;;;KAoFpB,UAAA;;ADjJY;AAUxB;;;;iBCgJgB,YAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,uBACP,GAAA;EAAA,SAAkB,MAAA,EAAQ,UAAA;AAAA;AAAA,iBAEvB,YAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,KAAA,EAAO,YAAA,CAAW,GAAA,aAAgB,YAAA,aACvB,GAAA;EAAA,SAA8B,MAAA,EAAQ,UAAA;AAAA;;;AAlJrB;AAiB9B;KAmLY,WAAA;EAAA,SAEG,GAAA;EAAA,SAAsB,MAAA,YAAkB,QAAA;AAAA;EAAA,SAGpC,GAAA;EAAA,SAAsB,MAAA,YAAkB,QAAA;AAAA;;;;;;iBAO3C,aAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,MAAA,EAAQ,WAAA,EACR,MAAA,YAAkB,QAAA,eACP,GAAA,IAAO,YAAA;AAAA,iBACJ,aAAA,aACF,MAAA,oBAA0B,MAAA,SAAe,QAAA,GAErD,MAAA,EAAQ,WAAA,GAAc,YAAA,EACtB,MAAA,YAAkB,QAAA,eACP,GAAA,gBAAmB,YAAA;;;;;;iBAiDhB,mBAAA,IAAuB,kBAAA;;;;;AAtPlB;iBAyQL,WAAA,kBACd,QAAA,EAAU,kBAAA,CAAmB,KAAA,UAC3B,IAAA,EAAM,KAAA,EAAO,OAAA,GAAU,iBAAA,KAAoB,gBAAA;;;;;;iBAgB/B,eAAA,IAAmB,aAAA;;;ADtUnC;;;;AAAA,UEQiB,eAAA,sBACM,WAAA,GAAc,WAAA;EAEnC,KAAA;EACA,UAAA,CAAW,QAAA,EAAU,QAAA,CAAS,YAAA;EAC9B,iBAAA,CAAkB,QAAA,EAAU,gBAAA;AAAA;;;;;;;;AFVN;AAUxB;;;;AAAkC;;;KEkBtB,iBAAA;ADVZ;;;;AAAA,KCqBY,kBAAA,mDACW,QAAA,IAAY,iBAAA;ADLnC;;;;;;AAAA,iBCsBgB,SAAA,0CAEE,eAAA,EAEhB,OAAA,EAAS,QAAA,SACT,YAAA,GAAe,OAAA,EAAS,QAAA,KAAa,OAAA,EACrC,MAAA,EAAQ,kBAAA,CAAmB,QAAA,IAC1B,OAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./use-client-
|
|
1
|
+
import{t as e}from"./use-client-D1Y1EDVp.js";import{createContext as t,useCallback as n,useContext as r,useEffect as i,useMemo as a,useState as o,useSyncExternalStore as s}from"react";import{jsx as c}from"react/jsx-runtime";import{isBoundQuery as l}from"@doync/core/internal";const u=t(null);function d({client:e,children:t}){return c(u.Provider,{value:e,children:t})}function f(){let e=r(u);if(e===null)throw Error(`doync: a doync hook was used outside a <DoyncProvider>`);return e}function p(e,t){let n=f(),r=t?.skip??!1,o=D(e),s=o?`|ttl=${t?.ttl??``}|skip=${r}|falsy=1`:`${E(e)}|ttl=${t?.ttl??``}|skip=${r}|falsy=0`,c=a(()=>{if(o)return S;let i=O(e,`useQuery`);return n.subscribe(i,{ttl:t?.ttl,skip:r})},[n,s]);i(()=>(c.retain(),()=>c.release()),[c]);let l=w(c),u=T(c),d=a(()=>o?!1:m(e.query,e.args),[s]);if(o)return[void 0,u];if(c.one!==void 0&&c.one!==d)throw Error(c.one?`doync: the subscribed statement is a one-query (sql.one / findFirst) but this client resolved it as multi-row — the query definition disagrees across the resolve boundary`:`doync: the subscribed statement is multi-row but this client resolved it as a one-query — the query definition disagrees across the resolve boundary`);return[c.one??d?l.length>0?l[0]:void 0:l,u]}function m(e,t){try{return e.resolve({ctx:null,args:t})?.one??!1}catch{return!1}}function h(e){let t=f(),n=D(e),r=a(()=>{if(n)return C;let r=O(e,`useQueryOnce`);return t.once(r)},[t,n?`falsy=1`:`${E(e)}|falsy=0`]),[s,c]=o(`loading`);i(()=>{if(n)return;let e=!0;return c(`loading`),r.server.then(()=>{e&&c(`success`)},()=>{e&&c(`error`)}),()=>{e=!1,r.dispose()}},[r,n]);let l=n?`skipped`:s,u=w(r);return[n?void 0:u,{status:l}]}function g(e,t=[]){let n=f(),r=D(e),o=r?null:typeof e==`function`?e():e,s=o===null?``:typeof o==`string`?o:o.sql,c=o===null||typeof o==`string`?t:o.params??[],l=a(()=>r?S:n.local(s,...c),[n,r?`falsy=1`:`${s}|${JSON.stringify(c)}|falsy=0`]);i(()=>(l.retain(),()=>l.release()),[l]);let u=w(l),d=T(l);return r?[void 0,d]:[u,d]}function _(){let e=f();return s(n(t=>e.onConnectionChange(t),[e]),()=>e.connectionStatus,()=>e.connectionStatus)}function v(e){let t=f();return n((n,r)=>t.mutate(e,n,r),[t,e])}function y(){let e=f();return s(n(t=>e.onSchemaChange(t),[e]),()=>e.schemaStatus,()=>e.schemaStatus)}const b=Object.freeze([]),x=Object.freeze({status:`unknown`}),S={current:()=>b,status:()=>x,onChange:()=>()=>{},retain:()=>{},release:()=>{}},C={current:()=>b,onChange:()=>()=>{},dispose:()=>{},server:new Promise(()=>{})};function w(e){let t=n(t=>e.onChange(t),[e]),r=n(()=>e.current(),[e]);return s(t,r,r)}function T(e){let t=n(t=>e.onChange(t),[e]),r=n(()=>e.status(),[e]);return s(t,r,r)}function E(e){return`${e.query.name??``}|${JSON.stringify(e.args??null)}`}function D(e){return e===!1||e==null}function O(e,t){if(l(e))return e;throw Error(typeof e==`function`?`doync: ${t} expected a BoundQuery — received a function; did you forget to call it?`:`doync: ${t} expected a BoundQuery (produced by queries.…(args)), got ${typeof e==`object`&&e?`an object`:String(e)}`)}export{d as DoyncProvider,e as useClient,_ as useConnectionStatus,f as useDoyncClient,g as useLocalQuery,v as useMutation,p as useQuery,h as useQueryOnce,y as useSchemaStatus};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/provider.tsx","../src/hooks.tsx"],"sourcesContent":["import type { DoyncClient } from '@doync/client'\nimport type { ReactElement, ReactNode } from 'react'\n\nimport { createContext, useContext } from 'react'\n\nconst DoyncContext = createContext<DoyncClient | null>(null)\n\n/** Props for {@link DoyncProvider}. */\nexport interface DoyncProviderProps {\n /** Client from `createWebClient` / `createMobileClient` (or a test double). */\n readonly client: DoyncClient\n readonly children: ReactNode\n}\n\n/** Provide a {@link DoyncClient} to `useQuery`, `useMutation`, and siblings. */\nexport function DoyncProvider({\n client,\n children,\n}: DoyncProviderProps): ReactElement {\n return (\n <DoyncContext.Provider value={client}>{children}</DoyncContext.Provider>\n )\n}\n\n/**\n * {@link DoyncClient} from the nearest {@link DoyncProvider}. Throws if used\n * outside a provider.\n */\nexport function useDoyncClient(): DoyncClient {\n const client = useContext(DoyncContext)\n if (client === null) {\n throw new Error('doync: a doync hook was used outside a <DoyncProvider>')\n }\n return client\n}\n","import type {\n ConnectionStatus,\n FalsyQuery,\n MutationOptions,\n MutationResult,\n OnceView,\n SchemaEvent,\n SubscribeOptions,\n View,\n ViewStatus,\n} from '@doync/client'\nimport type {\n BoundQuery,\n MutationDefinition,\n QueryDefinition,\n SqlValue,\n} from '@doync/core'\n\nimport { isBoundQuery } from '@doync/core/internal'\nimport {\n useCallback,\n useEffect,\n useMemo,\n useState,\n useSyncExternalStore,\n} from 'react'\n\nimport { useDoyncClient } from './provider'\n\n// ── useQuery (subscribe — the norm) ───────────────────────────────────────\n\n/**\n * Options for {@link useQuery}: `ttl` (server warm-grace after unmount) and\n * `skip`. One-ness is inferred from the bound query — there is no `one`\n * option.\n */\nexport type UseQueryOptions = SubscribeOptions\n\n/**\n * Live subscription to a bound query. Pass `queries.issues.open(args)`. Returns\n * `[rows, status]` and updates as local writes and server sync move the rows.\n *\n * - Multi-row queries → `[readonly Row[], status]`\n * - One-row (`` sql.one`…` `` / `findFirst`) → `[Row | undefined, status]`\n * - Falsy (`cond && bound`) → `[undefined, { status: 'unknown' }]` (stable hooks)\n * - `skip: true` → empty shaped like the query (`[]` or `undefined`), status\n * `unknown`\n *\n * Status runs `unknown` → `complete` on first confirmation, or `error` if the\n * subscribe fails. Mount retains the shared handle; unmount releases it.\n */\n\n// Non-falsy one-query.\nexport function useQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, true>,\n options?: UseQueryOptions,\n): [Row | undefined, ViewStatus]\n// Non-falsy multi-row.\nexport function useQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, false>,\n options?: UseQueryOptions,\n): [readonly Row[], ViewStatus]\n// Maybe one-query (argument can be falsy) — `| undefined` only here.\nexport function useQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, true> | FalsyQuery,\n options?: UseQueryOptions,\n): [Row | undefined, ViewStatus]\n// Maybe multi-row — rows themselves gain `| undefined` when no query.\nexport function useQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, false> | FalsyQuery,\n options?: UseQueryOptions,\n): [readonly Row[] | undefined, ViewStatus]\n// Implementation (boolean One + maybe).\nexport function useQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, boolean> | FalsyQuery,\n options?: UseQueryOptions,\n): [readonly Row[] | (Row | undefined) | undefined, ViewStatus] {\n const client = useDoyncClient()\n const skip = options?.skip ?? false\n const falsy = isFalsyQuery(query)\n // Falsy+skip in the key so toggles swap Views without reordering hooks.\n // Creation pure (ADR-0023/Q3); bind pure (ADR-0027) — key name+JSON args.\n const key = falsy\n ? `|ttl=${options?.ttl ?? ''}|skip=${skip}|falsy=1`\n : `${boundQueryKey(query)}|ttl=${options?.ttl ?? ''}|skip=${skip}|falsy=0`\n const view = useMemo((): View<Row> => {\n if (falsy) return INERT_VIEW as unknown as View<Row>\n // BoundQuery through (ADR-0027 / #200); reject truthy impostors.\n const bound = requireBoundQuery(query, 'useQuery')\n return client.subscribe(bound as BoundQuery<Row, boolean>, {\n ttl: options?.ttl,\n skip,\n }) as View<Row>\n // identity is `key`, not object identity\n }, [client, key])\n useEffect(() => {\n view.retain()\n return () => view.release()\n }, [view])\n const rows = useViewRows(view)\n const status = useViewStatus(view)\n // One-ness inferred (#115): resolve locally for immediate shape (topology\n // view.one is late; skipped never resolves). Falsy keep slot only.\n const resolvedOne = useMemo(() => {\n if (falsy) return false\n const bound = query as BoundQuery<Row, boolean>\n return resolvedOneness(bound.query, bound.args)\n }, [key])\n\n // Falsy (ADR-0027): rows undefined for both shapes; after hooks for order.\n // Distinct from skip (shape-preserving empties with a bound query).\n if (falsy) {\n return [undefined, status]\n }\n\n // view.one is authoritative once known — must agree with local resolve or the\n // resolve boundary disagrees (throw). LIMITATION: mixed resolvers\n // (`args.x ? sql.one : sql`) have type-level One=false but runtime agreement,\n // so this guard cannot fire; prefer pure sql.one/findFirst.\n if (view.one !== undefined && view.one !== resolvedOne) {\n throw new Error(\n view.one\n ? 'doync: the subscribed statement is a one-query (sql.one / findFirst) but this client resolved it as multi-row — the query definition disagrees across the resolve boundary'\n : 'doync: the subscribed statement is multi-row but this client resolved it as a one-query — the query definition disagrees across the resolve boundary',\n )\n }\n const one = view.one ?? resolvedOne\n const value = one ? (rows.length > 0 ? (rows[0] as Row) : undefined) : rows\n return [value, status]\n}\n\n/**\n * Local one-ness (#115): same resolve the engine does at subscribe so shape is\n * known before topology `view.one`. Unresolvable args ⇒ false (defer to\n * view.one).\n */\nfunction resolvedOneness(\n query: QueryDefinition<unknown, unknown, boolean>,\n args: unknown,\n): boolean {\n try {\n return query.resolve({ args })?.one ?? false\n } catch {\n return false\n }\n}\n\n// ── useQueryOnce (cache-and-network Once) ─────────────────────────────\n\n/**\n * Network half of a {@link useQueryOnce} read: `loading` → `success` / `error`,\n * or `skipped` when the argument is falsy (no request started).\n */\nexport type OnceStatus = 'loading' | 'success' | 'error' | 'skipped'\n\n/**\n * One-shot cache-and-network read. Rows come from the local replica\n * immediately, then update when the server answer lands. `status` tracks the\n * network half. Falsy argument → `[undefined, { status: 'skipped' }]` with\n * stable hooks. StrictMode-safe (remount keeps the in-flight request).\n */\n// Non-falsy: today's exact rows type.\nexport function useQueryOnce<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, boolean>,\n): [readonly Row[], { readonly status: OnceStatus }]\n// Maybe (argument can be falsy) — rows gain `| undefined` only here.\nexport function useQueryOnce<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, boolean> | FalsyQuery,\n): [readonly Row[] | undefined, { readonly status: OnceStatus }]\nexport function useQueryOnce<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, boolean> | FalsyQuery,\n): [readonly Row[] | undefined, { readonly status: OnceStatus }] {\n const client = useDoyncClient()\n const falsy = isFalsyQuery(query)\n const key = falsy ? 'falsy=1' : `${boundQueryKey(query)}|falsy=0`\n const view = useMemo((): OnceView<Row> => {\n if (falsy) return INERT_ONCE as unknown as OnceView<Row>\n // BoundQuery through (ADR-0027 / #200).\n const bound = requireBoundQuery(query, 'useQueryOnce')\n return client.once(bound as BoundQuery<Row, boolean>) as OnceView<Row>\n }, [client, key])\n // Live network status; falsy returns skipped sync so bound→falsy never flash\n // stale success/error (#197).\n const [liveStatus, setLiveStatus] =\n useState<Exclude<OnceStatus, 'skipped'>>('loading')\n useEffect(() => {\n // Commit owns Once (Q7). Falsy → nothing. Live starts network (cancels\n // StrictMode dispose arm). Cleanup = Warm-tick dispose.\n if (falsy) return\n let active = true\n setLiveStatus('loading')\n view.server.then(\n () => {\n if (active) setLiveStatus('success')\n },\n () => {\n if (active) setLiveStatus('error')\n },\n )\n return () => {\n active = false\n view.dispose()\n }\n }, [view, falsy])\n const status: OnceStatus = falsy ? 'skipped' : liveStatus\n // Always read store (hook order); falsy → undefined.\n const rows = useViewRows(view)\n return [falsy ? undefined : rows, { status }]\n}\n\n// ── useLocalQuery (arbitrary SQL over the replica) ─────────────────────────\n\n/**\n * SQL source for {@link useLocalQuery}: a string, `{ sql, params }`, a callback\n * returning either, or falsy (\"no local read\").\n */\nexport type LocalSource =\n | string\n | { readonly sql: string; readonly params?: readonly SqlValue[] }\n | (() =>\n | string\n | { readonly sql: string; readonly params?: readonly SqlValue[] })\n\n/**\n * Run arbitrary SQL over the local replica reactively (aggregates, joins, …).\n * Re-runs on local commits; never registered with the server — offline-capable\n * and free to the Mirror. Falsy source → `[undefined, { status: 'unknown' }]`.\n */\nexport function useLocalQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n source: LocalSource,\n params?: readonly SqlValue[],\n): [readonly Row[], ViewStatus]\nexport function useLocalQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n source: LocalSource | FalsyQuery,\n params?: readonly SqlValue[],\n): [readonly Row[] | undefined, ViewStatus]\nexport function useLocalQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n source: LocalSource | FalsyQuery,\n params: readonly SqlValue[] = [],\n): [readonly Row[] | undefined, ViewStatus] {\n const client = useDoyncClient()\n const falsy = isFalsyQuery(source)\n const resolved = falsy\n ? null\n : typeof source === 'function'\n ? source()\n : source\n const sql =\n resolved === null\n ? ''\n : typeof resolved === 'string'\n ? resolved\n : resolved.sql\n const sqlParams =\n resolved === null\n ? params\n : typeof resolved === 'string'\n ? params\n : (resolved.params ?? [])\n const key = falsy ? 'falsy=1' : `${sql}|${JSON.stringify(sqlParams)}|falsy=0`\n const view = useMemo((): View<Row> => {\n if (falsy) return INERT_VIEW as unknown as View<Row>\n return client.local<Row>(sql, ...sqlParams)\n }, [client, key])\n useEffect(() => {\n view.retain()\n return () => view.release()\n }, [view])\n // Both stores always (hook order). Live Local is always complete (#104).\n const rows = useViewRows(view)\n const status = useViewStatus(view)\n if (falsy) return [undefined, status]\n return [rows, status]\n}\n\n// ── useConnectionStatus (topology socket health) ──────────────────────────\n\n/**\n * Live {@link ConnectionStatus} to the Mirror: `connecting` / `connected` /\n * `disconnected` / `error` / `needs-auth`. Re-renders on every transition — use\n * for a status pill, offline banner, or needs-auth → logout flow.\n */\nexport function useConnectionStatus(): ConnectionStatus {\n const client = useDoyncClient()\n return useSyncExternalStore(\n useCallback(\n (onChange: () => void) => client.onConnectionChange(onChange),\n [client],\n ),\n () => client.connectionStatus,\n () => client.connectionStatus,\n )\n}\n\n// ── useMutation ────────────────────────────────────────────────────────────\n\n/**\n * Registered mutation as a callable. Call with `args` to apply optimistically\n * and push; returns `{ client, server }` — paint from `client`, await `server`\n * for Origin confirmation.\n */\nexport function useMutation<Input = unknown>(\n mutation: MutationDefinition<Input>,\n): (args: Input, options?: MutationOptions) => MutationResult {\n const client = useDoyncClient()\n return useCallback(\n (args: Input, options?: MutationOptions) =>\n client.mutate(mutation, args, options),\n [client, mutation],\n )\n}\n\n// ── useSchemaStatus ───────────────────────────────────────────────────────\n\n/**\n * Current schema/recovery state, or `null` when nominal. Use for a deploy-skew\n * / recovery banner — re-renders when the state clears too (a one-shot\n * `onSchemaEvent` callback alone would stick after recovery).\n */\nexport function useSchemaStatus(): SchemaEvent | null {\n const client = useDoyncClient()\n return useSyncExternalStore(\n useCallback(\n (onChange: () => void) => client.onSchemaChange(onChange),\n [client],\n ),\n () => client.schemaStatus,\n () => client.schemaStatus,\n )\n}\n\n// ── shared internals ───────────────────────────────────────────────────────\n\n/** Stable empty snapshots (fresh ones tear the store). */\nconst EMPTY_ROWS: readonly Record<string, unknown>[] = Object.freeze([])\nconst UNKNOWN_STATUS: ViewStatus = Object.freeze({ status: 'unknown' })\n\n/**\n * Inert View for falsy useQuery/useLocalQuery (ADR-0027): no desire, empty,\n * unknown, no-op retain. Hook adds undefined-rows (vs skip's shape-preserving\n * empty). Module-stable.\n */\nconst INERT_VIEW: View<Record<string, unknown>> = {\n current: () => EMPTY_ROWS,\n status: () => UNKNOWN_STATUS,\n onChange: () => () => {},\n retain: () => {},\n release: () => {},\n}\n\n/**\n * Inert Once for falsy useQueryOnce (ADR-0027): no network. Hook reports\n * skipped; never awaits server (does not settle).\n */\nconst INERT_ONCE: OnceView<Record<string, unknown>> = {\n current: () => EMPTY_ROWS,\n onChange: () => () => {},\n dispose: () => {},\n // Never settles — unused for the skipped status path.\n server: new Promise(() => {}),\n}\n\n/** Track View rows via useSyncExternalStore (stable current() until change). */\nfunction useViewRows<Row extends Record<string, unknown>>(\n view: View<Row> | OnceView<Row>,\n): readonly Row[] {\n const subscribe = useCallback(\n (onStoreChange: () => void) => view.onChange(onStoreChange),\n [view],\n )\n const getSnapshot = useCallback(() => view.current(), [view])\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\n/**\n * Track {@link ViewStatus} via useSyncExternalStore (#105): same onChange as\n * rows; stable until visible status changes.\n */\nfunction useViewStatus<Row extends Record<string, unknown>>(\n view: View<Row>,\n): ViewStatus {\n const subscribe = useCallback(\n (onStoreChange: () => void) => view.onChange(onStoreChange),\n [view],\n )\n const getSnapshot = useCallback(() => view.status(), [view])\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\n/** Dedup key for a bound-query call (name + args). */\nfunction boundQueryKey(bound: {\n readonly query: { readonly name?: string }\n readonly args: unknown\n}): string {\n return `${bound.query.name ?? ''}|${JSON.stringify(bound.args ?? null)}`\n}\n\n/** `false | null | undefined` — the \"no query\" sentinel (ADR-0027). */\nfunction isFalsyQuery(value: unknown): value is FalsyQuery {\n return value === false || value === null || value === undefined\n}\n\n/**\n * BoundQuery check at the react surface (ADR-0027). Bare function → forget-\n * to-call wording.\n */\nfunction requireBoundQuery(\n value: unknown,\n surface: 'useQuery' | 'useQueryOnce',\n): BoundQuery {\n if (isBoundQuery(value)) return value\n if (typeof value === 'function') {\n throw new Error(\n `doync: ${surface} expected a BoundQuery — received a function; did you forget to call it?`,\n )\n }\n throw new Error(\n `doync: ${surface} expected a BoundQuery (produced by queries.…(args)), got ${typeof value === 'object' && value !== null ? 'an object' : String(value)}`,\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/provider.tsx","../src/hooks.tsx"],"sourcesContent":["import type { DoyncClient } from '@doync/client'\nimport type { ReactElement, ReactNode } from 'react'\n\nimport { createContext, useContext } from 'react'\n\nconst DoyncContext = createContext<DoyncClient | null>(null)\n\n/** Props for {@link DoyncProvider}. */\nexport interface DoyncProviderProps {\n /** Client from `createWebClient` / `createMobileClient` (or a test double). */\n readonly client: DoyncClient\n readonly children: ReactNode\n}\n\n/** Provide a {@link DoyncClient} to `useQuery`, `useMutation`, and siblings. */\nexport function DoyncProvider({\n client,\n children,\n}: DoyncProviderProps): ReactElement {\n return (\n <DoyncContext.Provider value={client}>{children}</DoyncContext.Provider>\n )\n}\n\n/**\n * {@link DoyncClient} from the nearest {@link DoyncProvider}. Throws if used\n * outside a provider.\n */\nexport function useDoyncClient(): DoyncClient {\n const client = useContext(DoyncContext)\n if (client === null) {\n throw new Error('doync: a doync hook was used outside a <DoyncProvider>')\n }\n return client\n}\n","import type {\n ConnectionStatus,\n FalsyQuery,\n MutationOptions,\n MutationResult,\n OnceView,\n SchemaEvent,\n SubscribeOptions,\n View,\n ViewStatus,\n} from '@doync/client'\nimport type {\n BoundQuery,\n MutationDefinition,\n RegisteredQuery,\n SqlValue,\n} from '@doync/core'\n\nimport { isBoundQuery } from '@doync/core/internal'\nimport {\n useCallback,\n useEffect,\n useMemo,\n useState,\n useSyncExternalStore,\n} from 'react'\n\nimport { useDoyncClient } from './provider'\n\n// ── useQuery (subscribe — the norm) ───────────────────────────────────────\n\n/**\n * Options for {@link useQuery}: `ttl` (server warm-grace after unmount) and\n * `skip`. One-ness is inferred from the bound query — there is no `one`\n * option.\n */\nexport type UseQueryOptions = SubscribeOptions\n\n/**\n * Live subscription to a bound query. Pass `queries.issues.open(args)`. Returns\n * `[rows, status]` and updates as local writes and server sync move the rows.\n *\n * - Multi-row queries → `[readonly Row[], status]`\n * - One-row (`` sql.one`…` `` / `findFirst`) → `[Row | undefined, status]`\n * - Falsy (`cond && bound`) → `[undefined, { status: 'unknown' }]` (stable hooks)\n * - `skip: true` → empty shaped like the query (`[]` or `undefined`), status\n * `unknown`\n *\n * Status runs `unknown` → `complete` on first confirmation, or `error` if the\n * subscribe fails. Mount retains the shared handle; unmount releases it.\n */\n\n// Non-falsy one-query.\nexport function useQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, true>,\n options?: UseQueryOptions,\n): [Row | undefined, ViewStatus]\n// Non-falsy multi-row.\nexport function useQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, false>,\n options?: UseQueryOptions,\n): [readonly Row[], ViewStatus]\n// Maybe one-query (argument can be falsy) — `| undefined` only here.\nexport function useQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, true> | FalsyQuery,\n options?: UseQueryOptions,\n): [Row | undefined, ViewStatus]\n// Maybe multi-row — rows themselves gain `| undefined` when no query.\nexport function useQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, false> | FalsyQuery,\n options?: UseQueryOptions,\n): [readonly Row[] | undefined, ViewStatus]\n// Implementation (boolean One + maybe).\nexport function useQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, boolean> | FalsyQuery,\n options?: UseQueryOptions,\n): [readonly Row[] | (Row | undefined), ViewStatus] {\n const client = useDoyncClient()\n const skip = options?.skip ?? false\n const falsy = isFalsyQuery(query)\n // Falsy+skip in the key so toggles swap Views without reordering hooks.\n // Creation pure (ADR-0023/Q3); bind pure (ADR-0027) — key name+JSON args.\n const key = falsy\n ? `|ttl=${options?.ttl ?? ''}|skip=${skip}|falsy=1`\n : `${boundQueryKey(query)}|ttl=${options?.ttl ?? ''}|skip=${skip}|falsy=0`\n const view = useMemo((): View<Row> => {\n if (falsy) return INERT_VIEW as unknown as View<Row>\n // BoundQuery through (ADR-0027 / #200); reject truthy impostors.\n const bound = requireBoundQuery(query, 'useQuery')\n return client.subscribe(bound as BoundQuery<Row, boolean>, {\n ttl: options?.ttl,\n skip,\n }) as View<Row>\n // identity is `key`, not object identity\n }, [client, key])\n useEffect(() => {\n view.retain()\n return () => view.release()\n }, [view])\n const rows = useViewRows(view)\n const status = useViewStatus(view)\n // One-ness inferred (#115): resolve locally for immediate shape (topology\n // view.one is late; skipped never resolves). Falsy keep slot only.\n const resolvedOne = useMemo(() => {\n if (falsy) return false\n return resolvedOneness(query.query, query.args)\n }, [key])\n\n // Falsy (ADR-0027): rows undefined for both shapes; after hooks for order.\n // Distinct from skip (shape-preserving empties with a bound query).\n if (falsy) {\n return [undefined, status]\n }\n\n // view.one is authoritative once known — must agree with local resolve or\n // the resolve boundary disagrees (throw). One-ness is static per definition\n // (mixed resolvers do not compile), so the null-ctx local resolve below is\n // exact whenever it succeeds.\n if (view.one !== undefined && view.one !== resolvedOne) {\n throw new Error(\n view.one\n ? 'doync: the subscribed statement is a one-query (sql.one / findFirst) but this client resolved it as multi-row — the query definition disagrees across the resolve boundary'\n : 'doync: the subscribed statement is multi-row but this client resolved it as a one-query — the query definition disagrees across the resolve boundary',\n )\n }\n const one = view.one ?? resolvedOne\n const value = one ? (rows.length > 0 ? (rows[0] as Row) : undefined) : rows\n return [value, status]\n}\n\n/**\n * Local one-ness (#115): resolve once with `ctx: null` so shape is known before\n * topology `view.one`. Sound because one-ness is static per definition (mixed\n * resolvers do not compile) — any ctx yields the same one-ness. Unresolvable\n * args ⇒ false (defer to view.one).\n */\nfunction resolvedOneness(\n query: RegisteredQuery<unknown, unknown, boolean>,\n args: unknown,\n): boolean {\n try {\n return query.resolve({ ctx: null, args })?.one ?? false\n } catch {\n return false\n }\n}\n\n// ── useQueryOnce (cache-and-network Once) ─────────────────────────────\n\n/**\n * Network half of a {@link useQueryOnce} read: `loading` → `success` / `error`,\n * or `skipped` when the argument is falsy (no request started).\n */\nexport type OnceStatus = 'loading' | 'success' | 'error' | 'skipped'\n\n/**\n * One-shot cache-and-network read. Rows come from the local replica\n * immediately, then update when the server answer lands. `status` tracks the\n * network half. Falsy argument → `[undefined, { status: 'skipped' }]` with\n * stable hooks. StrictMode-safe (remount keeps the in-flight request).\n */\n// Non-falsy: today's exact rows type.\nexport function useQueryOnce<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, boolean>,\n): [readonly Row[], { readonly status: OnceStatus }]\n// Maybe (argument can be falsy) — rows gain `| undefined` only here.\nexport function useQueryOnce<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, boolean> | FalsyQuery,\n): [readonly Row[] | undefined, { readonly status: OnceStatus }]\nexport function useQueryOnce<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n query: BoundQuery<Row, boolean> | FalsyQuery,\n): [readonly Row[] | undefined, { readonly status: OnceStatus }] {\n const client = useDoyncClient()\n const falsy = isFalsyQuery(query)\n const key = falsy ? 'falsy=1' : `${boundQueryKey(query)}|falsy=0`\n const view = useMemo((): OnceView<Row> => {\n if (falsy) return INERT_ONCE as unknown as OnceView<Row>\n // BoundQuery through (ADR-0027 / #200).\n const bound = requireBoundQuery(query, 'useQueryOnce')\n return client.once(bound as BoundQuery<Row, boolean>) as OnceView<Row>\n }, [client, key])\n // Live network status; falsy returns skipped sync so bound→falsy never flash\n // stale success/error (#197).\n const [liveStatus, setLiveStatus] =\n useState<Exclude<OnceStatus, 'skipped'>>('loading')\n useEffect(() => {\n // Commit owns Once (Q7). Falsy → nothing. Live starts network (cancels\n // StrictMode dispose arm). Cleanup = Warm-tick dispose.\n if (falsy) return\n let active = true\n setLiveStatus('loading')\n view.server.then(\n () => {\n if (active) setLiveStatus('success')\n },\n () => {\n if (active) setLiveStatus('error')\n },\n )\n return () => {\n active = false\n view.dispose()\n }\n }, [view, falsy])\n const status: OnceStatus = falsy ? 'skipped' : liveStatus\n // Always read store (hook order); falsy → undefined.\n const rows = useViewRows(view)\n return [falsy ? undefined : rows, { status }]\n}\n\n// ── useLocalQuery (arbitrary SQL over the replica) ─────────────────────────\n\n/**\n * SQL source for {@link useLocalQuery}: a string, `{ sql, params }`, a callback\n * returning either, or falsy (\"no local read\").\n */\nexport type LocalSource =\n | string\n | { readonly sql: string; readonly params?: readonly SqlValue[] }\n | (() =>\n | string\n | { readonly sql: string; readonly params?: readonly SqlValue[] })\n\n/**\n * Run arbitrary SQL over the local replica reactively (aggregates, joins, …).\n * Re-runs on local commits; never registered with the server — offline-capable\n * and free to the Mirror. Falsy source → `[undefined, { status: 'unknown' }]`.\n */\nexport function useLocalQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n source: LocalSource,\n params?: readonly SqlValue[],\n): [readonly Row[], ViewStatus]\nexport function useLocalQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n source: LocalSource | FalsyQuery,\n params?: readonly SqlValue[],\n): [readonly Row[] | undefined, ViewStatus]\nexport function useLocalQuery<\n Row extends Record<string, unknown> = Record<string, SqlValue>,\n>(\n source: LocalSource | FalsyQuery,\n params: readonly SqlValue[] = [],\n): [readonly Row[] | undefined, ViewStatus] {\n const client = useDoyncClient()\n const falsy = isFalsyQuery(source)\n const resolved = falsy\n ? null\n : typeof source === 'function'\n ? source()\n : source\n const sql =\n resolved === null\n ? ''\n : typeof resolved === 'string'\n ? resolved\n : resolved.sql\n const sqlParams =\n resolved === null\n ? params\n : typeof resolved === 'string'\n ? params\n : (resolved.params ?? [])\n const key = falsy ? 'falsy=1' : `${sql}|${JSON.stringify(sqlParams)}|falsy=0`\n const view = useMemo((): View<Row> => {\n if (falsy) return INERT_VIEW as unknown as View<Row>\n return client.local<Row>(sql, ...sqlParams)\n }, [client, key])\n useEffect(() => {\n view.retain()\n return () => view.release()\n }, [view])\n // Both stores always (hook order). Live Local is always complete (#104).\n const rows = useViewRows(view)\n const status = useViewStatus(view)\n if (falsy) return [undefined, status]\n return [rows, status]\n}\n\n// ── useConnectionStatus (topology socket health) ──────────────────────────\n\n/**\n * Live {@link ConnectionStatus} to the Mirror: `connecting` / `connected` /\n * `disconnected` / `error` / `needs-auth`. Re-renders on every transition — use\n * for a status pill, offline banner, or needs-auth → logout flow.\n */\nexport function useConnectionStatus(): ConnectionStatus {\n const client = useDoyncClient()\n return useSyncExternalStore(\n useCallback(\n (onChange: () => void) => client.onConnectionChange(onChange),\n [client],\n ),\n () => client.connectionStatus,\n () => client.connectionStatus,\n )\n}\n\n// ── useMutation ────────────────────────────────────────────────────────────\n\n/**\n * Registered mutation as a callable. Call with `args` to apply optimistically\n * and push; returns `{ client, server }` — paint from `client`, await `server`\n * for Origin confirmation.\n */\nexport function useMutation<Input = unknown>(\n mutation: MutationDefinition<Input, any>,\n): (args: Input, options?: MutationOptions) => MutationResult {\n const client = useDoyncClient()\n return useCallback(\n (args: Input, options?: MutationOptions) =>\n client.mutate(mutation, args, options),\n [client, mutation],\n )\n}\n\n// ── useSchemaStatus ───────────────────────────────────────────────────────\n\n/**\n * Current schema/recovery state, or `null` when nominal. Use for a deploy-skew\n * / recovery banner — re-renders when the state clears too (a one-shot\n * `onSchemaEvent` callback alone would stick after recovery).\n */\nexport function useSchemaStatus(): SchemaEvent | null {\n const client = useDoyncClient()\n return useSyncExternalStore(\n useCallback(\n (onChange: () => void) => client.onSchemaChange(onChange),\n [client],\n ),\n () => client.schemaStatus,\n () => client.schemaStatus,\n )\n}\n\n// ── shared internals ───────────────────────────────────────────────────────\n\n/** Stable empty snapshots (fresh ones tear the store). */\nconst EMPTY_ROWS: readonly Record<string, unknown>[] = Object.freeze([])\nconst UNKNOWN_STATUS: ViewStatus = Object.freeze({ status: 'unknown' })\n\n/**\n * Inert View for falsy useQuery/useLocalQuery (ADR-0027): no desire, empty,\n * unknown, no-op retain. Hook adds undefined-rows (vs skip's shape-preserving\n * empty). Module-stable.\n */\nconst INERT_VIEW: View<Record<string, unknown>> = {\n current: () => EMPTY_ROWS,\n status: () => UNKNOWN_STATUS,\n onChange: () => () => {},\n retain: () => {},\n release: () => {},\n}\n\n/**\n * Inert Once for falsy useQueryOnce (ADR-0027): no network. Hook reports\n * skipped; never awaits server (does not settle).\n */\nconst INERT_ONCE: OnceView<Record<string, unknown>> = {\n current: () => EMPTY_ROWS,\n onChange: () => () => {},\n dispose: () => {},\n // Never settles — unused for the skipped status path.\n server: new Promise(() => {}),\n}\n\n/** Track View rows via useSyncExternalStore (stable current() until change). */\nfunction useViewRows<Row extends Record<string, unknown>>(\n view: View<Row> | OnceView<Row>,\n): readonly Row[] {\n const subscribe = useCallback(\n (onStoreChange: () => void) => view.onChange(onStoreChange),\n [view],\n )\n const getSnapshot = useCallback(() => view.current(), [view])\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\n/**\n * Track {@link ViewStatus} via useSyncExternalStore (#105): same onChange as\n * rows; stable until visible status changes.\n */\nfunction useViewStatus<Row extends Record<string, unknown>>(\n view: View<Row>,\n): ViewStatus {\n const subscribe = useCallback(\n (onStoreChange: () => void) => view.onChange(onStoreChange),\n [view],\n )\n const getSnapshot = useCallback(() => view.status(), [view])\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\n/** Dedup key for a bound-query call (name + args). */\nfunction boundQueryKey(bound: {\n readonly query: { readonly name?: string }\n readonly args: unknown\n}): string {\n return `${bound.query.name ?? ''}|${JSON.stringify(bound.args ?? null)}`\n}\n\n/** `false | null | undefined` — the \"no query\" sentinel (ADR-0027). */\nfunction isFalsyQuery(value: unknown): value is FalsyQuery {\n return value === false || value === null || value === undefined\n}\n\n/**\n * BoundQuery check at the react surface (ADR-0027). Bare function → forget-\n * to-call wording.\n */\nfunction requireBoundQuery(\n value: unknown,\n surface: 'useQuery' | 'useQueryOnce',\n): BoundQuery {\n if (isBoundQuery(value)) return value\n if (typeof value === 'function') {\n throw new Error(\n `doync: ${surface} expected a BoundQuery — received a function; did you forget to call it?`,\n )\n }\n throw new Error(\n `doync: ${surface} expected a BoundQuery (produced by queries.…(args)), got ${typeof value === 'object' && value !== null ? 'an object' : String(value)}`,\n )\n}\n"],"mappings":"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"}
|
package/dist/mobile.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./use-client-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./use-client-Dif9u1-9.cjs");let t=require("@doync/mobile");const n={name:`identity`,url:`throw`,authData:`auth`,logoutBehavior:`logoutBehavior`};function r(r){return e.t(r,t.createMobileClient,n)}exports.useMobileClient=r;
|
package/dist/mobile.d.cts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AuthContext } from "@doync/core";
|
|
1
2
|
import { CreateMobileClientOptions, MobileClient } from "@doync/mobile";
|
|
2
3
|
|
|
3
4
|
//#region src/mobile.d.ts
|
|
@@ -7,7 +8,7 @@ import { CreateMobileClientOptions, MobileClient } from "@doync/mobile";
|
|
|
7
8
|
* a `url` change throws. Pass `null` until the database id is known, then hand
|
|
8
9
|
* the result to `<DoyncProvider>`.
|
|
9
10
|
*/
|
|
10
|
-
declare function useMobileClient<
|
|
11
|
+
declare function useMobileClient<TAuthContext extends AuthContext = AuthContext>(options: CreateMobileClientOptions<TAuthContext> | null): MobileClient<TAuthContext> | null;
|
|
11
12
|
//#endregion
|
|
12
13
|
export { type CreateMobileClientOptions, type MobileClient, useMobileClient };
|
|
13
14
|
//# sourceMappingURL=mobile.d.cts.map
|
package/dist/mobile.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile.d.cts","names":[],"sources":["../src/mobile.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"mobile.d.cts","names":[],"sources":["../src/mobile.ts"],"mappings":";;;;;;;;;;iBAuDgB,eAAA,sBAAqC,WAAA,GAAc,WAAA,EACjE,OAAA,EAAS,yBAAA,CAA0B,YAAA,WAClC,YAAA,CAAa,YAAA"}
|
package/dist/mobile.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CreateMobileClientOptions, MobileClient } from "@doync/mobile";
|
|
2
|
+
import { AuthContext } from "@doync/core";
|
|
2
3
|
|
|
3
4
|
//#region src/mobile.d.ts
|
|
4
5
|
/**
|
|
@@ -7,7 +8,7 @@ import { CreateMobileClientOptions, MobileClient } from "@doync/mobile";
|
|
|
7
8
|
* a `url` change throws. Pass `null` until the database id is known, then hand
|
|
8
9
|
* the result to `<DoyncProvider>`.
|
|
9
10
|
*/
|
|
10
|
-
declare function useMobileClient<
|
|
11
|
+
declare function useMobileClient<TAuthContext extends AuthContext = AuthContext>(options: CreateMobileClientOptions<TAuthContext> | null): MobileClient<TAuthContext> | null;
|
|
11
12
|
//#endregion
|
|
12
13
|
export { type CreateMobileClientOptions, type MobileClient, useMobileClient };
|
|
13
14
|
//# sourceMappingURL=mobile.d.ts.map
|
package/dist/mobile.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile.d.ts","names":[],"sources":["../src/mobile.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"mobile.d.ts","names":[],"sources":["../src/mobile.ts"],"mappings":";;;;;;;;;;iBAuDgB,eAAA,sBAAqC,WAAA,GAAc,WAAA,EACjE,OAAA,EAAS,yBAAA,CAA0B,YAAA,WAClC,YAAA,CAAa,YAAA"}
|
package/dist/mobile.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./use-client-
|
|
1
|
+
import{t as e}from"./use-client-D1Y1EDVp.js";import{createMobileClient as t}from"@doync/mobile";const n={name:`identity`,url:`throw`,authData:`auth`,logoutBehavior:`logoutBehavior`};function r(r){return e(r,t,n)}export{r as useMobileClient};
|
|
2
2
|
//# sourceMappingURL=mobile.js.map
|
package/dist/mobile.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile.js","names":[],"sources":["../src/mobile.ts"],"sourcesContent":["/**\n * `@doync/react/mobile` — {@link useMobileClient}, a thin wrapper around\n * {@link useClient} that supplies `createMobileClient` and the mobile option\n * policy. Imports `@doync/mobile` as an optional peer of `@doync/react`, so a\n * web-only app never pulls it in.\n *\n * ```ts\n * import { useMobileClient } from '@doync/react/mobile'\n *\n * const client = useMobileClient(\n * id\n * ? {\n * name: id,\n * schema,\n * queries,\n * mutations,\n * url: 'wss://example.com/sync',\n *
|
|
1
|
+
{"version":3,"file":"mobile.js","names":[],"sources":["../src/mobile.ts"],"sourcesContent":["/**\n * `@doync/react/mobile` — {@link useMobileClient}, a thin wrapper around\n * {@link useClient} that supplies `createMobileClient` and the mobile option\n * policy. Imports `@doync/mobile` as an optional peer of `@doync/react`, so a\n * web-only app never pulls it in.\n *\n * ```ts\n * import { useMobileClient } from '@doync/react/mobile'\n *\n * const client = useMobileClient(\n * id\n * ? {\n * name: id,\n * schema,\n * queries,\n * mutations,\n * url: 'wss://example.com/sync',\n * authData,\n * ctxValidationSchema,\n * }\n * : null,\n * )\n * // pass client to <DoyncProvider> once non-null\n * ```\n */\n\nimport type { AuthContext } from '@doync/core'\nimport type { CreateMobileClientOptions, MobileClient } from '@doync/mobile'\n\nimport { createMobileClient } from '@doync/mobile'\n\nimport { type ClientOptionPolicy, useClient } from './use-client'\n\n/**\n * Mobile option dispositions (ADR-0034). Schema / queries / mutations (and any\n * monorepo-only injects on `CreateMobileClientOptionsForTest`) are\n * construction- time constants — the hook does not police them (absent from the\n * policy = not compared).\n *\n * `url` is throw-on-change: a config bug cannot silently reconnect elsewhere.\n * There is no `worker` on mobile (single process).\n */\nconst MOBILE_CLIENT_POLICY: ClientOptionPolicy<CreateMobileClientOptions> = {\n name: 'identity',\n url: 'throw',\n authData: 'auth',\n logoutBehavior: 'logoutBehavior',\n}\n\n/**\n * Hold a {@link MobileClient} for as long as `options` is non-null. Auth and\n * logout-behavior changes apply in place; a `name` change recreates the client;\n * a `url` change throws. Pass `null` until the database id is known, then hand\n * the result to `<DoyncProvider>`.\n */\nexport function useMobileClient<TAuthContext extends AuthContext = AuthContext>(\n options: CreateMobileClientOptions<TAuthContext> | null,\n): MobileClient<TAuthContext> | null {\n return useClient(options, createMobileClient, MOBILE_CLIENT_POLICY)\n}\n\nexport type { CreateMobileClientOptions, MobileClient }\n"],"mappings":"gGA0CA,MAAM,EAAsE,CAC1E,KAAM,WACN,IAAK,QACL,SAAU,OACV,eAAgB,gBAClB,EAQA,SAAgB,EACd,EACmC,CACnC,OAAO,EAAU,EAAS,EAAoB,CAAoB,CACpE"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{useLayoutEffect as e,useRef as t,useState as n}from"react";function r(r,a,s){let[c,l]=n(null),u=t({client:null,options:null}),d=t(a);d.current=a;let f=t(s);return f.current=s,e(()=>{let e=u.current.options,t=r,n=u.current.client,a=d.current,s=f.current;if(t!==null&&e!==null&&n!==null){if(o(e,t,s)){u.current.options=t;return}let r=i(e,t,n,a,s);r===null?u.current.options=t:(u.current={client:r,options:t},l(r));return}if(t!==null&&(e===null||n===null)){let e=a(t);u.current={client:e,options:t},l(e);return}t===null&&n!==null&&(n.close(),u.current={client:null,options:null},l(null))}),e(()=>()=>{u.current.client?.close(),u.current={client:null,options:null}},[]),c}function i(e,t,n,r,i){for(let n of l(i))if(i[n]===`throw`&&!s(e[n],t[n]))throw Error(`doync: useClient cannot change ${String(n)} after creation — close and recreate with the new value`);for(let a of l(i))if(i[a]===`identity`&&!s(e[a],t[a]))return n.close(),r(t);let o=a(e,t,i);o!==void 0&&n.updateAuth(o);for(let r of l(i))if(i[r]===`logoutBehavior`&&!s(e[r],t[r])){let e=t[r];e!==void 0&&n.setLogoutBehavior(e)}return null}function a(e,t,n){let r=!1,i,a=!1;for(let o of l(n))n[o]===`auth`&&(a=!0,s(e[o],t[o],!0)||(r=!0),i=t[o]);return!a||!r?null:i}function o(e,t,n){for(let r of l(n)){let i=n[r];if(i===void 0||i===`ignore`)continue;let a=i===`auth`;if(!s(e[r],t[r],a))return!1}return!0}function s(e,t,n=!1){return n?c(e)===c(t):Object.is(e,t)}function c(e){try{return JSON.stringify(e)??`undefined`}catch{return`\0ref:${typeof e}`}}function l(e){return Object.keys(e)}export{r as t};
|
|
2
|
+
//# sourceMappingURL=use-client-D1Y1EDVp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-client-D1Y1EDVp.js","names":[],"sources":["../src/use-client.ts"],"sourcesContent":["/**\n * Generic client lifecycle hook. Holds a client for as long as the options bag\n * is non-null and routes option changes onto `createClient` / `close` /\n * `updateAuth` / `setLogoutBehavior`, or throws — never invents its own\n * lifecycle. Platform wrappers (`useMobileClient`, `useWebClient`) supply the\n * factory and a {@link ClientOptionPolicy}; this module imports no platform\n * package.\n *\n * Options are compared by value (fresh object literals every render are fine;\n * no `useMemo` required). `authData` is compared by JSON. StrictMode\n * double-mount is create → close → create and is safe: ownership lives in a\n * layout-effect cleanup, not in render.\n */\n\nimport type { AuthData, LogoutBehavior } from '@doync/client'\nimport type { AuthContext } from '@doync/core'\n\nimport { useLayoutEffect, useRef, useState } from 'react'\n\n/**\n * Minimum call surface the hook routes onto. Platform clients (`MobileClient`,\n * `WebClient`) both satisfy this; tests inject a fake.\n */\nexport interface ClientLifecycle<\n TAuthContext extends AuthContext = AuthContext,\n> {\n close(): void\n updateAuth(authData: AuthData<TAuthContext> | null): void\n setLogoutBehavior(behavior: LogoutBehavior): void\n}\n\n/**\n * How a single options-bag key is handled when its value changes between\n * renders. Keys absent from the policy are not compared (construction-only\n * values like `schema` / `queries` — module-level constants in real apps).\n *\n * - `identity` — close the old client and create a new one (e.g. a different\n * database `name` is a different client; no in-place path).\n * - `auth` — `authData` feeds a single `updateAuth` call (compared by JSON).\n * - `logoutBehavior` — call `setLogoutBehavior`.\n * - `throw` — value change throws (e.g. `url`). Checked before `identity`, so a\n * same-render name+url swap cannot silently reconnect elsewhere.\n * - `ignore` — captured at creation; later changes are ignored (e.g. web's\n * `worker` factory, whose required inline-literal form makes the reference\n * unstable across renders).\n */\nexport type OptionDisposition =\n | 'identity'\n | 'auth'\n | 'logoutBehavior'\n | 'throw'\n | 'ignore'\n\n/**\n * Per-platform table mapping option keys to {@link OptionDisposition}. Keys not\n * listed are not compared; unknown keys on the options bag are ignored.\n */\nexport type ClientOptionPolicy<TOptions extends object> = {\n readonly [K in keyof TOptions]?: OptionDisposition\n}\n\n/** Live ownership slot shared by the diff effect and the unmount cleanup. */\ninterface Owned<TOptions, TClient> {\n client: TClient | null\n options: TOptions | null\n}\n\n// ADR-0034 / closeio/doync#281 — routes onto create/close/updateAuth/\n// setLogoutBehavior; throw-before-identity; name-swap recreates.\n/**\n * Hold a client for as long as `options` is non-null. Pass `null` until the\n * database id is known (e.g. after a create-session mutation acks). Option\n * changes route onto client verbs per `policy` — see\n * {@link OptionDisposition}.\n */\nexport function useClient<\n TOptions extends object,\n TClient extends ClientLifecycle,\n>(\n options: TOptions | null,\n createClient: (options: TOptions) => TClient,\n policy: ClientOptionPolicy<TOptions>,\n): TClient | null {\n const [client, setClient] = useState<TClient | null>(null)\n // Ownership lives in a ref so the unmount cleanup and the diff path share one\n // source of truth without putting unstable identities into effect deps.\n const owned = useRef<Owned<TOptions, TClient>>({\n client: null,\n options: null,\n })\n const createRef = useRef(createClient)\n createRef.current = createClient\n const policyRef = useRef(policy)\n policyRef.current = policy\n\n // Diff-and-route runs after every commit. Equal-by-value options are a no-op\n // so a fresh options literal every render neither recreates nor re-verbs.\n // Creation/teardown ownership lives here — NOT in render — so StrictMode's\n // double-mount is create → cleanup-close → create, and nothing leaks.\n useLayoutEffect(() => {\n const prev = owned.current.options\n const next = options\n const current = owned.current.client\n const create = createRef.current\n const pol = policyRef.current\n\n if (next !== null && prev !== null && current !== null) {\n if (optionsEqualByPolicy(prev, next, pol)) {\n // Keep the latest reference so subsequent diffs start from what the\n // caller last passed (in case a nested object was mutated — rare; the\n // equality check already saw them as equal).\n owned.current.options = next\n return\n }\n const created = applyChange(prev, next, current, create, pol)\n if (created !== null) {\n owned.current = { client: created, options: next }\n setClient(created)\n } else {\n // In-place verb path: live client kept, snapshot advances.\n owned.current.options = next\n }\n return\n }\n\n if (next !== null && (prev === null || current === null)) {\n // null → options (or remount after StrictMode cleanup cleared the refs).\n const created = create(next)\n owned.current = { client: created, options: next }\n setClient(created)\n return\n }\n\n if (next === null && current !== null) {\n // options → null: close and drop.\n current.close()\n owned.current = { client: null, options: null }\n setClient(null)\n }\n })\n\n // Unmount-only cleanup. Separate effect so the diff path above can re-run on\n // every options-change commit WITHOUT closing between equal renders.\n useLayoutEffect(() => {\n return () => {\n owned.current.client?.close()\n owned.current = { client: null, options: null }\n }\n }, [])\n\n return client\n}\n\n// ── internals ──────────────────────────────────────────────────────────────\n\n/**\n * Route a non-equal options change onto existing verbs. Returns a freshly\n * created client when identity keys moved (after closing the old one); returns\n * null when the live client was kept (auth / logoutBehavior routing, or throw\n * aborted before any mutation — throw propagates).\n */\nfunction applyChange<TOptions extends object, TClient extends ClientLifecycle>(\n prev: TOptions,\n next: TOptions,\n current: TClient,\n create: (options: TOptions) => TClient,\n policy: ClientOptionPolicy<TOptions>,\n): TClient | null {\n // Throw-on-change FIRST (url): a config bug must never silently reconnect\n // elsewhere, even when bundled with a name swap that would otherwise rebuild.\n for (const key of policyKeys(policy)) {\n if (policy[key] !== 'throw') continue\n if (!valueEqual(prev[key], next[key])) {\n throw new Error(\n `doync: useClient cannot change ${String(key)} after creation — close and recreate with the new value`,\n )\n }\n }\n\n // Identity keys — a name swap is a different Client; no in-place path. Fresh\n // create carries the full next bag (auth/logout already in the new options).\n for (const key of policyKeys(policy)) {\n if (policy[key] !== 'identity') continue\n if (!valueEqual(prev[key], next[key])) {\n current.close()\n return create(next)\n }\n }\n\n // authData — one updateAuth if it drifted (compared by JSON).\n const authData = resolveAuthData(prev, next, policy)\n if (authData !== undefined) current.updateAuth(authData)\n\n // logoutBehavior — only call when it actually moved.\n for (const key of policyKeys(policy)) {\n if (policy[key] !== 'logoutBehavior') continue\n if (!valueEqual(prev[key], next[key])) {\n const behavior = next[key] as LogoutBehavior | undefined\n if (behavior !== undefined) current.setLogoutBehavior(behavior)\n }\n }\n\n return null\n}\n\n/**\n * Returns the next `authData` when any policy-`auth` key moved, else\n * `undefined` (no call). `updateAuth` receives the whole next value (complete\n * identity or `null`); there is no patch semantics.\n */\nfunction resolveAuthData<TOptions extends object>(\n prev: TOptions,\n next: TOptions,\n policy: ClientOptionPolicy<TOptions>,\n): AuthData | null {\n let changed = false\n let nextAuth: unknown\n let sawAuth = false\n for (const key of policyKeys(policy)) {\n if (policy[key] !== 'auth') continue\n sawAuth = true\n if (!valueEqual(prev[key], next[key], true)) changed = true\n nextAuth = next[key]\n }\n if (!sawAuth || !changed) return null\n return nextAuth as AuthData | null\n}\n\nfunction optionsEqualByPolicy<TOptions extends object>(\n a: TOptions,\n b: TOptions,\n policy: ClientOptionPolicy<TOptions>,\n): boolean {\n for (const key of policyKeys(policy)) {\n const disposition = policy[key]\n if (disposition === undefined || disposition === 'ignore') continue\n const serialize = disposition === 'auth'\n if (!valueEqual(a[key], b[key], serialize)) return false\n }\n return true\n}\n\nfunction valueEqual(a: unknown, b: unknown, serialize = false): boolean {\n // authData compared serialized (shared-hub posture, ADR-0034) when asked.\n if (serialize) return jsonStable(a) === jsonStable(b)\n return Object.is(a, b)\n}\n\nfunction jsonStable(value: unknown): string {\n try {\n return JSON.stringify(value) ?? 'undefined'\n } catch {\n // Non-serializable value: fall back to type tag so a throw here never\n // bubbles out of a compare. Reference identity is lost — non-JSON auth is\n // already outside the hub's contract.\n return `\\0ref:${typeof value}`\n }\n}\n\nfunction policyKeys<TOptions extends object>(\n policy: ClientOptionPolicy<TOptions>,\n): (keyof TOptions)[] {\n return Object.keys(policy) as (keyof TOptions)[]\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
let e=require("react");function t(t,r,a){let[o,s]=(0,e.useState)(null),c=(0,e.useRef)({client:null,options:null}),l=(0,e.useRef)(r);l.current=r;let u=(0,e.useRef)(a);return u.current=a,(0,e.useLayoutEffect)(()=>{let e=c.current.options,r=t,a=c.current.client,o=l.current,d=u.current;if(r!==null&&e!==null&&a!==null){if(i(e,r,d)){c.current.options=r;return}let t=n(e,r,a,o,d);t===null?c.current.options=r:(c.current={client:t,options:r},s(t));return}if(r!==null&&(e===null||a===null)){let e=o(r);c.current={client:e,options:r},s(e);return}r===null&&a!==null&&(a.close(),c.current={client:null,options:null},s(null))}),(0,e.useLayoutEffect)(()=>()=>{c.current.client?.close(),c.current={client:null,options:null}},[]),o}function n(e,t,n,i,o){for(let n of s(o))if(o[n]===`throw`&&!a(e[n],t[n]))throw Error(`doync: useClient cannot change ${String(n)} after creation — close and recreate with the new value`);for(let r of s(o))if(o[r]===`identity`&&!a(e[r],t[r]))return n.close(),i(t);let c=r(e,t,o);c!==
|
|
1
|
+
let e=require("react");function t(t,r,a){let[o,s]=(0,e.useState)(null),c=(0,e.useRef)({client:null,options:null}),l=(0,e.useRef)(r);l.current=r;let u=(0,e.useRef)(a);return u.current=a,(0,e.useLayoutEffect)(()=>{let e=c.current.options,r=t,a=c.current.client,o=l.current,d=u.current;if(r!==null&&e!==null&&a!==null){if(i(e,r,d)){c.current.options=r;return}let t=n(e,r,a,o,d);t===null?c.current.options=r:(c.current={client:t,options:r},s(t));return}if(r!==null&&(e===null||a===null)){let e=o(r);c.current={client:e,options:r},s(e);return}r===null&&a!==null&&(a.close(),c.current={client:null,options:null},s(null))}),(0,e.useLayoutEffect)(()=>()=>{c.current.client?.close(),c.current={client:null,options:null}},[]),o}function n(e,t,n,i,o){for(let n of s(o))if(o[n]===`throw`&&!a(e[n],t[n]))throw Error(`doync: useClient cannot change ${String(n)} after creation — close and recreate with the new value`);for(let r of s(o))if(o[r]===`identity`&&!a(e[r],t[r]))return n.close(),i(t);let c=r(e,t,o);c!==void 0&&n.updateAuth(c);for(let r of s(o))if(o[r]===`logoutBehavior`&&!a(e[r],t[r])){let e=t[r];e!==void 0&&n.setLogoutBehavior(e)}return null}function r(e,t,n){let r=!1,i,o=!1;for(let c of s(n))n[c]===`auth`&&(o=!0,a(e[c],t[c],!0)||(r=!0),i=t[c]);return!o||!r?null:i}function i(e,t,n){for(let r of s(n)){let i=n[r];if(i===void 0||i===`ignore`)continue;let o=i===`auth`;if(!a(e[r],t[r],o))return!1}return!0}function a(e,t,n=!1){return n?o(e)===o(t):Object.is(e,t)}function o(e){try{return JSON.stringify(e)??`undefined`}catch{return`\0ref:${typeof e}`}}function s(e){return Object.keys(e)}Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return t}});
|
package/dist/web.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./use-client-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./use-client-Dif9u1-9.cjs");let t=require("@doync/web");const n={name:`identity`,url:`throw`,authData:`auth`,logoutBehavior:`logoutBehavior`,worker:`ignore`};function r(r){return e.t(r,t.createWebClient,n)}exports.useWebClient=r;
|
package/dist/web.d.cts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AuthContext } from "@doync/core";
|
|
1
2
|
import { CreateWebClientOptions, WebClient } from "@doync/web";
|
|
2
3
|
|
|
3
4
|
//#region src/web.d.ts
|
|
@@ -7,7 +8,7 @@ import { CreateWebClientOptions, WebClient } from "@doync/web";
|
|
|
7
8
|
* a `url` change throws; `worker` is fixed at creation. Pass `null` until the
|
|
8
9
|
* database id is known, then hand the result to `<DoyncProvider>`.
|
|
9
10
|
*/
|
|
10
|
-
declare function useWebClient<
|
|
11
|
+
declare function useWebClient<TAuthContext extends AuthContext = AuthContext>(options: CreateWebClientOptions<TAuthContext> | null): WebClient<TAuthContext> | null;
|
|
11
12
|
//#endregion
|
|
12
13
|
export { type CreateWebClientOptions, type WebClient, useWebClient };
|
|
13
14
|
//# sourceMappingURL=web.d.cts.map
|
package/dist/web.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.d.cts","names":[],"sources":["../src/web.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"web.d.cts","names":[],"sources":["../src/web.ts"],"mappings":";;;;;;;;;;iBA2DgB,YAAA,sBAAkC,WAAA,GAAc,WAAA,EAC9D,OAAA,EAAS,sBAAA,CAAuB,YAAA,WAC/B,SAAA,CAAU,YAAA"}
|
package/dist/web.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CreateWebClientOptions, WebClient } from "@doync/web";
|
|
2
|
+
import { AuthContext } from "@doync/core";
|
|
2
3
|
|
|
3
4
|
//#region src/web.d.ts
|
|
4
5
|
/**
|
|
@@ -7,7 +8,7 @@ import { CreateWebClientOptions, WebClient } from "@doync/web";
|
|
|
7
8
|
* a `url` change throws; `worker` is fixed at creation. Pass `null` until the
|
|
8
9
|
* database id is known, then hand the result to `<DoyncProvider>`.
|
|
9
10
|
*/
|
|
10
|
-
declare function useWebClient<
|
|
11
|
+
declare function useWebClient<TAuthContext extends AuthContext = AuthContext>(options: CreateWebClientOptions<TAuthContext> | null): WebClient<TAuthContext> | null;
|
|
11
12
|
//#endregion
|
|
12
13
|
export { type CreateWebClientOptions, type WebClient, useWebClient };
|
|
13
14
|
//# sourceMappingURL=web.d.ts.map
|
package/dist/web.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.d.ts","names":[],"sources":["../src/web.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"web.d.ts","names":[],"sources":["../src/web.ts"],"mappings":";;;;;;;;;;iBA2DgB,YAAA,sBAAkC,WAAA,GAAc,WAAA,EAC9D,OAAA,EAAS,sBAAA,CAAuB,YAAA,WAC/B,SAAA,CAAU,YAAA"}
|
package/dist/web.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./use-client-
|
|
1
|
+
import{t as e}from"./use-client-D1Y1EDVp.js";import{createWebClient as t}from"@doync/web";const n={name:`identity`,url:`throw`,authData:`auth`,logoutBehavior:`logoutBehavior`,worker:`ignore`};function r(r){return e(r,t,n)}export{r as useWebClient};
|
|
2
2
|
//# sourceMappingURL=web.js.map
|
package/dist/web.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.js","names":[],"sources":["../src/web.ts"],"sourcesContent":["/**\n * `@doync/react/web` — {@link useWebClient}, a thin wrapper around\n * {@link useClient} that supplies `createWebClient` and the web option policy.\n * Imports `@doync/web` as an optional peer of `@doync/react`, so a mobile-only\n * app never pulls it in.\n *\n * ```ts\n * import { useWebClient } from '@doync/react/web'\n *\n * const client = useWebClient(\n * id\n * ? {\n * name: id,\n * worker: () =>\n * new Worker(new URL('./worker.ts', import.meta.url), {\n * type: 'module',\n * }),\n * url: 'wss://example.com/sync',\n *
|
|
1
|
+
{"version":3,"file":"web.js","names":[],"sources":["../src/web.ts"],"sourcesContent":["/**\n * `@doync/react/web` — {@link useWebClient}, a thin wrapper around\n * {@link useClient} that supplies `createWebClient` and the web option policy.\n * Imports `@doync/web` as an optional peer of `@doync/react`, so a mobile-only\n * app never pulls it in.\n *\n * ```ts\n * import { useWebClient } from '@doync/react/web'\n *\n * const client = useWebClient(\n * id\n * ? {\n * name: id,\n * worker: () =>\n * new Worker(new URL('./worker.ts', import.meta.url), {\n * type: 'module',\n * }),\n * url: 'wss://example.com/sync',\n * authData,\n * ctxValidationSchema,\n * }\n * : null,\n * )\n * // pass client to <DoyncProvider> once non-null\n * ```\n */\n\nimport type { AuthContext } from '@doync/core'\nimport type { CreateWebClientOptions, WebClient } from '@doync/web'\n\nimport { createWebClient } from '@doync/web'\n\nimport { type ClientOptionPolicy, useClient } from './use-client'\n\n/**\n * Web option dispositions (ADR-0034). Schema / queries / mutations (and any\n * monorepo-only injects on `CreateWebClientOptionsForTest`) are construction-\n * time constants — the hook does not police them (absent from the policy = not\n * compared).\n *\n * `url` is throw-on-change: a config bug cannot silently reconnect elsewhere.\n * `worker` is IGNORE: captured at creation; later changes are ignored — its\n * required inline-literal form makes the reference unstable across renders.\n * `ctxValidationSchema` is fixed at creation (construction-time constant).\n */\nconst WEB_CLIENT_POLICY: ClientOptionPolicy<CreateWebClientOptions> = {\n name: 'identity',\n url: 'throw',\n authData: 'auth',\n logoutBehavior: 'logoutBehavior',\n worker: 'ignore',\n}\n\n/**\n * Hold a {@link WebClient} for as long as `options` is non-null. Auth and\n * logout-behavior changes apply in place; a `name` change recreates the client;\n * a `url` change throws; `worker` is fixed at creation. Pass `null` until the\n * database id is known, then hand the result to `<DoyncProvider>`.\n */\nexport function useWebClient<TAuthContext extends AuthContext = AuthContext>(\n options: CreateWebClientOptions<TAuthContext> | null,\n): WebClient<TAuthContext> | null {\n return useClient(options, createWebClient, WEB_CLIENT_POLICY)\n}\n\nexport type { CreateWebClientOptions, WebClient }\n"],"mappings":"0FA6CA,MAAM,EAAgE,CACpE,KAAM,WACN,IAAK,QACL,SAAU,OACV,eAAgB,iBAChB,OAAQ,QACV,EAQA,SAAgB,EACd,EACgC,CAChC,OAAO,EAAU,EAAS,EAAiB,CAAiB,CAC9D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doync/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "doync React adapter: thin useQuery / useQueryOnce / useLocalQuery / useMutation hooks over the client call surface",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"doync",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@doync/client": "0.1.
|
|
71
|
-
"@doync/core": "0.1.
|
|
70
|
+
"@doync/client": "0.1.1",
|
|
71
|
+
"@doync/core": "0.1.1"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@testing-library/react": "^16.1.0",
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
"tsdown": "^0.22.3",
|
|
81
81
|
"typescript": "^7.0.2",
|
|
82
82
|
"vitest": "4.1.9",
|
|
83
|
-
"@doync/mobile": "0.1.
|
|
84
|
-
"@doync/web": "0.1.
|
|
83
|
+
"@doync/mobile": "0.1.1",
|
|
84
|
+
"@doync/web": "0.1.1"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"@doync/mobile": "0.x",
|
package/src/hooks.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
import type {
|
|
13
13
|
BoundQuery,
|
|
14
14
|
MutationDefinition,
|
|
15
|
-
|
|
15
|
+
RegisteredQuery,
|
|
16
16
|
SqlValue,
|
|
17
17
|
} from '@doync/core'
|
|
18
18
|
|
|
@@ -84,7 +84,7 @@ export function useQuery<
|
|
|
84
84
|
>(
|
|
85
85
|
query: BoundQuery<Row, boolean> | FalsyQuery,
|
|
86
86
|
options?: UseQueryOptions,
|
|
87
|
-
): [readonly Row[] | (Row | undefined)
|
|
87
|
+
): [readonly Row[] | (Row | undefined), ViewStatus] {
|
|
88
88
|
const client = useDoyncClient()
|
|
89
89
|
const skip = options?.skip ?? false
|
|
90
90
|
const falsy = isFalsyQuery(query)
|
|
@@ -113,8 +113,7 @@ export function useQuery<
|
|
|
113
113
|
// view.one is late; skipped never resolves). Falsy keep slot only.
|
|
114
114
|
const resolvedOne = useMemo(() => {
|
|
115
115
|
if (falsy) return false
|
|
116
|
-
|
|
117
|
-
return resolvedOneness(bound.query, bound.args)
|
|
116
|
+
return resolvedOneness(query.query, query.args)
|
|
118
117
|
}, [key])
|
|
119
118
|
|
|
120
119
|
// Falsy (ADR-0027): rows undefined for both shapes; after hooks for order.
|
|
@@ -123,10 +122,10 @@ export function useQuery<
|
|
|
123
122
|
return [undefined, status]
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
// view.one is authoritative once known — must agree with local resolve or
|
|
127
|
-
// resolve boundary disagrees (throw).
|
|
128
|
-
// (
|
|
129
|
-
//
|
|
125
|
+
// view.one is authoritative once known — must agree with local resolve or
|
|
126
|
+
// the resolve boundary disagrees (throw). One-ness is static per definition
|
|
127
|
+
// (mixed resolvers do not compile), so the null-ctx local resolve below is
|
|
128
|
+
// exact whenever it succeeds.
|
|
130
129
|
if (view.one !== undefined && view.one !== resolvedOne) {
|
|
131
130
|
throw new Error(
|
|
132
131
|
view.one
|
|
@@ -140,16 +139,17 @@ export function useQuery<
|
|
|
140
139
|
}
|
|
141
140
|
|
|
142
141
|
/**
|
|
143
|
-
* Local one-ness (#115):
|
|
144
|
-
*
|
|
145
|
-
*
|
|
142
|
+
* Local one-ness (#115): resolve once with `ctx: null` so shape is known before
|
|
143
|
+
* topology `view.one`. Sound because one-ness is static per definition (mixed
|
|
144
|
+
* resolvers do not compile) — any ctx yields the same one-ness. Unresolvable
|
|
145
|
+
* args ⇒ false (defer to view.one).
|
|
146
146
|
*/
|
|
147
147
|
function resolvedOneness(
|
|
148
|
-
query:
|
|
148
|
+
query: RegisteredQuery<unknown, unknown, boolean>,
|
|
149
149
|
args: unknown,
|
|
150
150
|
): boolean {
|
|
151
151
|
try {
|
|
152
|
-
return query.resolve({ args })?.one ?? false
|
|
152
|
+
return query.resolve({ ctx: null, args })?.one ?? false
|
|
153
153
|
} catch {
|
|
154
154
|
return false
|
|
155
155
|
}
|
|
@@ -322,7 +322,7 @@ export function useConnectionStatus(): ConnectionStatus {
|
|
|
322
322
|
* for Origin confirmation.
|
|
323
323
|
*/
|
|
324
324
|
export function useMutation<Input = unknown>(
|
|
325
|
-
mutation: MutationDefinition<Input>,
|
|
325
|
+
mutation: MutationDefinition<Input, any>,
|
|
326
326
|
): (args: Input, options?: MutationOptions) => MutationResult {
|
|
327
327
|
const client = useDoyncClient()
|
|
328
328
|
return useCallback(
|
package/src/mobile.ts
CHANGED
|
@@ -15,9 +15,8 @@
|
|
|
15
15
|
* queries,
|
|
16
16
|
* mutations,
|
|
17
17
|
* url: 'wss://example.com/sync',
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* ctx,
|
|
18
|
+
* authData,
|
|
19
|
+
* ctxValidationSchema,
|
|
21
20
|
* }
|
|
22
21
|
* : null,
|
|
23
22
|
* )
|
|
@@ -25,11 +24,12 @@
|
|
|
25
24
|
* ```
|
|
26
25
|
*/
|
|
27
26
|
|
|
27
|
+
import type { AuthContext } from '@doync/core'
|
|
28
28
|
import type { CreateMobileClientOptions, MobileClient } from '@doync/mobile'
|
|
29
29
|
|
|
30
30
|
import { createMobileClient } from '@doync/mobile'
|
|
31
31
|
|
|
32
|
-
import {
|
|
32
|
+
import { type ClientOptionPolicy, useClient } from './use-client'
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Mobile option dispositions (ADR-0034). Schema / queries / mutations (and any
|
|
@@ -43,9 +43,7 @@ import { useClient, type ClientOptionPolicy } from './use-client'
|
|
|
43
43
|
const MOBILE_CLIENT_POLICY: ClientOptionPolicy<CreateMobileClientOptions> = {
|
|
44
44
|
name: 'identity',
|
|
45
45
|
url: 'throw',
|
|
46
|
-
|
|
47
|
-
userId: 'auth',
|
|
48
|
-
ctx: 'auth',
|
|
46
|
+
authData: 'auth',
|
|
49
47
|
logoutBehavior: 'logoutBehavior',
|
|
50
48
|
}
|
|
51
49
|
|
|
@@ -55,14 +53,10 @@ const MOBILE_CLIENT_POLICY: ClientOptionPolicy<CreateMobileClientOptions> = {
|
|
|
55
53
|
* a `url` change throws. Pass `null` until the database id is known, then hand
|
|
56
54
|
* the result to `<DoyncProvider>`.
|
|
57
55
|
*/
|
|
58
|
-
export function useMobileClient<
|
|
59
|
-
options: CreateMobileClientOptions<
|
|
60
|
-
): MobileClient | null {
|
|
61
|
-
return useClient(
|
|
62
|
-
options as CreateMobileClientOptions | null,
|
|
63
|
-
createMobileClient,
|
|
64
|
-
MOBILE_CLIENT_POLICY as ClientOptionPolicy<CreateMobileClientOptions>,
|
|
65
|
-
)
|
|
56
|
+
export function useMobileClient<TAuthContext extends AuthContext = AuthContext>(
|
|
57
|
+
options: CreateMobileClientOptions<TAuthContext> | null,
|
|
58
|
+
): MobileClient<TAuthContext> | null {
|
|
59
|
+
return useClient(options, createMobileClient, MOBILE_CLIENT_POLICY)
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
export type { CreateMobileClientOptions, MobileClient }
|
package/src/use-client.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generic client lifecycle hook. Holds a client for as long as the options bag
|
|
3
3
|
* is non-null and routes option changes onto `createClient` / `close` /
|
|
4
|
-
* `
|
|
5
|
-
* Platform wrappers (`useMobileClient`, `useWebClient`) supply the
|
|
6
|
-
* a {@link ClientOptionPolicy}; this module imports no platform
|
|
4
|
+
* `updateAuth` / `setLogoutBehavior`, or throws — never invents its own
|
|
5
|
+
* lifecycle. Platform wrappers (`useMobileClient`, `useWebClient`) supply the
|
|
6
|
+
* factory and a {@link ClientOptionPolicy}; this module imports no platform
|
|
7
|
+
* package.
|
|
7
8
|
*
|
|
8
9
|
* Options are compared by value (fresh object literals every render are fine;
|
|
9
|
-
* no `useMemo` required). `
|
|
10
|
-
* create → close → create and is safe: ownership lives in a
|
|
11
|
-
* cleanup, not in render.
|
|
10
|
+
* no `useMemo` required). `authData` is compared by JSON. StrictMode
|
|
11
|
+
* double-mount is create → close → create and is safe: ownership lives in a
|
|
12
|
+
* layout-effect cleanup, not in render.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
import type { LogoutBehavior } from '@doync/client'
|
|
15
|
+
import type { AuthData, LogoutBehavior } from '@doync/client'
|
|
16
|
+
import type { AuthContext } from '@doync/core'
|
|
15
17
|
|
|
16
18
|
import { useLayoutEffect, useRef, useState } from 'react'
|
|
17
19
|
|
|
@@ -19,13 +21,11 @@ import { useLayoutEffect, useRef, useState } from 'react'
|
|
|
19
21
|
* Minimum call surface the hook routes onto. Platform clients (`MobileClient`,
|
|
20
22
|
* `WebClient`) both satisfy this; tests inject a fake.
|
|
21
23
|
*/
|
|
22
|
-
export interface ClientLifecycle
|
|
24
|
+
export interface ClientLifecycle<
|
|
25
|
+
TAuthContext extends AuthContext = AuthContext,
|
|
26
|
+
> {
|
|
23
27
|
close(): void
|
|
24
|
-
|
|
25
|
-
token: string | null
|
|
26
|
-
ctx?: unknown
|
|
27
|
-
userId: string | null
|
|
28
|
-
}): void
|
|
28
|
+
updateAuth(authData: AuthData<TAuthContext> | null): void
|
|
29
29
|
setLogoutBehavior(behavior: LogoutBehavior): void
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -36,8 +36,7 @@ export interface ClientLifecycle {
|
|
|
36
36
|
*
|
|
37
37
|
* - `identity` — close the old client and create a new one (e.g. a different
|
|
38
38
|
* database `name` is a different client; no in-place path).
|
|
39
|
-
* - `auth` — `
|
|
40
|
-
* compared by JSON).
|
|
39
|
+
* - `auth` — `authData` feeds a single `updateAuth` call (compared by JSON).
|
|
41
40
|
* - `logoutBehavior` — call `setLogoutBehavior`.
|
|
42
41
|
* - `throw` — value change throws (e.g. `url`). Checked before `identity`, so a
|
|
43
42
|
* same-render name+url swap cannot silently reconnect elsewhere.
|
|
@@ -66,7 +65,7 @@ interface Owned<TOptions, TClient> {
|
|
|
66
65
|
options: TOptions | null
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
// ADR-0034 / closeio/doync#281 — routes onto create/close/
|
|
68
|
+
// ADR-0034 / closeio/doync#281 — routes onto create/close/updateAuth/
|
|
70
69
|
// setLogoutBehavior; throw-before-identity; name-swap recreates.
|
|
71
70
|
/**
|
|
72
71
|
* Hold a client for as long as `options` is non-null. Pass `null` until the
|
|
@@ -188,9 +187,9 @@ function applyChange<TOptions extends object, TClient extends ClientLifecycle>(
|
|
|
188
187
|
}
|
|
189
188
|
}
|
|
190
189
|
|
|
191
|
-
//
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
190
|
+
// authData — one updateAuth if it drifted (compared by JSON).
|
|
191
|
+
const authData = resolveAuthData(prev, next, policy)
|
|
192
|
+
if (authData !== undefined) current.updateAuth(authData)
|
|
194
193
|
|
|
195
194
|
// logoutBehavior — only call when it actually moved.
|
|
196
195
|
for (const key of policyKeys(policy)) {
|
|
@@ -205,60 +204,26 @@ function applyChange<TOptions extends object, TClient extends ClientLifecycle>(
|
|
|
205
204
|
}
|
|
206
205
|
|
|
207
206
|
/**
|
|
208
|
-
* Returns the
|
|
209
|
-
*
|
|
210
|
-
*
|
|
207
|
+
* Returns the next `authData` when any policy-`auth` key moved, else
|
|
208
|
+
* `undefined` (no call). `updateAuth` receives the whole next value (complete
|
|
209
|
+
* identity or `null`); there is no patch semantics.
|
|
211
210
|
*/
|
|
212
|
-
function
|
|
211
|
+
function resolveAuthData<TOptions extends object>(
|
|
213
212
|
prev: TOptions,
|
|
214
213
|
next: TOptions,
|
|
215
214
|
policy: ClientOptionPolicy<TOptions>,
|
|
216
|
-
): {
|
|
217
|
-
token: string | null
|
|
218
|
-
userId: string | null
|
|
219
|
-
ctx?: unknown
|
|
220
|
-
} | null {
|
|
215
|
+
): AuthData | null {
|
|
221
216
|
let changed = false
|
|
222
|
-
let
|
|
223
|
-
let
|
|
224
|
-
let sawCtx = false
|
|
225
|
-
let token: unknown
|
|
226
|
-
let userId: unknown
|
|
227
|
-
let ctx: unknown
|
|
217
|
+
let nextAuth: unknown
|
|
218
|
+
let sawAuth = false
|
|
228
219
|
for (const key of policyKeys(policy)) {
|
|
229
220
|
if (policy[key] !== 'auth') continue
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (pk === 'token') {
|
|
234
|
-
sawToken = true
|
|
235
|
-
token = next[key]
|
|
236
|
-
} else if (pk === 'userId') {
|
|
237
|
-
sawUserId = true
|
|
238
|
-
userId = next[key]
|
|
239
|
-
} else if (pk === 'ctx') {
|
|
240
|
-
sawCtx = true
|
|
241
|
-
ctx = next[key]
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
if (!changed) return null
|
|
245
|
-
const auth: {
|
|
246
|
-
token: string | null
|
|
247
|
-
userId: string | null
|
|
248
|
-
ctx?: unknown
|
|
249
|
-
} = {
|
|
250
|
-
token: sawToken
|
|
251
|
-
? ((token as string | null | undefined) ?? null)
|
|
252
|
-
: ((prev as { token?: string | null }).token ?? null),
|
|
253
|
-
userId: sawUserId
|
|
254
|
-
? ((userId as string | null | undefined) ?? null)
|
|
255
|
-
: ((prev as { userId?: string | null }).userId ?? null),
|
|
256
|
-
}
|
|
257
|
-
if (sawCtx) auth.ctx = ctx
|
|
258
|
-
else if ('ctx' in (prev as object)) {
|
|
259
|
-
auth.ctx = (prev as { ctx?: unknown }).ctx
|
|
221
|
+
sawAuth = true
|
|
222
|
+
if (!valueEqual(prev[key], next[key], true)) changed = true
|
|
223
|
+
nextAuth = next[key]
|
|
260
224
|
}
|
|
261
|
-
return
|
|
225
|
+
if (!sawAuth || !changed) return null
|
|
226
|
+
return nextAuth as AuthData | null
|
|
262
227
|
}
|
|
263
228
|
|
|
264
229
|
function optionsEqualByPolicy<TOptions extends object>(
|
|
@@ -269,14 +234,14 @@ function optionsEqualByPolicy<TOptions extends object>(
|
|
|
269
234
|
for (const key of policyKeys(policy)) {
|
|
270
235
|
const disposition = policy[key]
|
|
271
236
|
if (disposition === undefined || disposition === 'ignore') continue
|
|
272
|
-
const serialize = disposition === 'auth'
|
|
237
|
+
const serialize = disposition === 'auth'
|
|
273
238
|
if (!valueEqual(a[key], b[key], serialize)) return false
|
|
274
239
|
}
|
|
275
240
|
return true
|
|
276
241
|
}
|
|
277
242
|
|
|
278
243
|
function valueEqual(a: unknown, b: unknown, serialize = false): boolean {
|
|
279
|
-
//
|
|
244
|
+
// authData compared serialized (shared-hub posture, ADR-0034) when asked.
|
|
280
245
|
if (serialize) return jsonStable(a) === jsonStable(b)
|
|
281
246
|
return Object.is(a, b)
|
|
282
247
|
}
|
|
@@ -285,8 +250,8 @@ function jsonStable(value: unknown): string {
|
|
|
285
250
|
try {
|
|
286
251
|
return JSON.stringify(value) ?? 'undefined'
|
|
287
252
|
} catch {
|
|
288
|
-
// Non-serializable
|
|
289
|
-
// bubbles out of a compare. Reference identity is lost — non-JSON
|
|
253
|
+
// Non-serializable value: fall back to type tag so a throw here never
|
|
254
|
+
// bubbles out of a compare. Reference identity is lost — non-JSON auth is
|
|
290
255
|
// already outside the hub's contract.
|
|
291
256
|
return `\0ref:${typeof value}`
|
|
292
257
|
}
|
package/src/web.ts
CHANGED
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
* type: 'module',
|
|
17
17
|
* }),
|
|
18
18
|
* url: 'wss://example.com/sync',
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* ctx,
|
|
19
|
+
* authData,
|
|
20
|
+
* ctxValidationSchema,
|
|
22
21
|
* }
|
|
23
22
|
* : null,
|
|
24
23
|
* )
|
|
@@ -26,11 +25,12 @@
|
|
|
26
25
|
* ```
|
|
27
26
|
*/
|
|
28
27
|
|
|
28
|
+
import type { AuthContext } from '@doync/core'
|
|
29
29
|
import type { CreateWebClientOptions, WebClient } from '@doync/web'
|
|
30
30
|
|
|
31
31
|
import { createWebClient } from '@doync/web'
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import { type ClientOptionPolicy, useClient } from './use-client'
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Web option dispositions (ADR-0034). Schema / queries / mutations (and any
|
|
@@ -41,13 +41,12 @@ import { useClient, type ClientOptionPolicy } from './use-client'
|
|
|
41
41
|
* `url` is throw-on-change: a config bug cannot silently reconnect elsewhere.
|
|
42
42
|
* `worker` is IGNORE: captured at creation; later changes are ignored — its
|
|
43
43
|
* required inline-literal form makes the reference unstable across renders.
|
|
44
|
+
* `ctxValidationSchema` is fixed at creation (construction-time constant).
|
|
44
45
|
*/
|
|
45
46
|
const WEB_CLIENT_POLICY: ClientOptionPolicy<CreateWebClientOptions> = {
|
|
46
47
|
name: 'identity',
|
|
47
48
|
url: 'throw',
|
|
48
|
-
|
|
49
|
-
userId: 'auth',
|
|
50
|
-
ctx: 'auth',
|
|
49
|
+
authData: 'auth',
|
|
51
50
|
logoutBehavior: 'logoutBehavior',
|
|
52
51
|
worker: 'ignore',
|
|
53
52
|
}
|
|
@@ -58,14 +57,10 @@ const WEB_CLIENT_POLICY: ClientOptionPolicy<CreateWebClientOptions> = {
|
|
|
58
57
|
* a `url` change throws; `worker` is fixed at creation. Pass `null` until the
|
|
59
58
|
* database id is known, then hand the result to `<DoyncProvider>`.
|
|
60
59
|
*/
|
|
61
|
-
export function useWebClient<
|
|
62
|
-
options: CreateWebClientOptions<
|
|
63
|
-
): WebClient | null {
|
|
64
|
-
return useClient(
|
|
65
|
-
options as CreateWebClientOptions | null,
|
|
66
|
-
createWebClient,
|
|
67
|
-
WEB_CLIENT_POLICY as ClientOptionPolicy<CreateWebClientOptions>,
|
|
68
|
-
)
|
|
60
|
+
export function useWebClient<TAuthContext extends AuthContext = AuthContext>(
|
|
61
|
+
options: CreateWebClientOptions<TAuthContext> | null,
|
|
62
|
+
): WebClient<TAuthContext> | null {
|
|
63
|
+
return useClient(options, createWebClient, WEB_CLIENT_POLICY)
|
|
69
64
|
}
|
|
70
65
|
|
|
71
66
|
export type { CreateWebClientOptions, WebClient }
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{useLayoutEffect as e,useRef as t,useState as n}from"react";function r(r,a,s){let[c,l]=n(null),u=t({client:null,options:null}),d=t(a);d.current=a;let f=t(s);return f.current=s,e(()=>{let e=u.current.options,t=r,n=u.current.client,a=d.current,s=f.current;if(t!==null&&e!==null&&n!==null){if(o(e,t,s)){u.current.options=t;return}let r=i(e,t,n,a,s);r===null?u.current.options=t:(u.current={client:r,options:t},l(r));return}if(t!==null&&(e===null||n===null)){let e=a(t);u.current={client:e,options:t},l(e);return}t===null&&n!==null&&(n.close(),u.current={client:null,options:null},l(null))}),e(()=>()=>{u.current.client?.close(),u.current={client:null,options:null}},[]),c}function i(e,t,n,r,i){for(let n of l(i))if(i[n]===`throw`&&!s(e[n],t[n]))throw Error(`doync: useClient cannot change ${String(n)} after creation — close and recreate with the new value`);for(let a of l(i))if(i[a]===`identity`&&!s(e[a],t[a]))return n.close(),r(t);let o=a(e,t,i);o!==null&&n.setAuth(o);for(let r of l(i))if(i[r]===`logoutBehavior`&&!s(e[r],t[r])){let e=t[r];e!==void 0&&n.setLogoutBehavior(e)}return null}function a(e,t,n){let r=!1,i=!1,a=!1,o=!1,c,u,d;for(let f of l(n)){if(n[f]!==`auth`)continue;let l=f,p=l===`ctx`;s(e[f],t[f],p)||(r=!0),l===`token`?(i=!0,c=t[f]):l===`userId`?(a=!0,u=t[f]):l===`ctx`&&(o=!0,d=t[f])}if(!r)return null;let f={token:i?c??null:e.token??null,userId:a?u??null:e.userId??null};return o?f.ctx=d:`ctx`in e&&(f.ctx=e.ctx),f}function o(e,t,n){for(let r of l(n)){let i=n[r];if(i===void 0||i===`ignore`)continue;let a=i===`auth`&&r===`ctx`;if(!s(e[r],t[r],a))return!1}return!0}function s(e,t,n=!1){return n?c(e)===c(t):Object.is(e,t)}function c(e){try{return JSON.stringify(e)??`undefined`}catch{return`\0ref:${typeof e}`}}function l(e){return Object.keys(e)}export{r as t};
|
|
2
|
-
//# sourceMappingURL=use-client-DanwzoBF.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-client-DanwzoBF.js","names":[],"sources":["../src/use-client.ts"],"sourcesContent":["/**\n * Generic client lifecycle hook. Holds a client for as long as the options bag\n * is non-null and routes option changes onto `createClient` / `close` /\n * `setAuth` / `setLogoutBehavior`, or throws — never invents its own lifecycle.\n * Platform wrappers (`useMobileClient`, `useWebClient`) supply the factory and\n * a {@link ClientOptionPolicy}; this module imports no platform package.\n *\n * Options are compared by value (fresh object literals every render are fine;\n * no `useMemo` required). `ctx` is compared by JSON. StrictMode double-mount is\n * create → close → create and is safe: ownership lives in a layout-effect\n * cleanup, not in render.\n */\n\nimport type { LogoutBehavior } from '@doync/client'\n\nimport { useLayoutEffect, useRef, useState } from 'react'\n\n/**\n * Minimum call surface the hook routes onto. Platform clients (`MobileClient`,\n * `WebClient`) both satisfy this; tests inject a fake.\n */\nexport interface ClientLifecycle {\n close(): void\n setAuth(auth: {\n token: string | null\n ctx?: unknown\n userId: string | null\n }): void\n setLogoutBehavior(behavior: LogoutBehavior): void\n}\n\n/**\n * How a single options-bag key is handled when its value changes between\n * renders. Keys absent from the policy are not compared (construction-only\n * values like `schema` / `queries` — module-level constants in real apps).\n *\n * - `identity` — close the old client and create a new one (e.g. a different\n * database `name` is a different client; no in-place path).\n * - `auth` — `token` / `userId` / `ctx` feed a single `setAuth` call (`ctx` is\n * compared by JSON).\n * - `logoutBehavior` — call `setLogoutBehavior`.\n * - `throw` — value change throws (e.g. `url`). Checked before `identity`, so a\n * same-render name+url swap cannot silently reconnect elsewhere.\n * - `ignore` — captured at creation; later changes are ignored (e.g. web's\n * `worker` factory, whose required inline-literal form makes the reference\n * unstable across renders).\n */\nexport type OptionDisposition =\n | 'identity'\n | 'auth'\n | 'logoutBehavior'\n | 'throw'\n | 'ignore'\n\n/**\n * Per-platform table mapping option keys to {@link OptionDisposition}. Keys not\n * listed are not compared; unknown keys on the options bag are ignored.\n */\nexport type ClientOptionPolicy<TOptions extends object> = {\n readonly [K in keyof TOptions]?: OptionDisposition\n}\n\n/** Live ownership slot shared by the diff effect and the unmount cleanup. */\ninterface Owned<TOptions, TClient> {\n client: TClient | null\n options: TOptions | null\n}\n\n// ADR-0034 / closeio/doync#281 — routes onto create/close/setAuth/\n// setLogoutBehavior; throw-before-identity; name-swap recreates.\n/**\n * Hold a client for as long as `options` is non-null. Pass `null` until the\n * database id is known (e.g. after a create-session mutation acks). Option\n * changes route onto client verbs per `policy` — see\n * {@link OptionDisposition}.\n */\nexport function useClient<\n TOptions extends object,\n TClient extends ClientLifecycle,\n>(\n options: TOptions | null,\n createClient: (options: TOptions) => TClient,\n policy: ClientOptionPolicy<TOptions>,\n): TClient | null {\n const [client, setClient] = useState<TClient | null>(null)\n // Ownership lives in a ref so the unmount cleanup and the diff path share one\n // source of truth without putting unstable identities into effect deps.\n const owned = useRef<Owned<TOptions, TClient>>({\n client: null,\n options: null,\n })\n const createRef = useRef(createClient)\n createRef.current = createClient\n const policyRef = useRef(policy)\n policyRef.current = policy\n\n // Diff-and-route runs after every commit. Equal-by-value options are a no-op\n // so a fresh options literal every render neither recreates nor re-verbs.\n // Creation/teardown ownership lives here — NOT in render — so StrictMode's\n // double-mount is create → cleanup-close → create, and nothing leaks.\n useLayoutEffect(() => {\n const prev = owned.current.options\n const next = options\n const current = owned.current.client\n const create = createRef.current\n const pol = policyRef.current\n\n if (next !== null && prev !== null && current !== null) {\n if (optionsEqualByPolicy(prev, next, pol)) {\n // Keep the latest reference so subsequent diffs start from what the\n // caller last passed (in case a nested object was mutated — rare; the\n // equality check already saw them as equal).\n owned.current.options = next\n return\n }\n const created = applyChange(prev, next, current, create, pol)\n if (created !== null) {\n owned.current = { client: created, options: next }\n setClient(created)\n } else {\n // In-place verb path: live client kept, snapshot advances.\n owned.current.options = next\n }\n return\n }\n\n if (next !== null && (prev === null || current === null)) {\n // null → options (or remount after StrictMode cleanup cleared the refs).\n const created = create(next)\n owned.current = { client: created, options: next }\n setClient(created)\n return\n }\n\n if (next === null && current !== null) {\n // options → null: close and drop.\n current.close()\n owned.current = { client: null, options: null }\n setClient(null)\n }\n })\n\n // Unmount-only cleanup. Separate effect so the diff path above can re-run on\n // every options-change commit WITHOUT closing between equal renders.\n useLayoutEffect(() => {\n return () => {\n owned.current.client?.close()\n owned.current = { client: null, options: null }\n }\n }, [])\n\n return client\n}\n\n// ── internals ──────────────────────────────────────────────────────────────\n\n/**\n * Route a non-equal options change onto existing verbs. Returns a freshly\n * created client when identity keys moved (after closing the old one); returns\n * null when the live client was kept (auth / logoutBehavior routing, or throw\n * aborted before any mutation — throw propagates).\n */\nfunction applyChange<TOptions extends object, TClient extends ClientLifecycle>(\n prev: TOptions,\n next: TOptions,\n current: TClient,\n create: (options: TOptions) => TClient,\n policy: ClientOptionPolicy<TOptions>,\n): TClient | null {\n // Throw-on-change FIRST (url): a config bug must never silently reconnect\n // elsewhere, even when bundled with a name swap that would otherwise rebuild.\n for (const key of policyKeys(policy)) {\n if (policy[key] !== 'throw') continue\n if (!valueEqual(prev[key], next[key])) {\n throw new Error(\n `doync: useClient cannot change ${String(key)} after creation — close and recreate with the new value`,\n )\n }\n }\n\n // Identity keys — a name swap is a different Client; no in-place path. Fresh\n // create carries the full next bag (auth/logout already in the new options).\n for (const key of policyKeys(policy)) {\n if (policy[key] !== 'identity') continue\n if (!valueEqual(prev[key], next[key])) {\n current.close()\n return create(next)\n }\n }\n\n // Auth triple — one setAuth if any of token/userId/ctx drifted.\n const auth = resolveAuthTriple(prev, next, policy)\n if (auth !== null) current.setAuth(auth)\n\n // logoutBehavior — only call when it actually moved.\n for (const key of policyKeys(policy)) {\n if (policy[key] !== 'logoutBehavior') continue\n if (!valueEqual(prev[key], next[key])) {\n const behavior = next[key] as LogoutBehavior | undefined\n if (behavior !== undefined) current.setLogoutBehavior(behavior)\n }\n }\n\n return null\n}\n\n/**\n * Returns the asserted auth triple when any policy-`auth` key moved, else null.\n * setAuth's contract is a full identity assert, never a patch — fall back to\n * prev for any auth key the next bag omitted.\n */\nfunction resolveAuthTriple<TOptions extends object>(\n prev: TOptions,\n next: TOptions,\n policy: ClientOptionPolicy<TOptions>,\n): {\n token: string | null\n userId: string | null\n ctx?: unknown\n} | null {\n let changed = false\n let sawToken = false\n let sawUserId = false\n let sawCtx = false\n let token: unknown\n let userId: unknown\n let ctx: unknown\n for (const key of policyKeys(policy)) {\n if (policy[key] !== 'auth') continue\n const pk = key as keyof TOptions & string\n const serialize = pk === 'ctx'\n if (!valueEqual(prev[key], next[key], serialize)) changed = true\n if (pk === 'token') {\n sawToken = true\n token = next[key]\n } else if (pk === 'userId') {\n sawUserId = true\n userId = next[key]\n } else if (pk === 'ctx') {\n sawCtx = true\n ctx = next[key]\n }\n }\n if (!changed) return null\n const auth: {\n token: string | null\n userId: string | null\n ctx?: unknown\n } = {\n token: sawToken\n ? ((token as string | null | undefined) ?? null)\n : ((prev as { token?: string | null }).token ?? null),\n userId: sawUserId\n ? ((userId as string | null | undefined) ?? null)\n : ((prev as { userId?: string | null }).userId ?? null),\n }\n if (sawCtx) auth.ctx = ctx\n else if ('ctx' in (prev as object)) {\n auth.ctx = (prev as { ctx?: unknown }).ctx\n }\n return auth\n}\n\nfunction optionsEqualByPolicy<TOptions extends object>(\n a: TOptions,\n b: TOptions,\n policy: ClientOptionPolicy<TOptions>,\n): boolean {\n for (const key of policyKeys(policy)) {\n const disposition = policy[key]\n if (disposition === undefined || disposition === 'ignore') continue\n const serialize = disposition === 'auth' && key === 'ctx'\n if (!valueEqual(a[key], b[key], serialize)) return false\n }\n return true\n}\n\nfunction valueEqual(a: unknown, b: unknown, serialize = false): boolean {\n // ctx compared serialized (shared-hub posture, ADR-0034) when asked.\n if (serialize) return jsonStable(a) === jsonStable(b)\n return Object.is(a, b)\n}\n\nfunction jsonStable(value: unknown): string {\n try {\n return JSON.stringify(value) ?? 'undefined'\n } catch {\n // Non-serializable ctx: fall back to type tag so a throw here never\n // bubbles out of a compare. Reference identity is lost — non-JSON ctx is\n // already outside the hub's contract.\n return `\\0ref:${typeof value}`\n }\n}\n\nfunction policyKeys<TOptions extends object>(\n policy: ClientOptionPolicy<TOptions>,\n): (keyof TOptions)[] {\n return Object.keys(policy) as (keyof TOptions)[]\n}\n"],"mappings":"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"}
|