snapback2 0.0.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.
Files changed (132) hide show
  1. package/README.md +127 -0
  2. package/bin/snapback2.mjs +36 -0
  3. package/dist/api.d.ts +20 -0
  4. package/dist/assets.d.ts +34 -0
  5. package/dist/client-assets.d.ts +26 -0
  6. package/dist/client-offline.d.ts +94 -0
  7. package/dist/client-outbox.d.ts +58 -0
  8. package/dist/client-upload.d.ts +41 -0
  9. package/dist/client-wire.d.ts +41 -0
  10. package/dist/client.d.ts +12 -0
  11. package/dist/client.mjs +5 -0
  12. package/dist/client.mjs.map +7 -0
  13. package/dist/compiler-core.mjs +60 -0
  14. package/dist/compiler-core.mjs.map +7 -0
  15. package/dist/compiler-lib.d.ts +71 -0
  16. package/dist/compiler.mjs +5 -0
  17. package/dist/compiler.mjs.map +7 -0
  18. package/dist/drain.d.ts +33 -0
  19. package/dist/expo/files.d.ts +3 -0
  20. package/dist/expo/index.d.ts +16 -0
  21. package/dist/expo/token-store.d.ts +7 -0
  22. package/dist/expo/witness.d.ts +28 -0
  23. package/dist/expo/witness.mjs +2 -0
  24. package/dist/expo/witness.mjs.map +7 -0
  25. package/dist/expo.d.ts +4 -0
  26. package/dist/expo.mjs +9 -0
  27. package/dist/expo.mjs.map +7 -0
  28. package/dist/guide/auth.md +89 -0
  29. package/dist/guide/effects.md +52 -0
  30. package/dist/guide/families.json +446 -0
  31. package/dist/guide/grammar.md +52 -0
  32. package/dist/guide/live-query.md +27 -0
  33. package/dist/guide/offline.md +75 -0
  34. package/dist/guide/personas.md +15 -0
  35. package/dist/guide/quarry.md +297 -0
  36. package/dist/guide/testing.md +57 -0
  37. package/dist/index.d.ts +362 -0
  38. package/dist/index.mjs +2 -0
  39. package/dist/index.mjs.map +7 -0
  40. package/dist/offline-protocol.d.ts +180 -0
  41. package/dist/offline-schema.d.ts +1 -0
  42. package/dist/offline.d.ts +177 -0
  43. package/dist/react-core.d.ts +91 -0
  44. package/dist/react-core.mjs +2 -0
  45. package/dist/react-core.mjs.map +7 -0
  46. package/dist/react-native/components.d.ts +23 -0
  47. package/dist/react-native/files.d.ts +43 -0
  48. package/dist/react-native/source.d.ts +16 -0
  49. package/dist/react-native.d.ts +4 -0
  50. package/dist/react-native.mjs +2 -0
  51. package/dist/react-native.mjs.map +7 -0
  52. package/dist/react.d.ts +19 -0
  53. package/dist/react.mjs +2 -0
  54. package/dist/react.mjs.map +7 -0
  55. package/dist/sqlite-test.mjs +1720 -0
  56. package/dist/sqlite-test.mjs.map +7 -0
  57. package/dist/sse.d.ts +44 -0
  58. package/dist/store/byte-cache.d.ts +162 -0
  59. package/dist/store/canonical.d.ts +2 -0
  60. package/dist/store/indexeddb.d.ts +23 -0
  61. package/dist/store/locks.d.ts +11 -0
  62. package/dist/store/outbox.d.ts +113 -0
  63. package/dist/store/overlay-retirement.d.ts +17 -0
  64. package/dist/store/projection.d.ts +11 -0
  65. package/dist/store/range-store.d.ts +388 -0
  66. package/dist/store/sqlite-driver.conformance.d.ts +7 -0
  67. package/dist/store/sqlite-driver.d.ts +16 -0
  68. package/dist/store/sqlite-expo.d.ts +3 -0
  69. package/dist/store/sqlite.d.ts +31 -0
  70. package/dist/templates/chat/expo/App.tsx +29 -0
  71. package/dist/templates/chat/expo/app.json +11 -0
  72. package/dist/templates/chat/expo/index.js +3 -0
  73. package/dist/templates/chat/expo/shared/log.js +92 -0
  74. package/dist/templates/chat/expo/shared/log.ts +130 -0
  75. package/dist/templates/chat/expo/shared/offline.js +8 -0
  76. package/dist/templates/chat/expo/shared/offline.ts +9 -0
  77. package/dist/templates/chat/expo/src/Chat.tsx +23 -0
  78. package/dist/templates/chat/expo/src/Composer.tsx +23 -0
  79. package/dist/templates/chat/expo/src/MediaView.tsx +29 -0
  80. package/dist/templates/chat/expo/src/Witness.tsx +131 -0
  81. package/dist/templates/chat/expo/src/screens/Inbox.tsx +9 -0
  82. package/dist/templates/chat/expo/src/screens/Search.tsx +10 -0
  83. package/dist/templates/chat/expo/src/screens/Thread.tsx +22 -0
  84. package/dist/templates/chat/expo/src/witness-state.ts +68 -0
  85. package/dist/templates/chat/expo/tsconfig.json +5 -0
  86. package/dist/templates/chat/react/index.html +5 -0
  87. package/dist/templates/chat/react/shared/log.js +92 -0
  88. package/dist/templates/chat/react/shared/log.ts +130 -0
  89. package/dist/templates/chat/react/shared/offline.js +8 -0
  90. package/dist/templates/chat/react/shared/offline.ts +9 -0
  91. package/dist/templates/chat/react/src/App.tsx +36 -0
  92. package/dist/templates/chat/react/src/Composer.tsx +45 -0
  93. package/dist/templates/chat/react/src/Inbox.tsx +24 -0
  94. package/dist/templates/chat/react/src/Search.tsx +26 -0
  95. package/dist/templates/chat/react/src/Thread.tsx +126 -0
  96. package/dist/templates/chat/react/src/env.d.ts +1 -0
  97. package/dist/templates/chat/react/src/main.tsx +20 -0
  98. package/dist/templates/chat/shared/log.js +92 -0
  99. package/dist/templates/chat/shared/log.ts +130 -0
  100. package/dist/templates/chat/shared/offline.js +8 -0
  101. package/dist/templates/chat/shared/offline.ts +9 -0
  102. package/dist/templates/chat/snapback/deliveries.q +33 -0
  103. package/dist/templates/chat/snapback/delivery.ts +30 -0
  104. package/dist/templates/chat/snapback/feeds.q +14 -0
  105. package/dist/templates/chat/snapback/follows.q +11 -0
  106. package/dist/templates/chat/snapback/groups.q +40 -0
  107. package/dist/templates/chat/snapback/messages.q +32 -0
  108. package/dist/templates/chat/snapback/notifications.q +7 -0
  109. package/dist/templates/chat/snapback/posts.q +4 -0
  110. package/dist/templates/chat/snapback/profiles.q +17 -0
  111. package/dist/templates/chat/snapback/schema.q +136 -0
  112. package/dist/templates/chat/snapback/seed.ts +45 -0
  113. package/dist/templates/chat/snapback/tests/chat.test.ts +759 -0
  114. package/dist/templates/react/index.html +5 -0
  115. package/dist/templates/react/src/App.tsx +5 -0
  116. package/dist/templates/react/src/main.tsx +18 -0
  117. package/dist/templates/todos/snapback/schema.q +11 -0
  118. package/dist/templates/todos/snapback/seed.ts +10 -0
  119. package/dist/templates/todos/snapback/tests/todos.test.ts +13 -0
  120. package/dist/templates/todos/snapback/todos.q +6 -0
  121. package/dist/test-runner.mjs +8541 -0
  122. package/dist/test-runner.mjs.map +7 -0
  123. package/dist/test.d.ts +28 -0
  124. package/dist/test.mjs +8541 -0
  125. package/dist/test.mjs.map +7 -0
  126. package/dist/token-store.d.ts +3 -0
  127. package/dist/twin-hydrate.d.ts +43 -0
  128. package/dist/twin.d.ts +70 -0
  129. package/dist/types.d.ts +410 -0
  130. package/dist/witness-test.mjs +2 -0
  131. package/dist/witness-test.mjs.map +7 -0
  132. package/package.json +117 -0
@@ -0,0 +1,91 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { type SnapbackRefusal } from "./client.js";
3
+ import { type Channel as ChannelRef, type Op } from "./api.js";
4
+ import { type AuthMe, type AuthSession, type AssetRecord, type AssetInput, type AssetUrlOptions, type ChannelHandle, type ClientSnapshot, type ConnectionState, type CreateClientOptions, type MutationResult, type OutboxEntry, type Principal, type QueryHookResult, type RefusalEnvelope, type SnapbackClient } from "./types.js";
5
+ export { initialQueryState, reduceQueryState } from "./types.js";
6
+ export type { QueryView, QueryViewState } from "./types.js";
7
+ export type SnapbackProviderProps = ((CreateClientOptions & {
8
+ client?: never;
9
+ }) | {
10
+ client: SnapbackClient;
11
+ url?: never;
12
+ as?: never;
13
+ token?: never;
14
+ tokenStore?: never;
15
+ fetch?: never;
16
+ offline?: never;
17
+ }) & {
18
+ children: ReactNode;
19
+ };
20
+ export declare function SnapbackProvider({ client: supplied, url, as, token, tokenStore, fetch, offline, children }: SnapbackProviderProps): React.ReactElement;
21
+ /** The provider-owned client, for first-party platform components and custom transports. */
22
+ export declare function useSnapbackClient(): SnapbackClient;
23
+ export declare function useSnapbackSnapshot(): ClientSnapshot;
24
+ export declare function useConnection(): ConnectionState;
25
+ /**
26
+ * Reads a receipt-backed query. It subscribes by default; pass `{ live: false }`
27
+ * for a one-shot read. Receipt state stays `complete`, `capped`, `partial`, or
28
+ * `denied`; `live` and `connection` report the subscription transport separately.
29
+ */
30
+ export declare function useQuery<Args, Result>(ref: Op<Args, Result>, args: NoInfer<Args>, options?: {
31
+ live?: false;
32
+ drain?: boolean;
33
+ }): QueryHookResult<Result>;
34
+ export declare function useSubscription<Args, Result>(ref: Op<Args, Result>, args: NoInfer<Args>): QueryHookResult<Result>;
35
+ export interface UseMutationResult<Args, Result> {
36
+ run(args: AssetInput<Args>): Promise<MutationResult<Result>>;
37
+ /** A `run()` is awaiting the owner right now. */
38
+ inFlight: boolean;
39
+ /** This op's intents still queued in the durable outbox, FIFO (LLP 1012 §7). */
40
+ pending: OutboxEntry[];
41
+ /**
42
+ * `run()` will not refuse `E_OFFLINE_ID_BLOCK` for want of a replay grant, a
43
+ * known bound, or capacity for `1 + maxNewIds`: no partition, or one holding a
44
+ * current grant this write fits. False until the device has all three (the
45
+ * first drain reaching its watermark, or a login, earns the grant); gate the
46
+ * send on it rather than retrying the refusal (LLP 1012 §7).
47
+ */
48
+ ready: boolean;
49
+ last: MutationResult<Result> | undefined;
50
+ transport: SnapbackRefusal | undefined;
51
+ }
52
+ /** The durable outbox of the open partition, FIFO, with `dismiss` for settled intents. */
53
+ export interface UseOutboxResult {
54
+ entries: OutboxEntry[];
55
+ /** Grant-present readiness (`client.outbox.ready()`): no particular write bound is implied, and the fence does not gate it. */
56
+ ready: boolean;
57
+ /** Past means queued until this partition resumes; accepting writes remains enabled. */
58
+ fence: "inside" | "past";
59
+ dismiss(intent: string): Promise<void>;
60
+ }
61
+ export declare function useOutbox(): UseOutboxResult;
62
+ export declare function useMutation<Args, Result>(ref: Op<Args, Result>): UseMutationResult<Args, Result>;
63
+ export type AuthHookState = "hydrating" | "authenticated" | "anonymous" | "refused";
64
+ export interface UseAuthResult {
65
+ state: AuthHookState;
66
+ viewer?: Principal;
67
+ kind?: string;
68
+ token?: string;
69
+ refusal?: RefusalEnvelope;
70
+ signup(input: {
71
+ email: string;
72
+ password: string;
73
+ }): Promise<AuthSession>;
74
+ login(input: {
75
+ email: string;
76
+ password: string;
77
+ }): Promise<AuthSession>;
78
+ guest(): Promise<AuthSession>;
79
+ logout(): Promise<{
80
+ ok: boolean;
81
+ }>;
82
+ me(): Promise<AuthMe>;
83
+ }
84
+ export declare function useAuth(): UseAuthResult;
85
+ export declare function useChannel<Args, Payload = unknown>(ref: ChannelRef<Args, Payload>, args: NoInfer<Args>): ChannelHandle<Payload>;
86
+ /**
87
+ * Resolves records without putting authority in image URLs. A carrier is minted
88
+ * only when explicitly requested for a player/download that cannot send headers.
89
+ */
90
+ export declare function useAssetUrl(asset: AssetRecord | null | undefined, options?: AssetUrlOptions): string | undefined;
91
+ export declare function useAssetUrl(assets: readonly AssetRecord[], options?: AssetUrlOptions): (string | undefined)[] | undefined;
@@ -0,0 +1,2 @@
1
+ import Y,{createContext as G,useCallback as _,useContext as K,useEffect as O,useMemo as w,useReducer as W,useRef as v,useState as C}from"react";import{createClient as J,isSnapbackRefusal as S}from"snapback2/client";var H=Symbol.for("snapback2.SnapbackRefusal"),B=class extends Error{constructor(s,i=!1,n){super(s.message);this.retryable=i;this.status=n;this.name="SnapbackRefusal",this.envelope=s,this.code=s.code}[H]=!0;envelope;code};var D=/^s2id-[0-9a-f]{32}-[0-9a-f]{16}$/;async function V(t,e){let s=new Set;return t&&await Promise.all(e.map(async i=>{D.test(i)&&await t.viewed(i)&&s.add(i)})),s}var I=Symbol.for("snapback.referencePath"),M=new Map;function N(t){let e=t.join("."),s=M.get(e);if(s)return s;let i=Object.assign({},{op:e,[I]:e}),n=new Proxy(i,{get(o,a){if(a===I)return e;if(a===Symbol.toStringTag)return"SnapbackOp";if(a!=="then")return a==="op"&&t.length>=2?e:typeof a=="string"?N([...t,a]):void 0}});return M.set(e,n),n}function P(t){return t[I]}var ce=N([]);function E(){return{state:"hydrating",live:!1}}function A(t){return t===void 0?"undefined":t===null||typeof t!="object"?JSON.stringify(t):Array.isArray(t)?`[${t.map(A).join(",")}]`:`{${Object.keys(t).sort().map(e=>`${JSON.stringify(e)}:${A(t[e])}`).join(",")}}`}function T(t){return A(t)}function U(t,e){let s="data"in e?e.data:void 0,i="data"in t?t.data:void 0,n=A(s)===A(i)?i:s,o="live"in e?e.live:!1,a={..."stale"in e&&e.stale!==void 0?{stale:e.stale}:{},..."retained"in e&&e.retained!==void 0?{retained:e.retained}:{},..."caughtUp"in e&&e.caughtUp!==void 0?{caughtUp:e.caughtUp}:{},..."watermark"in e&&e.watermark!==void 0?{watermark:e.watermark}:{},..."predicted"in e&&e.predicted!==void 0?{predicted:e.predicted}:{}},l;switch(e.state){case"complete":l={state:"complete",live:o,data:n,seq:e.seq,next:e.next,...a};break;case"capped":l={state:"capped",live:o,data:n,seq:e.seq,next:e.next,...a};break;case"partial":l=n===void 0?{state:"partial",live:o,seq:e.seq,reason:e.reason,...a}:{state:"partial",live:o,data:n,seq:e.seq,reason:e.reason,...a};break;case"denied":l={state:"denied",live:o,rule:e.rule,seq:e.seq,...a};break;case"hydrating":l={state:"hydrating",live:!1};break;case"refused":{let y={code:e.code,family:e.family,message:e.message,guide:e.guide};e.site!==void 0&&(y.site=e.site),e.rule!==void 0&&(y.rule=e.rule),e.rewrite!==void 0&&(y.rewrite=e.rewrite),l={state:"refused",live:!1,refusal:y};break}case"stale":l=n===void 0?{state:"stale",live:!1,...e.seq===void 0?{}:{seq:e.seq},since:e.since}:{state:"stale",live:!1,data:n,...e.seq===void 0?{}:{seq:e.seq},since:e.since};break}return A(t)===A(l)?t:l}var q=G(null);function X(t,e){return t===e?!0:!t||!e?!1:t.store===e.store&&t.fenceDays===e.fenceDays&&t.maxAssetBytes===e.maxAssetBytes}function Z(t,e){return{store:e.store,...e.fenceDays===void 0?{}:{fenceDays:e.fenceDays},...e.maxAssetBytes===void 0?{}:{maxAssetBytes:e.maxAssetBytes},onTombstone:s=>t.current?.onTombstone?.(s)??[],viewOnce:(s,i)=>t.current?.viewOnce?.(s,i)??!1}}function ge({client:t,url:e,as:s,token:i,tokenStore:n,fetch:o,offline:a,children:l}){if(s!==void 0&&(i!==void 0||n!==void 0))throw new TypeError("SnapbackProvider cannot combine dev persona `as` with bearer token custody");let y=[t,e,s,i,n,o],c=v(a);c.current=a;let p=v(void 0);if(!p.current||p.current.identity.some((d,R)=>d!==y[R])||!X(p.current.offline,a)){let d=a===void 0?void 0:Z(c,a);p.current={identity:y,offline:a,client:t??J(s!==void 0?{url:e,as:s,fetch:o,offline:d}:{url:e,token:i,tokenStore:n,fetch:o,offline:d})}}let r=p.current.client,[h,f]=C({client:r,snapshot:r.snapshot}),m=h.client===r?h.snapshot:r.snapshot,u=v(void 0);return O(()=>{u.current?.client===r&&(clearTimeout(u.current.timer),u.current=void 0);let d=!0,R=r.subscribeStatus(x=>{d&&f({client:r,snapshot:x})});return f({client:r,snapshot:r.snapshot}),r.manifest().catch(()=>{}),()=>{d=!1,R(),t||(u.current={client:r,timer:setTimeout(()=>r.close(),0)})}},[r,t]),Y.createElement(q.Provider,{value:{client:r,snapshot:m}},l)}function b(){let t=K(q);if(!t)throw new Error("Snapback hooks require <SnapbackProvider>");return t}function we(){return b().client}function be(){return b().snapshot}function Re(){return b().snapshot.connection}function ee(t,e,s={}){let{client:i,snapshot:n}=b(),o=T(e),a=w(()=>e,[o]),l=s.live!==!1;if(!l&&s.drain)throw new TypeError("useQuery drain requires a live subscription");let y=P(t),c=w(()=>({client:i,key:`${n.authGeneration}:${y}:${o}:${l}:${s.drain===!0}`}),[i,n.authGeneration,y,o,l,s.drain]),[p,r]=W((f,m)=>({identity:m.identity,view:U(f.identity===m.identity?f.view:E(),m.event)}),{identity:c,view:E()}),h=p.identity===c?p.view:E();return O(()=>{let f=!0;if(l){let u=i.subscribe(t,a,d=>{f&&r({identity:c,event:d})},{drain:s.drain});return()=>{f=!1,u.close()}}let m;return(async()=>{let u=250;for(;f;)try{let d=await i.query(t,a);f&&r({identity:c,event:d});return}catch(d){if(!f||S(d)&&d.code==="E_AUTH_SUPERSEDED")return;if(S(d)&&d.retryable){await new Promise(x=>{m=setTimeout(x,u)}),u=Math.min(u*2,5e3);continue}let R=S(d)?d.envelope:{code:"E_CLIENT",family:"owner",message:String(d),guide:"owner/connect"};r({identity:c,event:{state:"refused",live:!1,...R}});return}})(),()=>{f=!1,m!==void 0&&clearTimeout(m)}},[c,a]),w(()=>({...h,connection:n.connection}),[h,n.connection])}function Ae(t,e){return ee(t,e)}function F(t){let{client:e,snapshot:s}=b(),i=t===void 0?void 0:P(t),n=w(()=>({}),[e,s.authGeneration,i]),[o,a]=C({entries:[],ready:!1,fence:"inside"});O(()=>{let y=!0,c=0,p=e.outbox.subscribe(r=>{let h=++c,f=(m,u)=>{y&&h===c&&a({identity:n,entries:r,ready:m,fence:u})};Promise.all([e.outbox.ready(t),e.outbox.fence()]).then(([m,u])=>f(m,u),()=>f(!1,"past"))});return()=>{y=!1,p()}},[n]);let l=_(y=>e.outbox.dismiss(y),[e]);return w(()=>({entries:o.identity===n?o.entries:[],ready:o.identity===n&&o.ready,fence:o.identity===n?o.fence:"inside",dismiss:l}),[o,n,l])}function Se(){return F()}function xe(t){let{client:e,snapshot:s}=b(),i=P(t),n=w(()=>({client:e,generation:s.authGeneration,operation:i}),[e,s.authGeneration,i]),o=v(n);o.current=n;let{entries:a,ready:l}=F(t),y=w(()=>a.filter(f=>f.op===i&&f.state==="queued"),[a,i]),[c,p]=C({identity:n,pending:0}),r=c.identity===n?c:{identity:n,pending:0},h=_(async f=>{let m=()=>o.current===n&&e.snapshot.authGeneration===n.generation;if(!m())return e.mutation(t,f);p(u=>u.identity===n?{...u,pending:u.pending+1}:{identity:n,pending:1});try{let u=await e.mutation(t,f);return m()&&p(d=>({identity:n,pending:d.identity===n?d.pending:0,last:u})),u}catch(u){throw m()&&S(u)&&u.retryable&&p(d=>d.identity===n?{...d,transport:u}:{identity:n,pending:0,transport:u}),u}finally{m()&&p(u=>u.identity===n?{...u,pending:Math.max(0,u.pending-1)}:u)}},[n]);return w(()=>({run:h,inFlight:r.pending>0,pending:y,ready:l,last:r.last,transport:r.transport}),[h,r.pending,y,l,r.last,r.transport])}function ve(){let{client:t,snapshot:e}=b(),s=v(0),i=v(t);i.current!==t&&(i.current=t,s.current+=1);let[n,o]=C({client:t,generation:e.authGeneration,value:{state:"hydrating",token:e.token}}),a=n.client===t&&n.generation===e.authGeneration?n.value:{state:"hydrating",token:e.token},l=_(async()=>{let c=e.authGeneration,p=++s.current;try{let r=await t.auth.me();return p===s.current&&i.current===t&&t.snapshot.authGeneration===c&&o({client:t,generation:c,value:{state:"authenticated",...r,token:t.snapshot.token}}),r}catch(r){if(p===s.current&&i.current===t&&t.snapshot.authGeneration===c){if(S(r)&&(r.code==="E_AUTH"||r.code==="E_AUTH_REQUIRED"||r.code==="E_AUTH_SESSION"))o({client:t,generation:c,value:{state:"anonymous",refusal:r.envelope}});else if(!S(r)||r.code!=="E_AUTH_SUPERSEDED"){let h=S(r)?r.envelope:void 0;o({client:t,generation:c,value:{state:"refused",refusal:h,token:t.snapshot.token}})}}throw r}},[t,e.authGeneration]);O(()=>(l().catch(()=>{}),()=>{s.current+=1}),[l]);let y=_(async c=>c(),[]);return{...a,token:e.token,signup:c=>y(()=>t.auth.signup(c)),login:c=>y(()=>t.auth.login(c)),guest:()=>y(()=>t.auth.guest()),logout:async()=>{s.current+=1;let c=await t.auth.logout();if(i.current===t){s.current+=1;let p=t.snapshot.authGeneration;o({client:t,generation:p,value:{state:"anonymous"}})}return c},me:l}}function ke(t,e){let{client:s,snapshot:i}=b(),n=T(e);return w(()=>s.channel(t,e),[s,i.authGeneration,P(t),n])}function Pe(t,e={}){let{client:s,snapshot:i}=b(),n=T({assets:Array.isArray(t)?t.map(p=>p.id):t==null?null:t.id,width:e.width,carrier:e.carrier===!0,download:e.download===!0,authGeneration:i.authGeneration}),o=w(()=>t,[n]),a=w(()=>({client:s,key:n}),[s,n]),[l,y]=C({identity:a}),c=l.identity===a?l.value:void 0;return O(()=>{let p=!0,r=[],h=Array.isArray(o)?o:o==null?[]:[o];return(async()=>{await s.ready;let f=s.assetCache,m=typeof URL.createObjectURL=="function",u=await Promise.all(h.map(g=>m?f?.get(g.id,e.width??null):void 0)),d=h.filter((g,k)=>u[k]===void 0),R=await V(f,d.map(g=>g.id)),x=d.filter(g=>!R.has(g.id)),L=await Promise.all(x.map(g=>s.assetUrl(g,e))),z=new Map(x.map((g,k)=>[g.id,L[k]])),$=h.map((g,k)=>{let j=u[k];if(!j)return z.get(g.id);let Q=URL.createObjectURL(j);return r.push(Q),Q});if(!p){for(let g of r)URL.revokeObjectURL(g);return}y({identity:a,value:Array.isArray(o)?$:$[0]})})().catch(()=>{}),()=>{p=!1;for(let f of r)URL.revokeObjectURL(f)}},[a,o,e.width,e.carrier,e.download]),c}export{ge as SnapbackProvider,E as initialQueryState,U as reduceQueryState,Pe as useAssetUrl,ve as useAuth,ke as useChannel,Re as useConnection,xe as useMutation,Se as useOutbox,ee as useQuery,we as useSnapbackClient,be as useSnapbackSnapshot,Ae as useSubscription};
2
+ //# sourceMappingURL=react-core.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/react-core.tsx", "../src/client-wire.ts", "../src/store/byte-cache.ts", "../src/client-assets.ts", "../src/api.ts", "../src/types.ts"],
4
+ "sourcesContent": ["import React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useReducer,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { createClient, isSnapbackRefusal, type SnapbackRefusal } from \"./client.js\";\nimport { viewedAssetIds } from \"./client-assets.js\";\nimport { referenceName, type Channel as ChannelRef, type Op } from \"./api.js\";\nimport {\n initialQueryState,\n reduceQueryState,\n stableSerialize,\n type AuthMe,\n type AuthSession,\n type AssetRecord,\n type AssetInput,\n type AssetUrlOptions,\n type ChannelHandle,\n type ClientSnapshot,\n type ConnectionState,\n type CreateClientOptions,\n type MutationResult,\n type OfflineClientOptions,\n type OutboxEntry,\n type Principal,\n type QueryHookResult,\n type QueryMachineEvent,\n type QueryView,\n type RefusalEnvelope,\n type SnapbackClient,\n} from \"./types.js\";\n\nexport { initialQueryState, reduceQueryState } from \"./types.js\";\nexport type { QueryView, QueryViewState } from \"./types.js\";\n\ninterface ProviderValue {\n client: SnapbackClient;\n snapshot: ClientSnapshot;\n}\n\nconst Context = createContext<ProviderValue | null>(null);\n\nexport type SnapbackProviderProps = (\n | (CreateClientOptions & { client?: never })\n | { client: SnapbackClient; url?: never; as?: never; token?: never; tokenStore?: never; fetch?: never; offline?: never }\n) & { children: ReactNode };\n\n/**\n * The offline options by value: the carrier factory by identity, the numbers\n * by value. A parent that renders an inline `offline={{ store, onTombstone:\n * (row) => \u2026 }}` allocates a new object and new closures every render; that\n * must not allocate a new lock-holding client, so the callbacks are read\n * through a ref the runtime calls into (`throughRef`) and never compared.\n */\nfunction sameOffline(left: OfflineClientOptions | undefined, right: OfflineClientOptions | undefined): boolean {\n if (left === right) return true;\n if (!left || !right) return false;\n return left.store === right.store\n && left.fenceDays === right.fenceDays\n && left.maxAssetBytes === right.maxAssetBytes;\n}\n\n/** The options the client is created with: stable wrappers that call the latest callbacks the parent rendered. */\nfunction throughRef(latest: { current: OfflineClientOptions | undefined }, options: OfflineClientOptions): OfflineClientOptions {\n return {\n store: options.store,\n ...(options.fenceDays === undefined ? {} : { fenceDays: options.fenceDays }),\n ...(options.maxAssetBytes === undefined ? {} : { maxAssetBytes: options.maxAssetBytes }),\n onTombstone: (row) => latest.current?.onTombstone?.(row) ?? [],\n viewOnce: (table, row) => latest.current?.viewOnce?.(table, row) ?? false,\n };\n}\n\n// @ref LLP 1002#9-the-client-snapback2client-snapback2react-snapback2test \u2014 React web is the first arm over the shared headless client.\nexport function SnapbackProvider({ client: supplied, url, as, token, tokenStore, fetch, offline, children }: SnapbackProviderProps): React.ReactElement {\n if (as !== undefined && (token !== undefined || tokenStore !== undefined)) {\n throw new TypeError(\"SnapbackProvider cannot combine dev persona `as` with bearer token custody\");\n }\n // A client opens its offline partition (and takes the principal's Web Lock)\n // the moment it is created, so it must be created exactly once per identity.\n // StrictMode double-invokes memo factories and discards one result, which\n // would leak a lock-holding client; a ref survives the re-render untouched,\n // and the identity compares `offline` by its values, not its reference.\n const identity = [supplied, url, as, token, tokenStore, fetch] as const;\n const latestOffline = useRef<OfflineClientOptions | undefined>(offline);\n latestOffline.current = offline;\n const held = useRef<{ identity: readonly unknown[]; offline: OfflineClientOptions | undefined; client: SnapbackClient }>(undefined);\n if (!held.current\n || held.current.identity.some((item, index) => item !== identity[index])\n || !sameOffline(held.current.offline, offline)) {\n const stable = offline === undefined ? undefined : throughRef(latestOffline, offline);\n held.current = {\n identity,\n offline,\n client: supplied ?? createClient(as !== undefined ? { url: url!, as, fetch, offline: stable } : { url: url!, token, tokenStore, fetch, offline: stable }),\n };\n }\n const client = held.current.client;\n const [status, setStatus] = useState({ client, snapshot: client.snapshot });\n const snapshot = status.client === client ? status.snapshot : client.snapshot;\n const pendingClose = useRef<{ client: SnapbackClient; timer: ReturnType<typeof setTimeout> } | undefined>(undefined);\n\n useEffect(() => {\n // StrictMode unmounts and remounts synchronously; a close deferred by one\n // tick is cancelled by the remount and still runs on a real unmount.\n if (pendingClose.current?.client === client) {\n clearTimeout(pendingClose.current.timer);\n pendingClose.current = undefined;\n }\n let active = true;\n const unsubscribe = client.subscribeStatus((snapshot) => { if (active) setStatus({ client, snapshot }); });\n setStatus({ client, snapshot: client.snapshot });\n void client.manifest().catch(() => undefined);\n return () => {\n active = false;\n unsubscribe();\n // Supplied clients belong to the caller and may outlive this provider.\n if (!supplied) pendingClose.current = { client, timer: setTimeout(() => client.close(), 0) };\n };\n }, [client, supplied]);\n\n return <Context.Provider value={{ client, snapshot }}>{children}</Context.Provider>;\n}\n\nfunction provider(): ProviderValue {\n const value = useContext(Context);\n if (!value) throw new Error(\"Snapback hooks require <SnapbackProvider>\");\n return value;\n}\n\n/** The provider-owned client, for first-party platform components and custom transports. */\nexport function useSnapbackClient(): SnapbackClient {\n return provider().client;\n}\n\nexport function useSnapbackSnapshot(): ClientSnapshot {\n return provider().snapshot;\n}\n\nexport function useConnection(): ConnectionState {\n return provider().snapshot.connection;\n}\n\n/**\n * Reads a receipt-backed query. It subscribes by default; pass `{ live: false }`\n * for a one-shot read. Receipt state stays `complete`, `capped`, `partial`, or\n * `denied`; `live` and `connection` report the subscription transport separately.\n */\nexport function useQuery<Args, Result>(\n ref: Op<Args, Result>,\n args: NoInfer<Args>,\n options: { live?: false; drain?: boolean } = {},\n): QueryHookResult<Result> {\n const { client, snapshot } = provider();\n const argsKey = stableSerialize(args);\n const stableArgs = useMemo(() => args, [argsKey]);\n const live = options.live !== false;\n if (!live && options.drain) throw new TypeError(\"useQuery drain requires a live subscription\");\n const operation = referenceName(ref);\n const identity = useMemo(\n () => ({ client, key: `${snapshot.authGeneration}:${operation}:${argsKey}:${live}:${options.drain === true}` }),\n [client, snapshot.authGeneration, operation, argsKey, live, options.drain],\n );\n const [machine, dispatch] = useReducer(\n (\n current: { identity: typeof identity; view: QueryView<Result> },\n action: { identity: typeof identity; event: QueryMachineEvent<Result> },\n ) => ({\n identity: action.identity,\n view: reduceQueryState(\n current.identity === action.identity ? current.view : initialQueryState<Result>(),\n action.event,\n ),\n }),\n { identity, view: initialQueryState<Result>() },\n );\n const state = machine.identity === identity ? machine.view : initialQueryState<Result>();\n\n useEffect(() => {\n let active = true;\n if (live) {\n const subscription = client.subscribe(ref, stableArgs, (delivery) => {\n if (active) dispatch({ identity, event: delivery });\n }, { drain: options.drain });\n return () => {\n active = false;\n subscription.close();\n };\n }\n let timer: ReturnType<typeof setTimeout> | undefined;\n void (async () => {\n let backoff = 250;\n while (active) {\n try {\n const result = await client.query(ref, stableArgs);\n if (active) dispatch({ identity, event: result });\n return;\n } catch (error) {\n if (!active) return;\n if (isSnapbackRefusal(error) && error.code === \"E_AUTH_SUPERSEDED\") return;\n if (isSnapbackRefusal(error) && error.retryable) {\n await new Promise<void>((resolve) => { timer = setTimeout(resolve, backoff); });\n backoff = Math.min(backoff * 2, 5_000);\n continue;\n }\n const envelope = isSnapbackRefusal(error)\n ? error.envelope\n : { code: \"E_CLIENT\", family: \"owner\", message: String(error), guide: \"owner/connect\" };\n dispatch({ identity, event: { state: \"refused\", live: false, ...envelope } });\n return;\n }\n }\n })();\n return () => {\n active = false;\n if (timer !== undefined) clearTimeout(timer);\n };\n }, [identity, stableArgs]);\n\n return useMemo(() => ({ ...state, connection: snapshot.connection }), [state, snapshot.connection]);\n}\n\nexport function useSubscription<Args, Result>(\n ref: Op<Args, Result>,\n args: NoInfer<Args>,\n): QueryHookResult<Result> {\n return useQuery(ref, args);\n}\n\nexport interface UseMutationResult<Args, Result> {\n run(args: AssetInput<Args>): Promise<MutationResult<Result>>;\n /** A `run()` is awaiting the owner right now. */\n inFlight: boolean;\n /** This op's intents still queued in the durable outbox, FIFO (LLP 1012 \u00A77). */\n pending: OutboxEntry[];\n /**\n * `run()` will not refuse `E_OFFLINE_ID_BLOCK` for want of a replay grant, a\n * known bound, or capacity for `1 + maxNewIds`: no partition, or one holding a\n * current grant this write fits. False until the device has all three (the\n * first drain reaching its watermark, or a login, earns the grant); gate the\n * send on it rather than retrying the refusal (LLP 1012 \u00A77).\n */\n ready: boolean;\n last: MutationResult<Result> | undefined;\n transport: SnapbackRefusal | undefined;\n}\n\n/** The durable outbox of the open partition, FIFO, with `dismiss` for settled intents. */\nexport interface UseOutboxResult {\n entries: OutboxEntry[];\n /** Grant-present readiness (`client.outbox.ready()`): no particular write bound is implied, and the fence does not gate it. */\n ready: boolean;\n /** Past means queued until this partition resumes; accepting writes remains enabled. */\n fence: \"inside\" | \"past\";\n dismiss(intent: string): Promise<void>;\n}\n\n// @ref LLP 1012#7-writes-offline \u2014 the outbox opens before the first request,\n// so a queued message is on screen at cold start before the network is tried.\nfunction useOutboxState(op?: Op<any, any>): UseOutboxResult {\n const { client, snapshot } = provider();\n const operation = op === undefined ? undefined : referenceName(op);\n const identity = useMemo(() => ({}), [client, snapshot.authGeneration, operation]);\n const [state, setState] = useState<{ identity?: object; entries: OutboxEntry[]; ready: boolean; fence: \"inside\" | \"past\" }>({ entries: [], ready: false, fence: \"inside\" });\n useEffect(() => {\n let active = true;\n let probe = 0;\n const unsubscribe = client.outbox.subscribe((entries) => {\n const current = ++probe;\n const publish = (ready: boolean, fence: \"inside\" | \"past\") => {\n if (active && current === probe) setState({ identity, entries, ready, fence });\n };\n void Promise.all([client.outbox.ready(op), client.outbox.fence()])\n .then(([ready, fence]) => publish(ready, fence), () => publish(false, \"past\"));\n });\n return () => { active = false; unsubscribe(); };\n }, [identity]);\n const dismiss = useCallback((intent: string) => client.outbox.dismiss(intent), [client]);\n return useMemo(() => ({ entries: state.identity === identity ? state.entries : [],\n ready: state.identity === identity && state.ready,\n fence: state.identity === identity ? state.fence : \"inside\", dismiss }), [state, identity, dismiss]);\n}\n\nexport function useOutbox(): UseOutboxResult {\n return useOutboxState();\n}\n\nexport function useMutation<Args, Result>(ref: Op<Args, Result>): UseMutationResult<Args, Result> {\n const { client, snapshot } = provider();\n const operation = referenceName(ref);\n const identity = useMemo(() => ({ client, generation: snapshot.authGeneration, operation }), [client, snapshot.authGeneration, operation]);\n const activeIdentity = useRef(identity);\n activeIdentity.current = identity;\n const { entries, ready } = useOutboxState(ref);\n const pending = useMemo(\n () => entries.filter((entry) => entry.op === operation && entry.state === \"queued\"),\n [entries, operation],\n );\n const [machine, setMachine] = useState<{\n identity: typeof identity;\n pending: number;\n last?: MutationResult<Result>;\n transport?: SnapbackRefusal;\n }>({ identity, pending: 0 });\n const visible = machine.identity === identity\n ? machine\n : { identity, pending: 0 };\n const run = useCallback(async (args: AssetInput<Args>) => {\n const active = () => activeIdentity.current === identity && client.snapshot.authGeneration === identity.generation;\n if (!active()) return client.mutation(ref, args);\n setMachine((current) => current.identity === identity\n ? { ...current, pending: current.pending + 1 }\n : { identity, pending: 1 });\n try {\n const result = await client.mutation(ref, args);\n if (active()) setMachine((current) => ({\n identity,\n pending: current.identity === identity ? current.pending : 0,\n last: result,\n }));\n return result;\n } catch (error) {\n if (active() && isSnapbackRefusal(error) && error.retryable) {\n setMachine((current) => current.identity === identity\n ? { ...current, transport: error }\n : { identity, pending: 0, transport: error });\n }\n throw error;\n } finally {\n if (active()) setMachine((current) => current.identity === identity\n ? { ...current, pending: Math.max(0, current.pending - 1) }\n : current);\n }\n }, [identity]);\n return useMemo(\n () => ({ run, inFlight: visible.pending > 0, pending, ready, last: visible.last, transport: visible.transport }),\n [run, visible.pending, pending, ready, visible.last, visible.transport],\n );\n}\n\nexport type AuthHookState = \"hydrating\" | \"authenticated\" | \"anonymous\" | \"refused\";\nexport interface UseAuthResult {\n state: AuthHookState;\n viewer?: Principal;\n kind?: string;\n token?: string;\n refusal?: RefusalEnvelope;\n signup(input: { email: string; password: string }): Promise<AuthSession>;\n login(input: { email: string; password: string }): Promise<AuthSession>;\n guest(): Promise<AuthSession>;\n logout(): Promise<{ ok: boolean }>;\n me(): Promise<AuthMe>;\n}\n\nexport function useAuth(): UseAuthResult {\n const { client, snapshot } = provider();\n const requestEpoch = useRef(0);\n const activeClient = useRef(client);\n if (activeClient.current !== client) {\n activeClient.current = client;\n requestEpoch.current += 1;\n }\n type AuthState = Omit<UseAuthResult, \"signup\" | \"login\" | \"guest\" | \"logout\" | \"me\">;\n const [machine, setMachine] = useState<{ client: SnapbackClient; generation: number; value: AuthState }>({\n client,\n generation: snapshot.authGeneration,\n value: { state: \"hydrating\", token: snapshot.token },\n });\n const state = machine.client === client && machine.generation === snapshot.authGeneration\n ? machine.value\n : { state: \"hydrating\" as const, token: snapshot.token };\n const me = useCallback(async () => {\n const generation = snapshot.authGeneration;\n const epoch = ++requestEpoch.current;\n try {\n const value = await client.auth.me();\n if (epoch === requestEpoch.current\n && activeClient.current === client\n && client.snapshot.authGeneration === generation) {\n setMachine({ client, generation, value: { state: \"authenticated\", ...value, token: client.snapshot.token } });\n }\n return value;\n } catch (error) {\n if (epoch === requestEpoch.current\n && activeClient.current === client\n && client.snapshot.authGeneration === generation) {\n if (isSnapbackRefusal(error)\n && (error.code === \"E_AUTH\" || error.code === \"E_AUTH_REQUIRED\" || error.code === \"E_AUTH_SESSION\")) {\n setMachine({ client, generation, value: { state: \"anonymous\", refusal: error.envelope } });\n } else if (!isSnapbackRefusal(error) || error.code !== \"E_AUTH_SUPERSEDED\") {\n const refusal = isSnapbackRefusal(error) ? error.envelope : undefined;\n setMachine({ client, generation, value: { state: \"refused\", refusal, token: client.snapshot.token } });\n }\n }\n throw error;\n }\n }, [client, snapshot.authGeneration]);\n\n useEffect(() => {\n void me().catch(() => undefined);\n return () => {\n requestEpoch.current += 1;\n };\n }, [me]);\n\n const establish = useCallback(async (action: () => Promise<AuthSession>) => {\n return action();\n }, []);\n\n return {\n ...state,\n token: snapshot.token,\n signup: (input) => establish(() => client.auth.signup(input)),\n login: (input) => establish(() => client.auth.login(input)),\n guest: () => establish(() => client.auth.guest()),\n logout: async () => {\n requestEpoch.current += 1;\n const result = await client.auth.logout();\n if (activeClient.current === client) {\n requestEpoch.current += 1;\n const generation = client.snapshot.authGeneration;\n setMachine({ client, generation, value: { state: \"anonymous\" } });\n }\n return result;\n },\n me,\n };\n}\n\nexport function useChannel<Args, Payload = unknown>(ref: ChannelRef<Args, Payload>, args: NoInfer<Args>): ChannelHandle<Payload> {\n const { client, snapshot } = provider();\n const argsKey = stableSerialize(args);\n return useMemo(\n () => client.channel<Args, Payload>(ref, args),\n [client, snapshot.authGeneration, referenceName(ref), argsKey],\n );\n}\n\ntype AssetUrlValue = string | (string | undefined)[] | undefined;\n\n/**\n * Resolves records without putting authority in image URLs. A carrier is minted\n * only when explicitly requested for a player/download that cannot send headers.\n */\n// @ref LLP 1006#2b-the-session-carrier-author-ruling-2026-09-03 \u2014 refreshes\n// affect newly resolved URLs only; an already returned player src is immutable.\nexport function useAssetUrl(asset: AssetRecord | null | undefined, options?: AssetUrlOptions): string | undefined;\nexport function useAssetUrl(assets: readonly AssetRecord[], options?: AssetUrlOptions): (string | undefined)[] | undefined;\nexport function useAssetUrl(\n assetOrList: AssetRecord | readonly AssetRecord[] | null | undefined,\n options: AssetUrlOptions = {},\n): AssetUrlValue {\n const { client, snapshot } = provider();\n // A blob: URL belongs to one client identity on one partition: a new client or an\n // auth transition (login, logout, expiry) re-runs the effect, which revokes and re-mints.\n const key = stableSerialize({\n assets: Array.isArray(assetOrList)\n ? assetOrList.map((asset) => asset.id)\n : assetOrList == null ? null : (assetOrList as AssetRecord).id,\n width: options.width,\n carrier: options.carrier === true,\n download: options.download === true,\n authGeneration: snapshot.authGeneration,\n });\n const stableAsset = useMemo(() => assetOrList, [key]);\n const identity = useMemo(() => ({ client, key }), [client, key]);\n const [resolved, setResolved] = useState<{ identity: typeof identity; value?: AssetUrlValue }>({ identity });\n const value = resolved.identity === identity ? resolved.value : undefined;\n useEffect(() => {\n let active = true;\n const created: string[] = [];\n const list: readonly AssetRecord[] = Array.isArray(stableAsset)\n ? stableAsset\n : stableAsset == null ? [] : [stableAsset as AssetRecord];\n // @ref LLP 1012#9-asset-bytes-on-the-device \u2014 a rendition the byte cache holds\n // answers as a blob: URL before any network URL or carrier is minted, and a viewed\n // view-once row gets no URL at all: the guard is the one `assetBlob` applies.\n void (async () => {\n await client.ready;\n const cache = client.assetCache;\n const objectUrls = typeof URL.createObjectURL === \"function\";\n const held = await Promise.all(list.map((asset) => objectUrls ? cache?.get(asset.id, options.width ?? null) : undefined));\n const missing = list.filter((_asset, index) => held[index] === undefined);\n const viewed = await viewedAssetIds(cache, missing.map((asset) => asset.id));\n const mintable = missing.filter((asset) => !viewed.has(asset.id));\n // A viewed guard can drop an entry during minting; preserve each asset's position.\n const minted = await Promise.all(mintable.map((asset) => client.assetUrl(asset, options)));\n const mintedById = new Map(mintable.map((asset, index) => [asset.id, minted[index]]));\n const urls = list.map((asset, index) => {\n const blob = held[index];\n if (!blob) return mintedById.get(asset.id);\n const url = URL.createObjectURL(blob);\n created.push(url);\n return url;\n });\n if (!active) {\n for (const url of created) URL.revokeObjectURL(url);\n return;\n }\n setResolved({ identity, value: Array.isArray(stableAsset) ? urls : urls[0] });\n })().catch(() => undefined);\n return () => {\n active = false;\n for (const url of created) URL.revokeObjectURL(url);\n };\n }, [identity, stableAsset, options.width, options.carrier, options.download]);\n return value;\n}\n", "// @ref LLP 1002#4-the-wire \u2014 every owner response is validated before public client code observes it.\nimport type {\n AuthMe,\n AuthSession,\n ChannelDelivery,\n ChannelPublishResult,\n Cursor,\n LiveQueryDelivery,\n Manifest,\n MutationResult,\n Principal,\n QueryRefusedDelivery,\n RefusalEnvelope,\n RefusedDelivery,\n ServerQueryResult,\n} from \"./types.js\";\n\nexport function fallbackRefusal(code: string, message: string): RefusalEnvelope {\n return { code, family: \"owner\", message, guide: \"owner/connect\" };\n}\n\nconst snapbackRefusalBrand = Symbol.for(\"snapback2.SnapbackRefusal\");\n\nexport class SnapbackRefusal extends Error {\n readonly [snapbackRefusalBrand] = true;\n readonly envelope: RefusalEnvelope;\n readonly code: string;\n\n constructor(envelope: RefusalEnvelope, readonly retryable = false, readonly status?: number) {\n super(envelope.message);\n this.name = \"SnapbackRefusal\";\n this.envelope = envelope;\n this.code = envelope.code;\n }\n}\n\nexport function isSnapbackRefusal(value: unknown): value is SnapbackRefusal {\n return typeof value === \"object\"\n && value !== null\n && (value as { name?: unknown }).name === \"SnapbackRefusal\"\n && (value as Record<symbol, unknown>)[snapbackRefusalBrand] === true;\n}\n\nexport function wireShape(message: string): SnapbackRefusal {\n return new SnapbackRefusal({ code: \"E_WIRE_SHAPE\", family: \"op\", message, guide: \"op/wire-shape\" });\n}\n\nexport function wireObject(value: unknown, surface: string): Record<string, unknown> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw wireShape(`${surface} must be an object`);\n return value as Record<string, unknown>;\n}\n\nfunction has(record: Record<string, unknown>, field: string): boolean {\n return Object.prototype.hasOwnProperty.call(record, field);\n}\n\nfunction present(record: Record<string, unknown>, field: string, surface: string): unknown {\n if (!has(record, field)) throw wireShape(`${surface} is missing ${field}`);\n return record[field];\n}\n\nfunction stringField(record: Record<string, unknown>, field: string, surface: string): string {\n const value = present(record, field, surface);\n if (typeof value !== \"string\" || value.length === 0) throw wireShape(`${surface}.${field} must be a non-empty string`);\n return value;\n}\n\nfunction numberField(record: Record<string, unknown>, field: string, surface: string): number {\n const value = present(record, field, surface);\n if (typeof value !== \"number\" || !Number.isFinite(value)) throw wireShape(`${surface}.${field} must be a number`);\n return value;\n}\n\nfunction optionalString(record: Record<string, unknown>, field: string, surface: string): string | undefined {\n if (!has(record, field)) return undefined;\n const value = record[field];\n if (typeof value !== \"string\") throw wireShape(`${surface}.${field} must be a string`);\n return value;\n}\n\nfunction optionalNumber(record: Record<string, unknown>, field: string, surface: string): number | undefined {\n if (!has(record, field)) return undefined;\n const value = record[field];\n if (typeof value !== \"number\" || !Number.isFinite(value)) throw wireShape(`${surface}.${field} must be a number`);\n return value;\n}\n\nfunction nullableString(record: Record<string, unknown>, field: string, surface: string): string | null {\n const value = present(record, field, surface);\n if (value !== null && typeof value !== \"string\") throw wireShape(`${surface}.${field} must be a string or null`);\n return value;\n}\n\nexport function wireRefusalEnvelope(value: unknown, surface: string): RefusalEnvelope {\n const record = wireObject(value, surface);\n const envelope: RefusalEnvelope = {\n code: stringField(record, \"code\", surface),\n family: stringField(record, \"family\", surface),\n message: stringField(record, \"message\", surface),\n guide: stringField(record, \"guide\", surface),\n };\n for (const field of [\"site\", \"rule\", \"rewrite\"] as const) {\n const item = optionalString(record, field, surface);\n if (item !== undefined) envelope[field] = item;\n }\n const which = optionalString(record, \"which\", surface);\n if (which !== undefined) envelope.which = which;\n const cap = optionalNumber(record, \"cap\", surface);\n if (cap !== undefined) envelope.cap = cap;\n return envelope;\n}\n\nexport function copyRefusal(envelope: RefusalEnvelope): RefusalEnvelope {\n const refusal: RefusalEnvelope = {\n code: envelope.code, family: envelope.family, message: envelope.message, guide: envelope.guide,\n };\n if (envelope.site !== undefined) refusal.site = envelope.site;\n if (envelope.rule !== undefined) refusal.rule = envelope.rule;\n if (envelope.rewrite !== undefined) refusal.rewrite = envelope.rewrite;\n if (envelope.which !== undefined) refusal.which = envelope.which;\n if (envelope.cap !== undefined) refusal.cap = envelope.cap;\n return refusal;\n}\n\nexport function isTerminalSession(envelope: Pick<RefusalEnvelope, \"code\" | \"family\">): boolean {\n return envelope.code === \"E_AUTH_SESSION\" && envelope.family === \"auth\";\n}\n\nexport function wireQuery<Result>(value: unknown, surface = \"query response\"): ServerQueryResult<Result> {\n const record = wireObject(value, surface);\n switch (stringField(record, \"state\", surface)) {\n case \"complete\":\n return {\n state: \"complete\", data: present(record, \"data\", surface) as Result,\n seq: numberField(record, \"seq\", surface), next: nullableString(record, \"next\", surface) as Cursor | null,\n };\n case \"capped\":\n for (const field of [\"site\", \"take\", \"reason\"]) {\n if (has(record, field)) throw wireShape(`${surface}.capped must not carry ${field}`);\n }\n return {\n state: \"capped\", data: present(record, \"data\", surface) as Result,\n seq: numberField(record, \"seq\", surface), next: nullableString(record, \"next\", surface) as Cursor | null,\n };\n case \"partial\": {\n if (has(record, \"next\")) throw wireShape(`${surface}.partial must not carry next`);\n const partial = {\n state: \"partial\" as const,\n seq: numberField(record, \"seq\", surface),\n reason: stringField(record, \"reason\", surface),\n };\n return has(record, \"data\") ? { ...partial, data: record.data as Result } : partial;\n }\n case \"denied\":\n if (has(record, \"next\")) throw wireShape(`${surface}.denied must not carry next`);\n return { state: \"denied\", seq: numberField(record, \"seq\", surface), rule: stringField(record, \"rule\", surface) };\n default:\n throw wireShape(`${surface}.state must be complete, capped, partial, or denied`);\n }\n}\n\nexport function wireMutation<Result>(value: unknown): MutationResult<Result> {\n const surface = \"mutation response\";\n const record = wireObject(value, surface);\n if (has(record, \"next\")) throw wireShape(`${surface} must not carry next`);\n switch (stringField(record, \"state\", surface)) {\n case \"committed\":\n return { state: \"committed\", data: present(record, \"data\", surface) as Result, seq: numberField(record, \"seq\", surface) };\n case \"rejected\":\n return { state: \"rejected\", ...wireRefusalEnvelope(record, \"mutation rejected response\") };\n default:\n throw wireShape(`${surface}.state must be committed or rejected`);\n }\n}\n\nexport function wireSubscription<Result>(data: string): LiveQueryDelivery<Result> | QueryRefusedDelivery {\n const surface = \"subscription delivery\";\n const record = sseObject(data, surface);\n if (stringField(record, \"state\", surface) === \"refused\") {\n if (has(record, \"next\")) throw wireShape(`${surface}.refused must not carry next`);\n const refusal = wireRefusalEnvelope(record, `${surface} refusal`);\n const seq = optionalNumber(record, \"seq\", surface);\n return { state: \"refused\", live: false, ...refusal, ...(seq === undefined ? {} : { seq }) };\n }\n return { ...wireQuery<Result>(record, surface), live: true };\n}\n\nexport function wireChannelDelivery<Payload>(data: string): ChannelDelivery<Payload> {\n const surface = \"channel delivery\";\n const record = sseObject(data, surface);\n if (record.state === \"refused\") {\n return { state: \"refused\", ...wireRefusalEnvelope(record, `${surface} refusal`) } as RefusedDelivery;\n }\n return {\n from: stringField(record, \"from\", surface) as Principal,\n payload: present(record, \"payload\", surface) as Payload,\n at: numberField(record, \"at\", surface),\n };\n}\n\nexport function wireChannelPublish(value: unknown): ChannelPublishResult {\n const surface = \"channel publish response\";\n const record = wireObject(value, surface);\n if (record.ok === true) return { ok: true };\n if (record.state !== \"refused\") throw wireShape(`${surface} must contain ok: true or state: refused`);\n return { state: \"refused\", ...wireRefusalEnvelope(record, `${surface} refusal`) };\n}\n\nexport function terminalSessionEnvelope(value: unknown): RefusalEnvelope | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const record = value as Record<string, unknown>;\n if (record.state !== \"rejected\" && record.state !== \"refused\") return undefined;\n // A replayed write's trace conflict carries no code (LLP 1012 \u00A77); it is never a session terminal.\n if (record.state === \"rejected\" && record.reason === \"conflict\") return undefined;\n const refusal = wireRefusalEnvelope(record, \"terminal session response\");\n return isTerminalSession(refusal) ? refusal : undefined;\n}\n\nexport function wireAuthSession(value: unknown, surface: string): AuthSession {\n const record = wireObject(value, surface);\n return { token: stringField(record, \"token\", surface), viewer: stringField(record, \"viewer\", surface) as Principal };\n}\n\nexport function wireAuthMe(value: unknown): AuthMe {\n const surface = \"auth me response\";\n const record = wireObject(value, surface);\n return { viewer: stringField(record, \"viewer\", surface) as Principal, kind: stringField(record, \"kind\", surface) };\n}\n\nexport function wireLogout(value: unknown): { ok: boolean } {\n const surface = \"auth logout response\";\n const record = wireObject(value, surface);\n if (typeof present(record, \"ok\", surface) !== \"boolean\") throw wireShape(`${surface}.ok must be a boolean`);\n return { ok: record.ok as boolean };\n}\n\nexport function wireManifest(value: unknown): Manifest {\n const surface = \"manifest response\";\n const record = wireObject(value, surface);\n const ops = wireObject(present(record, \"ops\", surface), `${surface}.ops`);\n for (const [name, operation] of Object.entries(ops)) {\n const item = wireObject(operation, `${surface}.ops.${name}`);\n stringField(item, \"kind\", `${surface}.ops.${name}`);\n stringField(item, \"planHash\", `${surface}.ops.${name}`);\n present(item, \"args\", `${surface}.ops.${name}`);\n }\n const manifest: Manifest = {\n generation: stringField(record, \"generation\", surface),\n programGeneration: stringField(record, \"programGeneration\", surface),\n ops: ops as unknown as Manifest[\"ops\"],\n channels: wireObject(present(record, \"channels\", surface), `${surface}.channels`),\n auth: wireObject(present(record, \"auth\", surface), `${surface}.auth`),\n };\n if (typeof record.schemaHash === \"string\") manifest.schemaHash = record.schemaHash;\n if (record.tables && typeof record.tables === \"object\" && !Array.isArray(record.tables)) {\n manifest.tables = record.tables as Manifest[\"tables\"];\n }\n if (record.schema && typeof record.schema === \"object\" && !Array.isArray(record.schema)) {\n manifest.schema = record.schema as Manifest[\"schema\"];\n }\n return manifest;\n}\n\nexport function wireAssetSession(value: unknown): { token: string; expiresAt: number } {\n const surface = \"asset session response\";\n const record = wireObject(value, surface);\n return { token: stringField(record, \"token\", surface), expiresAt: numberField(record, \"expiresAt\", surface) };\n}\n\nexport async function responseRefusal(response: Response): Promise<SnapbackRefusal> {\n let value: unknown;\n try {\n value = await response.json();\n } catch {\n throw new SnapbackRefusal(\n fallbackRefusal(\"E_TRANSPORT\", `HTTP ${response.status} did not contain a refusal envelope`),\n true,\n response.status,\n );\n }\n if (!value || typeof value !== \"object\" || Array.isArray(value)\n || ![\"code\", \"family\", \"guide\"].some((field) => has(value as Record<string, unknown>, field))) {\n throw new SnapbackRefusal(\n fallbackRefusal(\"E_TRANSPORT\", `HTTP ${response.status} did not contain a refusal envelope`),\n true,\n response.status,\n );\n }\n return new SnapbackRefusal(wireRefusalEnvelope(value, `HTTP ${response.status} refusal`), false, response.status);\n}\n\nexport function sseObject(data: string, surface: string): Record<string, unknown> {\n try {\n return wireObject(JSON.parse(data), surface);\n } catch (error) {\n if (isSnapbackRefusal(error)) throw error;\n throw wireShape(`${surface} must contain valid JSON`);\n }\n}\n\n/**\n * `AbortSignal.prototype.throwIfAborted` does not exist on React Native's AbortSignal\n * (Hermes, RN 0.81 / Expo SDK 54), so every abort check goes through this helper.\n */\nexport function throwIfAborted(signal?: AbortSignal | null): void {\n if (!signal?.aborted) return;\n const reason: unknown = (signal as { reason?: unknown }).reason;\n if (reason instanceof Error) throw reason;\n const error = new Error(typeof reason === \"string\" ? reason : \"The operation was aborted\");\n error.name = \"AbortError\";\n throw error;\n}\n", "// @ref LLP 1006#9b-bytes-on-the-device-author-ruling-2026-09-04 \u2014 bytes are cached under the owning row's lease, or not at all.\n// @ref LLP 1012#9-asset-bytes-on-the-device \u2014 the ByteCache sits beside the range store and joins its swap; view-once is excluded at view time.\n\nimport type { RangeStore, RowRef, StoredRow } from \"./range-store.js\";\n\n/** One cached rendition: what a component fetched at the width it asked for. */\nexport interface ByteCacheEntry {\n assetId: string;\n width: number | null;\n size: number;\n type: string;\n /** Logical last-read stamp on the range store's clock; the LRU orders by it. */\n lastRead: number;\n}\n\n/** A view-once row that was viewed, with the asset ids it carried at that moment. */\nexport interface ViewedRecord { at: number; assets: string[] }\n\nexport interface ByteCachePut { assetId: string; width: number | null; bytes: Uint8Array; type: string }\nexport interface ByteSwapRequest { put?: ByteCachePut[]; touch?: string[]; ceiling?: number }\n\n/** What a carrier persists beside the state in the same swap: payloads to write and keys to drop. */\nexport interface ByteDelta { put: Array<{ key: string; bytes: Uint8Array }>; remove: string[] }\n\nexport interface ByteCacheStatus { entries: ByteCacheEntry[]; totalBytes: number; viewed: RowRef[] }\nexport interface AssetBytes { bytes: Uint8Array; type: string }\n/** An opaque identity held by one mounted viewing instance, never shared by asset id. */\nexport interface AssetFetchOptions { signal?: AbortSignal; viewing?: object }\nexport type ByteCacheOutcome = \"cached\" | \"viewed\" | \"unleased\" | \"refused\";\nexport type ViewOncePredicate = (table: string, row: Record<string, unknown>) => boolean;\n\n/** The byte surface the swap engine exposes beside `RangeStore`; only the carriers implement it. */\nexport interface ByteCarrier {\n readBytes(assetId: string, width: number | null): Promise<{ bytes: Uint8Array; type: string } | undefined>;\n bytes(): Promise<ByteCacheStatus>;\n rowsReferencing(assetId: string): Promise<StoredRow[]>;\n viewedAsset(assetId: string): Promise<boolean>;\n viewedRows(): Promise<RowRef[]>;\n /** LRU read stamps, coalesced: stamped in memory now, persisted by the next swap. */\n touchBytes(keys: string[]): Promise<void>;\n commit(swap: { bytes?: ByteSwapRequest; viewed?: RowRef[] }): Promise<void>;\n}\n\n/** The client-facing byte cache: `client.assetCache` when the client has an offline store. */\nexport interface AssetCache {\n /** Session custody changes revoke mounted object URLs. */\n subscribeCustody?(listener: () => void): () => void;\n getCustody?(): number;\n getBytes(assetId: string, width: number | null, viewing?: object): Promise<AssetBytes | undefined>;\n putBytes(assetId: string, width: number | null, value: AssetBytes, viewing?: object): Promise<ByteCacheOutcome>;\n canServe(assetId: string, viewing?: object): Promise<boolean>;\n /** The held bytes for an asset at a width, or nothing: never fetched, evicted with its row, viewed, or sealed. */\n get(assetId: string, width: number | null, viewing?: object): Promise<Blob | undefined>;\n /** True once a view-once row carrying this asset was viewed on this partition. */\n viewed(assetId: string): Promise<boolean>;\n /** True while a held row carrying this asset is view-once (`offline.viewOnce`) and not yet viewed. */\n viewOnce(assetId: string): Promise<boolean>;\n /** Marks the held view-once rows carrying this asset viewed: what a player calls when playback starts. */\n markViewed(assetId: string): Promise<boolean>;\n status(): Promise<ByteCacheStatus>;\n}\n\nexport const ASSET_ID = /^s2id-[0-9a-f]{32}-[0-9a-f]{16}$/;\nconst SESSION_VIEW_LIMIT = 8;\n\nexport function byteKey(assetId: string, width: number | null): string {\n return width === null ? assetId : `${assetId}.w${width}`;\n}\n\nfunction rowKey(ref: RowRef): string {\n return `${ref.table}\\u0000${ref.id}`;\n}\n\nfunction positive(value: unknown): boolean {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value > 0;\n}\n\n/** The closed asset record shape (LLP 1006 \u00A71): exactly id, width, height, and duration for video. */\nexport function assetRecordId(value: unknown): string | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const record = value as Record<string, unknown>;\n const keys = Object.keys(record).sort().join(\",\");\n if (keys !== \"height,id,width\" && keys !== \"duration,height,id,width\") return undefined;\n if (typeof record.id !== \"string\" || !ASSET_ID.test(record.id)) return undefined;\n if (!positive(record.width) || !positive(record.height)) return undefined;\n if (\"duration\" in record && !positive(record.duration)) return undefined;\n return record.id;\n}\n\nexport function assetIdsIn(value: unknown, output = new Set<string>()): Set<string> {\n const id = assetRecordId(value);\n if (id !== undefined) output.add(id);\n else if (Array.isArray(value)) for (const item of value) assetIdsIn(item, output);\n else if (value && typeof value === \"object\") {\n for (const item of Object.values(value as Record<string, unknown>)) assetIdsIn(item, output);\n }\n return output;\n}\n\n/** Assets a held delivery references: through pooled rows and through inline answer data. */\nexport function leasedAssets(rows: Iterable<StoredRow>, answers: Iterable<{ data?: unknown }>): Set<string> {\n const leased = new Set<string>();\n for (const row of rows) assetIdsIn(row.row, leased);\n for (const answer of answers) if (answer.data !== undefined) assetIdsIn(answer.data, leased);\n return leased;\n}\n\nexport interface ByteSwapState {\n clock: number;\n rows: Record<string, StoredRow>;\n assets: Record<string, ByteCacheEntry>;\n viewed: Record<string, ViewedRecord>;\n}\n\n/**\n * View-once is tracked per row, never per asset id: an asset is refused only while\n * every held row carrying it was viewed. A row that is not viewed keeps its lease,\n * so one shared id never poisons an ordinary row's bytes.\n */\nexport function viewedOnlyAssets(state: Pick<ByteSwapState, \"rows\" | \"viewed\">): Set<string> {\n const viewed = new Set<string>();\n for (const record of Object.values(state.viewed)) for (const id of record.assets) viewed.add(id);\n if (viewed.size === 0) return viewed;\n for (const [key, row] of Object.entries(state.rows)) {\n if (state.viewed[key]) continue;\n for (const id of assetIdsIn(row.row)) viewed.delete(id);\n }\n return viewed;\n}\n\nfunction checkedPut(put: ByteCachePut): void {\n if (!put || typeof put !== \"object\" || typeof put.assetId !== \"string\" || !ASSET_ID.test(put.assetId)) {\n throw new TypeError(\"byte cache put must name an engine-minted asset id\");\n }\n if (put.width !== null && !positive(put.width)) throw new TypeError(\"byte cache put width must be null or a positive integer\");\n if (!(put.bytes instanceof Uint8Array)) throw new TypeError(\"byte cache put bytes must be a Uint8Array\");\n if (typeof put.type !== \"string\") throw new TypeError(\"byte cache put type must be a string\");\n}\n\n/**\n * The byte half of one swap, run after the row rules settled `state.rows`: viewed\n * marks, the lease purge, read touches, puts, then the ceiling. Returns what the\n * carrier must write and drop in the same transaction.\n */\nexport function applyByteSwap(\n state: ByteSwapState,\n swap: { bytes?: ByteSwapRequest; viewed?: RowRef[] },\n leased: Set<string>,\n): ByteDelta {\n const delta: ByteDelta = { put: [], remove: [] };\n for (const ref of swap.viewed ?? []) {\n if (!ref || typeof ref.table !== \"string\" || typeof ref.id !== \"string\") throw new TypeError(\"viewed rows must be row references\");\n const key = rowKey(ref);\n const assets = new Set(state.viewed[key]?.assets ?? []);\n assetIdsIn(state.rows[key]?.row, assets);\n state.viewed[key] = { at: Date.now(), assets: [...assets] };\n }\n const viewedAssets = viewedOnlyAssets(state);\n for (const [key, entry] of Object.entries(state.assets)) {\n if (leased.has(entry.assetId) && !viewedAssets.has(entry.assetId)) continue;\n delete state.assets[key];\n delta.remove.push(key);\n }\n for (const key of swap.bytes?.touch ?? []) {\n const entry = state.assets[key];\n if (entry) entry.lastRead = ++state.clock;\n }\n const ceiling = swap.bytes?.ceiling ?? Number.POSITIVE_INFINITY;\n if (!(ceiling > 0)) throw new TypeError(\"byte cache ceiling must be positive\");\n for (const put of swap.bytes?.put ?? []) {\n checkedPut(put);\n if (!leased.has(put.assetId) || viewedAssets.has(put.assetId)) continue;\n const size = put.bytes.byteLength;\n if (size === 0 || size > ceiling) continue;\n const key = byteKey(put.assetId, put.width);\n state.assets[key] = { assetId: put.assetId, width: put.width, size, type: put.type, lastRead: ++state.clock };\n delta.put.push({ key, bytes: put.bytes });\n }\n let total = Object.values(state.assets).reduce((sum, entry) => sum + entry.size, 0);\n while (total > ceiling) {\n const [key, victim] = Object.entries(state.assets)\n .reduce((least, item) => item[1].lastRead < least[1].lastRead ? item : least);\n delete state.assets[key];\n delta.remove.push(key);\n total -= victim.size;\n }\n delta.put = delta.put.filter((item) => state.assets[item.key] !== undefined);\n delta.remove = [...new Set(delta.remove)].filter((key) => state.assets[key] === undefined);\n return delta;\n}\n\nfunction isByteCarrier(store: RangeStore): store is RangeStore & ByteCarrier {\n const candidate = store as Partial<ByteCarrier>;\n return typeof candidate.readBytes === \"function\"\n && typeof candidate.bytes === \"function\"\n && typeof candidate.rowsReferencing === \"function\"\n && typeof candidate.viewedAsset === \"function\"\n && typeof candidate.viewedRows === \"function\"\n && typeof candidate.touchBytes === \"function\";\n}\n\nfunction checkedAssetId(assetId: string): string {\n if (typeof assetId !== \"string\" || !ASSET_ID.test(assetId)) throw new TypeError(\"asset id must be engine-minted\");\n return assetId;\n}\n\n/**\n * Bytes a component fetched, kept under the owning row's lease in the same\n * partition as the rows. A store without a byte carrier caches nothing.\n */\nexport class ByteCache implements AssetCache {\n /**\n * View-once bytes stay renderable inside the instance that viewed them, keyed by\n * the partition that viewed them; nothing here is durable. The runtime forgets it\n * on seal, wipe, partition change, and close, and `get` serves it only under the\n * custody the carrier path applies: this partition, unsealed, the row still held.\n */\n private readonly session = new Map<string, { viewing: object; value: AssetBytes }>();\n private partition?: string;\n private custody = 0;\n private readonly custodyListeners = new Set<() => void>();\n readonly getCustody = () => this.custody;\n readonly subscribeCustody = (listener: () => void) => {\n this.custodyListeners.add(listener);\n return () => { this.custodyListeners.delete(listener); };\n };\n private admissions: Promise<unknown> = Promise.resolve();\n\n constructor(\n readonly store: RangeStore,\n private readonly options: { maxAssetBytes: number; viewOnce?: ViewOncePredicate },\n ) {}\n\n /** The partition the runtime opened; a change drops every session blob. */\n bind(partition: string | undefined): void {\n if (this.partition !== partition) this.forget();\n this.partition = partition;\n }\n\n /** Drops the session blobs: seal, close, and wipe call this. */\n forget(): void {\n this.custody++;\n this.session.clear();\n for (const listener of this.custodyListeners) listener();\n }\n\n async get(assetId: string, width: number | null, viewing?: object): Promise<Blob | undefined> {\n const held = await this.getBytes(assetId, width, viewing);\n return held ? new Blob([held.bytes as BlobPart], { type: held.type }) : undefined;\n }\n\n // @ref LLP 1013#2-decisions \u2014 native paths exchange bytes, never RN Blobs.\n async getBytes(assetId: string, width: number | null, viewing?: object): Promise<AssetBytes | undefined> {\n const custody = this.custody;\n checkedAssetId(assetId);\n const store = this.carrier();\n if (!store) return undefined;\n // `viewedAsset` is false on a sealed partition, so the session is never consulted past a seal.\n if (await store.viewedAsset(assetId)) {\n const held = await this.sessionBytes(assetId, width, viewing);\n return custody === this.custody ? held : undefined;\n }\n if (await this.viewOnce(assetId)) return undefined;\n const held = await store.readBytes(assetId, width);\n if (!held) return undefined;\n // The touch is the LRU stamp, coalesced into the next swap; a closing partition may refuse it.\n await store.touchBytes([byteKey(assetId, width)]).catch(() => undefined);\n return await this.canServe(assetId, viewing) && custody === this.custody ? held : undefined;\n }\n\n /** Completion fence for both a cached source and an uncached ordinary source. */\n async canServe(assetId: string, viewing?: object): Promise<boolean> {\n const custody = this.custody;\n const store = this.carrier();\n if (!store) return false;\n const rows = await store.rowsReferencing(assetId);\n if (rows.length === 0) return false;\n const viewed = await store.viewedAsset(assetId);\n const once = rows.every((row) => this.options.viewOnce?.(row.table, row.row) === true);\n const owns = (!viewed && !once) || [...this.session.entries()].some(([key, entry]) =>\n entry.viewing === viewing && key.startsWith(this.sessionKey(assetId)));\n return custody === this.custody && owns;\n }\n\n async viewed(assetId: string): Promise<boolean> {\n checkedAssetId(assetId);\n const store = this.carrier();\n return store ? store.viewedAsset(assetId) : false;\n }\n\n async viewOnce(assetId: string): Promise<boolean> {\n checkedAssetId(assetId);\n const store = this.carrier();\n if (!store) return false;\n const once = await this.viewOnceRows(assetId);\n if (once.length === 0) return false;\n const viewed = new Set((await store.viewedRows()).map(rowKey));\n return once.some((ref) => !viewed.has(rowKey(ref)));\n }\n\n async markViewed(assetId: string): Promise<boolean> {\n checkedAssetId(assetId);\n const store = this.carrier();\n if (!store) return false;\n const once = await this.viewOnceRows(assetId);\n if (once.length === 0) return false;\n await store.commit({ viewed: once });\n return true;\n }\n\n /** After a network fetch: cache under the row's lease, or mark the view-once rows viewed and cache nothing. */\n async put(assetId: string, width: number | null, blob: Blob, viewing?: object): Promise<ByteCacheOutcome> {\n const custody = this.custody;\n const bytes = new Uint8Array(await blob.arrayBuffer());\n if (custody !== this.custody) return \"refused\";\n return this.putBytes(assetId, width, { bytes, type: blob.type }, viewing);\n }\n\n putBytes(assetId: string, width: number | null, value: AssetBytes, viewing?: object): Promise<ByteCacheOutcome> {\n const custody = this.custody;\n // Two concurrent consumers may fetch; only one can acquire the viewing allowance.\n const result = this.admissions.then(() => custody === this.custody\n ? this.admit(assetId, width, value, viewing) : \"refused\" as const);\n this.admissions = result.catch(() => undefined);\n return result;\n }\n\n private async admit(assetId: string, width: number | null, value: AssetBytes, viewing?: object): Promise<ByteCacheOutcome> {\n const custody = this.custody;\n checkedAssetId(assetId);\n checkedPut({ assetId, width, ...value });\n const store = this.carrier();\n if (!store) return \"unleased\";\n if (await store.viewedAsset(assetId)) return \"refused\";\n const rows = await store.rowsReferencing(assetId);\n if (rows.length === 0 || custody !== this.custody) return \"unleased\";\n const once = rows\n .filter((row) => this.options.viewOnce?.(row.table, row.row) === true)\n .map(({ table, id }) => ({ table, id }));\n // Per row: the view-once rows are marked viewed; an ordinary row sharing the id still leases the bytes.\n if (once.length > 0 && !viewing) return \"refused\";\n if (once.length === rows.length) {\n try {\n await store.commit({ viewed: once });\n } catch {\n return \"refused\";\n }\n if (custody !== this.custody || (await store.rowsReferencing(assetId)).length === 0\n || !await store.viewedAsset(assetId) || custody !== this.custody) return \"unleased\";\n if (this.session.size >= SESSION_VIEW_LIMIT) this.session.delete(this.session.keys().next().value!);\n if (viewing) this.session.set(this.sessionKey(assetId, width), { viewing, value: { bytes: value.bytes.slice(), type: value.type } });\n return \"viewed\";\n }\n try {\n await store.commit({\n ...(once.length === 0 ? {} : { viewed: once }),\n bytes: { put: [{ assetId, width, ...value }], ceiling: this.options.maxAssetBytes },\n });\n } catch {\n return \"refused\";\n }\n const status = await store.bytes();\n return custody === this.custody && status.entries.some((entry) => entry.assetId === assetId && entry.width === width) ? \"cached\" : \"refused\";\n }\n\n async status(): Promise<ByteCacheStatus> {\n const store = this.carrier();\n return store ? store.bytes() : { entries: [], totalBytes: 0, viewed: [] };\n }\n\n /** Nothing is served, marked, or cached without an open partition and a byte carrier. */\n private carrier(): (RangeStore & ByteCarrier) | undefined {\n return this.partition !== undefined && isByteCarrier(this.store) ? this.store : undefined;\n }\n\n private sessionKey(assetId: string, width?: number | null): string {\n return `${this.partition}\\u0000${assetId}\\u0000${width === undefined ? \"\" : width}`;\n }\n\n /** The session blob, only while the row that was viewed is still held on this partition. */\n private async sessionBytes(assetId: string, width: number | null, viewing?: object): Promise<AssetBytes | undefined> {\n const store = this.carrier();\n if (!store) return undefined;\n const key = this.sessionKey(assetId, width);\n const blob = this.session.get(key);\n if (!viewing || blob?.viewing !== viewing) return undefined;\n if ((await store.rowsReferencing(assetId)).length > 0) return blob.value;\n this.session.delete(key);\n return undefined;\n }\n\n private async viewOnceRows(assetId: string): Promise<RowRef[]> {\n const store = this.carrier();\n if (!store) return [];\n const rows = await store.rowsReferencing(assetId);\n return rows\n .filter((row) => this.options.viewOnce?.(row.table, row.row) === true)\n .map(({ table, id }) => ({ table, id }));\n }\n}\n", "// @ref LLP 1012#9-asset-bytes-on-the-device \u2014 the byte cache is read before the\n// network and filled after it; a viewed view-once row is refused before any byte moves.\nimport { AssetInputError } from \"./assets.js\";\nimport { SnapbackRefusal } from \"./client-wire.js\";\nimport { ASSET_ID, type AssetCache, type AssetBytes, type ByteCache } from \"./store/byte-cache.js\";\nimport type { RefusalEnvelope } from \"./types.js\";\n\nexport interface AssetBlobTarget { url: string; assetId: string; width: number | null }\n\n/** The tokenless asset URL this client issued, split into the cache key it names. */\nexport function assetBlobTarget(baseUrl: string, url: string): AssetBlobTarget {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new AssetInputError(\"E_ASSET_TYPE\", \"asset blob URL must be absolute\");\n }\n const base = new URL(baseUrl);\n if (parsed.origin !== base.origin || !parsed.pathname.startsWith(\"/api/asset/\") || parsed.searchParams.has(\"s\")) {\n throw new AssetInputError(\"E_ASSET_TYPE\", \"asset blob fetch requires a tokenless URL issued by this client\");\n }\n const assetId = decodeURIComponent(parsed.pathname.slice(\"/api/asset/\".length));\n const raw = parsed.searchParams.get(\"w\");\n let width: number | null = null;\n if (raw !== null) {\n width = Number(raw);\n if (!Number.isSafeInteger(width) || width <= 0) throw new AssetInputError(\"E_ASSET_TYPE\", \"asset width must be a positive integer\");\n }\n return { url, assetId: ASSET_ID.test(assetId) ? assetId : \"\", width };\n}\n\nexport function viewedRefusal(assetId: string): SnapbackRefusal {\n return new SnapbackRefusal({\n code: \"E_ASSET_VIEWED\",\n family: \"offline\",\n message: `asset ${assetId} belongs to a view-once row that was already viewed on this device`,\n site: \"assetBlob\",\n rewrite: \"render a view-once row once; do not fetch its bytes again\",\n guide: \"offline\",\n });\n}\n\n/**\n * The one viewed guard every byte path consults after the cache missed and before\n * a URL is minted or a byte moves: the ids whose view-once row was already viewed\n * on this partition. `assetBlob` refuses them; `useAssetUrl` mints no URL for them.\n */\nexport async function viewedAssetIds(cache: AssetCache | undefined, ids: readonly string[]): Promise<Set<string>> {\n const viewed = new Set<string>();\n if (!cache) return viewed;\n await Promise.all(ids.map(async (id) => {\n if (ASSET_ID.test(id) && await cache.viewed(id)) viewed.add(id);\n }));\n return viewed;\n}\n\n/**\n * Cache-first bytes for one asset rendition. Without a byte cache this is the\n * network fetch; with one, a held rendition (or what this instance showed for a\n * view-once row) is answered without the network, a viewed view-once row is refused\n * before any byte moves, and a fetched rendition is kept under its row's lease.\n */\nexport async function cachedAssetBlob(\n cache: ByteCache | undefined,\n target: AssetBlobTarget,\n network: () => Promise<Blob>,\n record: (refusal: RefusalEnvelope) => void, current: () => void, viewing?: object,\n): Promise<Blob> {\n const value = await cachedAssetBytes(cache, target, async () => {\n const blob = await network();\n return { bytes: new Uint8Array(await blob.arrayBuffer()), type: blob.type };\n }, record, current, viewing);\n return new Blob([value.bytes as BlobPart], { type: value.type });\n}\n\n/** Native byte ingress; the caller fences custody after each awaited read. */\nexport async function cachedAssetBytes(\n cache: ByteCache | undefined, target: AssetBlobTarget, network: () => Promise<AssetBytes>,\n record: (refusal: RefusalEnvelope) => void, current: () => void, viewing?: object,\n): Promise<AssetBytes> {\n if (!cache || target.assetId === \"\") return network();\n const held = await cache.getBytes(target.assetId, target.width, viewing);\n current();\n if (held) return held;\n const viewed = await cache.viewed(target.assetId);\n current();\n if (viewed) {\n const refusal = viewedRefusal(target.assetId);\n record(refusal.envelope);\n throw refusal;\n }\n const initiallyOnce = await cache.viewOnce(target.assetId);\n if (initiallyOnce && !viewing) {\n const refusal = new SnapbackRefusal({ code: \"E_ASSET_TYPE\", family: \"input\", site: \"assetBlob\",\n message: \"view-once bytes require a mounted viewing instance\",\n rewrite: \"pass the same viewing token for the lifetime of the mounted consumer\", guide: \"assets/client\" });\n record(refusal.envelope);\n throw refusal;\n }\n // Revocation during the fetch must also suppress an initially view-once row.\n const leased = initiallyOnce || await cache.canServe(target.assetId);\n current();\n const value = await network();\n current();\n const admitted = await cache.putBytes(target.assetId, target.width, value, viewing);\n current();\n const once = await cache.viewOnce(target.assetId);\n const viewedNow = await cache.viewed(target.assetId);\n const allowed = await cache.canServe(target.assetId, viewing);\n current();\n if ((once && admitted !== \"viewed\" && admitted !== \"cached\") || ((leased || once || viewedNow || admitted === \"viewed\") && !allowed)) {\n if (viewedNow && !allowed) {\n const refusal = viewedRefusal(target.assetId);\n record(refusal.envelope);\n throw refusal;\n }\n const refusal = new SnapbackRefusal({ code: \"E_OFFLINE_CARRIER\", family: \"offline\",\n site: \"assetBytes\", message: \"asset viewing allowance was not durably admitted or its row lease changed\",\n rewrite: \"restore the carrier and fetch a currently held, unviewed row\", guide: \"offline\" });\n record(refusal.envelope);\n throw refusal;\n }\n return value;\n}\n", "export const referencePath: unique symbol = Symbol.for(\"snapback.referencePath\");\n\nexport interface Op<Args, Result> {\n readonly op: string;\n readonly [referencePath]: string;\n readonly __args?: (args: Args) => void;\n readonly __result?: Result;\n}\n\nexport interface Channel<Args, Payload = unknown> {\n readonly [referencePath]: string;\n readonly __channelArgs?: (args: Args) => void;\n readonly __payload?: Payload;\n}\n\nexport interface Api {}\n\nexport type AnyOp = Op<any, any>;\n\nexport interface ApiProxy {\n readonly [referencePath]: string;\n}\n\nconst proxies = new Map<string, ApiProxy>();\n\nfunction proxy(path: string[]): ApiProxy {\n const op = path.join(\".\");\n const existing = proxies.get(op);\n if (existing) return existing;\n\n const target = Object.assign({}, { op, [referencePath]: op }) as ApiProxy;\n const value = new Proxy(target, {\n get(_current, property) {\n if (property === referencePath) return op;\n if (property === Symbol.toStringTag) return \"SnapbackOp\";\n if (property === \"then\") return undefined;\n if (property === \"op\" && path.length >= 2) return op;\n return typeof property === \"string\" ? proxy([...path, property]) : undefined;\n },\n });\n proxies.set(op, value);\n return value;\n}\n\nexport function referenceName(ref: ApiProxy): string {\n return ref[referencePath];\n}\n\n// @ref LLP 1002#9-the-client-snapback2client-snapback2react-snapback2test \u2014 generated Api augmentation types the inert operation proxy.\nexport const api: Api = proxy([]) as Api;\n", "import type { AssetFetchOptions } from \"./store/byte-cache.js\";\nimport type { Channel as ChannelRef, Op } from \"./api.js\";\nimport type { AssetCache, ByteCacheEntry, ByteCacheStatus, ViewOncePredicate } from \"./store/byte-cache.js\";\nimport type { OutboxEntry, RangeStoreFactory, RowRef, StoredRow } from \"./store/range-store.js\";\n\nexport type { OutboxEntry, OutboxGrant, OutboxState, OutboxTrace } from \"./store/range-store.js\";\n\nexport type { AssetCache, ByteCacheEntry, ByteCacheStatus, ViewOncePredicate };\n\n// @ref LLP 1006#1-the-move \u2014 the phantom kind keeps the two closed asset records\n// nominal; these must stay identical to the package-root declarations the\n// generated `snapback2` API imports, or a client-minted record cannot flow into\n// a generated argument.\nexport type ImageAsset = { readonly __asset: \"image\"; readonly id: string; readonly width: number; readonly height: number };\nexport type VideoAsset = { readonly __asset: \"video\"; readonly id: string; readonly width: number; readonly height: number; readonly duration: number };\n\n/** Server-minted authority subject; structurally compatible with `snapback2.Principal`. */\nexport type Principal = string & { readonly __snapbackPrincipal: \"Principal\" };\nexport type Cursor = string & { readonly __snapbackCursor: \"Cursor\" };\n\nexport interface RefusalEnvelope {\n code: string;\n family: string;\n message: string;\n site?: string;\n rule?: string;\n rewrite?: string;\n guide: string;\n /** Offline resume detail identifying the request member that exceeded a cap. */\n which?: string;\n /** Offline resume detail limiting the next bounded request. */\n cap?: number;\n}\n\n/** Opt-in body fields for a query request that asks for local-first custody. */\nexport interface QueryReceiptRequest {\n readonly receipt: true;\n readonly validator?: string;\n}\n\n/** Opt-in query fields for a subscription request that asks for local-first custody. */\nexport interface SubscribeReceiptRequest {\n readonly receipt: true;\n readonly validator?: string;\n}\n\nexport type QueryComplete<Result> = { state: \"complete\"; data: Result; seq: number; next: Cursor | null };\nexport type QueryCapped<Result> = {\n state: \"capped\";\n data: Result;\n seq: number;\n next: Cursor | null;\n};\nexport type QueryPartial<Result> = {\n state: \"partial\";\n data?: Result;\n seq: number;\n reason: string;\n};\nexport type QueryDenied = {\n state: \"denied\";\n seq: number;\n rule: string;\n};\nexport type ServerQueryResult<Result> =\n | QueryComplete<Result>\n | QueryCapped<Result>\n | QueryPartial<Result>\n | QueryDenied;\n\nexport type OfflineFacts = {\n live: false;\n connection: \"offline\";\n stale: { since: number };\n retained?: boolean;\n caughtUp?: boolean;\n watermark?: Cursor | null;\n /** Queued intents whose predicted rows are inside `data`, in order (the twin, LLP 1012 \u00A77). */\n predicted?: string[];\n /** A local `partial`: the uncovered site and every uncovered site with its reason. */\n site?: string;\n sites?: Array<{ site: string; reason: \"never-held\" | \"evicted\" | \"computed-key\" }>;\n};\nexport type OfflineQueryResult<Result> =\n | (QueryComplete<Result> & OfflineFacts)\n | (QueryCapped<Result> & OfflineFacts)\n | (QueryPartial<Result> & OfflineFacts)\n | (QueryDenied & OfflineFacts)\n | { state: \"hydrating\"; live: false; connection: \"offline\" };\nexport type QueryResult<Result> = ServerQueryResult<Result> | OfflineQueryResult<Result>;\n\nexport type MutationCommitted<Result> = { state: \"committed\"; seq: number; data: Result; intent?: string };\nexport type MutationRejected = {\n state: \"rejected\";\n code: string;\n family: string;\n message: string;\n retryable?: boolean;\n rewrite?: string;\n site?: string;\n rule?: string;\n guide: string;\n /** `conflict` when trace validation rolled a replayed write back (LLP 1012 \u00A77). */\n reason?: string;\n intent?: string;\n};\n/** The write is durable in this device's outbox and will replay after the next resume. */\nexport type MutationQueued = { state: \"queued\"; intent: string; ids: string[] };\nexport type MutationResult<Result> = MutationCommitted<Result> | MutationRejected | MutationQueued;\n\nexport type LiveQueryDelivery<Result> = ServerQueryResult<Result> & { live: true; caughtUp?: boolean; watermark?: Cursor | null };\nexport type StaleDelivery<Result> = {\n state: \"stale\";\n live: false;\n since: number;\n data?: Result;\n seq?: number;\n};\nexport type RefusedDelivery = {\n state: \"refused\";\n code: string;\n family: string;\n message: string;\n site?: string;\n rule?: string;\n rewrite?: string;\n guide: string;\n seq?: number;\n};\nexport type QueryRefusedDelivery = RefusedDelivery & { live: false };\nexport type SubscriptionDelivery<Result> =\n | LiveQueryDelivery<Result>\n | (ServerQueryResult<Result> & { live: false; caughtUp?: boolean; watermark?: Cursor | null })\n | OfflineQueryResult<Result>\n | StaleDelivery<Result>\n | QueryRefusedDelivery;\n\nexport interface Subscription<Event> {\n close(): void;\n until(predicate: (event: Event) => boolean, timeoutMs?: number): Promise<Event>;\n}\n\nexport interface AuthSession { token: string; viewer: Principal }\nexport interface AuthMe { viewer: Principal; kind: string }\nexport interface AuthApi {\n signup(input: { email: string; password: string }): Promise<AuthSession>;\n login(input: { email: string; password: string }): Promise<AuthSession>;\n guest(): Promise<AuthSession>;\n logout(): Promise<{ ok: boolean }>;\n me(): Promise<AuthMe>;\n}\n\nexport interface ChannelEvent<Payload = unknown> { from: Principal; payload: Payload; at: number }\nexport type ChannelDelivery<Payload = unknown> = ChannelEvent<Payload> | RefusedDelivery;\nexport type ChannelPublishResult =\n | { ok: true }\n | ({ state: \"refused\" } & RefusalEnvelope);\nexport interface ChannelHandle<Payload = unknown> {\n publish(payload: Payload): Promise<ChannelPublishResult>;\n subscribe(callback: (event: ChannelDelivery<Payload>) => void): Subscription<ChannelDelivery<Payload>>;\n}\n\nexport interface ManifestOperation { kind: string; args: unknown; planHash: string }\nexport interface ManifestTable { retain?: \"until-revoked\" | \"delivered-history\"; [key: string]: unknown }\nexport interface Manifest {\n generation: string;\n programGeneration: string;\n ops: Record<string, ManifestOperation>;\n channels: Record<string, unknown>;\n auth: Record<string, unknown>;\n schemaHash?: string;\n tables?: Record<string, ManifestTable>;\n schema?: { tables?: Record<string, ManifestTable>; [key: string]: unknown };\n}\n\nexport type AssetRecord = ImageAsset | VideoAsset;\nexport interface NativeAssetSource {\n readonly uri: string;\n readonly size?: number;\n readonly fileSize?: number;\n readonly type?: string;\n readonly mimeType?: string;\n}\nexport type AssetUploadSource = File | NativeAssetSource;\nexport interface UploadProgress {\n readonly loaded: number;\n readonly total: number;\n}\nexport type AssetKind = \"image\" | \"video\";\nexport interface AssetUploadOptions {\n /**\n * The nominal record expected back. The server sniffs the bytes and decides the\n * kind; when `kind` is given the client verifies the returned record and refuses\n * `E_ASSET_TYPE` on a mismatch, so the result can flow straight into a generated\n * `image`/`video` argument (LLP 1006 \u00A710). Without it the result is the union.\n */\n readonly kind?: AssetKind;\n /**\n * Reports streamed byte progress where the runtime supports request streams.\n * Expo native File uploads use expo/fetch's direct File body and report only\n * whole-body start and successful completion (0, then total).\n */\n readonly onProgress?: (progress: UploadProgress) => void;\n}\nexport interface AssetUrlOptions {\n readonly width?: number;\n readonly carrier?: boolean;\n readonly download?: boolean;\n}\n\n/** Replaces generated asset leaves with the browser/native upload convenience accepted by the SDK. */\nexport type AssetInput<Value> =\n Value extends VideoAsset ? Value | File | NativeAssetSource\n : Value extends ImageAsset ? Value | File | NativeAssetSource\n : Value extends File ? Value\n : Value extends readonly (infer Item)[] ? AssetInput<Item>[]\n : Value extends object ? { [Key in keyof Value]: AssetInput<Value[Key]> }\n : Value;\n\nexport type ConnectionState = \"connected\" | \"reconnecting\" | \"offline\";\nexport interface ClientSnapshot {\n /** Client-local cache/transport partition; never a server bearer or epoch. */\n authGeneration: number;\n token?: string;\n persona?: string;\n connection: ConnectionState;\n generation?: string;\n lastRefusal?: RefusalEnvelope;\n}\n\nexport interface AuthTokenStore {\n /** Restores the bearer before `client.ready` settles. */\n load(): string | null | undefined | Promise<string | null | undefined>;\n /** Persists a bearer, or clears custody with `null`. */\n save(token: string | null): void | Promise<void>;\n /** Optional companion custody needed to reopen a per-principal offline partition before network I/O. */\n loadPrincipal?(): string | null | undefined | Promise<string | null | undefined>;\n savePrincipal?(principal: string | null): void | Promise<void>;\n}\n\ninterface ClientLocationOptions {\n url: string;\n fetch?: typeof globalThis.fetch;\n offline?: OfflineClientOptions;\n}\n\nexport interface OfflineClientOptions {\n store: RangeStoreFactory;\n fenceDays?: number;\n /** Ceiling on cached asset bytes per partition, LRU by each rendition's own last read; default 256 MiB. */\n maxAssetBytes?: number;\n onTombstone?: (row: StoredRow) => RowRef[];\n /**\n * Names the rows whose media renders once: a viewed row is excluded from every\n * later offline answer and its bytes are never cached (LLP 1012 \u00A79). This stands\n * in for LLP 1006's one-use delivery until that lands on the server.\n */\n viewOnce?: ViewOncePredicate;\n}\n\n/** A client has exactly one authority source: a dev persona or bearer custody. */\nexport type CreateClientOptions = ClientLocationOptions & (\n | { as: string; token?: never; tokenStore?: never }\n | { as?: never; token?: string; tokenStore?: AuthTokenStore }\n);\n\n// @ref LLP 1012#7-writes-offline \u2014 the outbox is readable before the first\n// request, so a queued message is on screen at cold start.\nexport type OutboxSettlement = void | { stopped: \"fence\" };\n\nexport interface OutboxApi {\n /** Every durable intent of the open partition, FIFO; empty without a store. */\n entries(): Promise<OutboxEntry[]>;\n /** Drops one settled entry; queued intents cannot be dismissed. */\n dismiss(intent: string): Promise<void>;\n /** Observes the outbox; called with the current entries at once and after every change. */\n subscribe(listener: (entries: OutboxEntry[]) => void): () => void;\n /**\n * Without an op, true when a write will not refuse `E_OFFLINE_ID_BLOCK` for\n * want of a replay grant: no partition is open (today's bytes), or the open\n * partition holds a current, unexpired grant \u2014 minted by a login, a guest\n * call, or the first drain reaching its watermark. The fence gates replay and\n * prediction, not this (see `fence`). With an op, also means this write would\n * not refuse `E_OFFLINE_ID_BLOCK` for want of a known bound or capacity for\n * `1 + maxNewIds`. Grant, bound, fence, and expiry changes are observable\n * through `subscribe` even when the entries are unchanged (LLP 1012 \u00A77).\n */\n ready<Args = unknown, Result = unknown>(op?: Op<Args, Result>): Promise<boolean>;\n /** Past means replay waits for this partition's successful resume; enqueue remains ready. */\n fence(): Promise<\"inside\" | \"past\">;\n /** Replays FIFO; a fence stop returns { stopped: \"fence\" } with the intents still queued. */\n drain(): Promise<OutboxSettlement>;\n /**\n * Resolves once the in-flight replay, if any, emptied the queue or a retryable\n * transport failure stopped it; returns { stopped: \"fence\" } if resume is\n * required, and rejects with a non-retryable refusal that stopped it.\n */\n settled(): Promise<OutboxSettlement>;\n}\n\nexport interface SnapbackClient {\n readonly auth: AuthApi;\n readonly outbox: OutboxApi;\n /** Settles only after explicit token restoration and any initial persistence. */\n readonly ready: Promise<void>;\n readonly snapshot: ClientSnapshot;\n query<Args, Result>(ref: Op<Args, Result>, args: NoInfer<Args>): Promise<QueryResult<Result>>;\n mutation<Args, Result>(ref: Op<Args, Result>, args: NoInfer<AssetInput<Args>>): Promise<MutationResult<Result>>;\n /** Uploads one browser File or Expo URI-backed File and returns its closed asset record. */\n upload(source: AssetUploadSource, options: AssetUploadOptions & { readonly kind: \"image\" }): Promise<ImageAsset>;\n upload(source: AssetUploadSource, options: AssetUploadOptions & { readonly kind: \"video\" }): Promise<VideoAsset>;\n upload(source: AssetUploadSource, options?: AssetUploadOptions): Promise<AssetRecord>;\n assetUrl(asset: AssetRecord | null | undefined, options?: AssetUrlOptions): Promise<string | undefined>;\n assetUrl(assets: readonly AssetRecord[], options?: AssetUrlOptions): Promise<string[]>;\n /** Authentication headers for a native image source; carriers stay reserved for players/downloads. */\n assetHeaders(): Promise<Record<string, string>>;\n /** Fetches a client-issued asset URL: the byte cache under the row's lease first, then the network with the header credential. */\n assetBlob(url: string, options?: AssetFetchOptions): Promise<Blob>;\n /** Native byte path: cache first, then an authenticated ArrayBuffer response; no Blob. */\n assetBytes(url: string, options?: AssetFetchOptions): Promise<import(\"./store/byte-cache.js\").AssetBytes>;\n /** The partition's byte cache when the client has an offline store; components read it before the network. */\n readonly assetCache: AssetCache | undefined;\n /** Delivers validated query receipt states with `live: true`; transport loss is client-made `stale`. */\n subscribe<Args, Result>(\n ref: Op<Args, Result>,\n args: NoInfer<Args>,\n callback: (delivery: SubscriptionDelivery<Result>) => void,\n options?: { drain?: boolean },\n ): Subscription<SubscriptionDelivery<Result>>;\n channel<Args, Payload = unknown>(ref: ChannelRef<Args, Payload>, args: NoInfer<Args>): ChannelHandle<Payload>;\n manifest(): Promise<Manifest>;\n subscribeStatus(callback: (snapshot: ClientSnapshot) => void): () => void;\n /** Cut only this client's transport; the owner remains running. */\n offline(): Promise<void>;\n /** Restore transport and perform resume before new subscription opens. */\n online(): Promise<void>;\n /** Close and reopen this logical device on the same durable partition. */\n restart(): Promise<SnapbackClient>;\n /** Resolves when the offline carrier is quiet; safe to remove the store after. */\n close(): Promise<void>;\n}\n\ntype ViewFacts = { live: boolean; stale?: { since: number }; retained?: boolean; caughtUp?: boolean; watermark?: Cursor | null; predicted?: string[] };\nexport type QueryView<Result> =\n | (QueryComplete<Result> & ViewFacts)\n | (QueryCapped<Result> & ViewFacts)\n | (QueryPartial<Result> & ViewFacts)\n | (QueryDenied & ViewFacts)\n | { state: \"refused\"; live: false; refusal: RefusalEnvelope }\n | { state: \"stale\"; live: false; data?: Result; seq?: number; since: number }\n | { state: \"hydrating\"; live: false };\nexport type QueryViewState = QueryView<unknown>[\"state\"];\nexport type QueryHookResult<Result> = QueryView<Result> & { connection: ConnectionState };\nexport type QueryMachineEvent<Result> = QueryResult<Result> | SubscriptionDelivery<Result>;\n\nexport function initialQueryState<Result>(): QueryView<Result> {\n return { state: \"hydrating\", live: false };\n}\n\nfunction canonical(value: unknown): string {\n if (value === undefined) return \"undefined\";\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(canonical).join(\",\")}]`;\n return `{${Object.keys(value as object)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`)\n .join(\",\")}}`;\n}\n\nexport function stableSerialize(value: unknown): string {\n return canonical(value);\n}\n\n// @ref LLP 1002#9-the-client-snapback2client-snapback2react-snapback2test \u2014 React preserves data identity while exposing every receipt-backed state.\nexport function reduceQueryState<Result>(\n current: QueryView<Result>,\n event: QueryMachineEvent<Result>,\n): QueryView<Result> {\n const eventData = \"data\" in event ? event.data : undefined;\n const currentData = \"data\" in current ? current.data : undefined;\n const data = canonical(eventData) === canonical(currentData) ? currentData : eventData;\n const live = \"live\" in event ? event.live : false;\n const facts = {\n ...(\"stale\" in event && event.stale !== undefined ? { stale: event.stale } : {}),\n ...(\"retained\" in event && event.retained !== undefined ? { retained: event.retained } : {}),\n ...(\"caughtUp\" in event && event.caughtUp !== undefined ? { caughtUp: event.caughtUp } : {}),\n ...(\"watermark\" in event && event.watermark !== undefined ? { watermark: event.watermark } : {}),\n ...(\"predicted\" in event && event.predicted !== undefined ? { predicted: event.predicted } : {}),\n };\n let next: QueryView<Result>;\n switch (event.state) {\n case \"complete\":\n next = { state: \"complete\", live, data: data as Result, seq: event.seq, next: event.next, ...facts };\n break;\n case \"capped\":\n next = { state: \"capped\", live, data: data as Result, seq: event.seq, next: event.next, ...facts };\n break;\n case \"partial\":\n next = data === undefined\n ? { state: \"partial\", live, seq: event.seq, reason: event.reason, ...facts }\n : { state: \"partial\", live, data: data as Result, seq: event.seq, reason: event.reason, ...facts };\n break;\n case \"denied\":\n next = { state: \"denied\", live, rule: event.rule, seq: event.seq, ...facts };\n break;\n case \"hydrating\":\n next = { state: \"hydrating\", live: false };\n break;\n case \"refused\": {\n const refusal: RefusalEnvelope = {\n code: event.code,\n family: event.family,\n message: event.message,\n guide: event.guide,\n };\n if (event.site !== undefined) refusal.site = event.site;\n if (event.rule !== undefined) refusal.rule = event.rule;\n if (event.rewrite !== undefined) refusal.rewrite = event.rewrite;\n next = { state: \"refused\", live: false, refusal };\n break;\n }\n case \"stale\":\n next = data === undefined\n ? { state: \"stale\", live: false, ...(event.seq === undefined ? {} : { seq: event.seq }), since: event.since }\n : {\n state: \"stale\",\n live: false,\n data: data as Result,\n ...(event.seq === undefined ? {} : { seq: event.seq }),\n since: event.since,\n };\n break;\n }\n return canonical(current) === canonical(next) ? current : next;\n}\n"],
5
+ "mappings": "AAAA,OAAOA,GACL,iBAAAC,EACA,eAAAC,EACA,cAAAC,EACA,aAAAC,EACA,WAAAC,EACA,cAAAC,EACA,UAAAC,EACA,YAAAC,MAEK,QACP,OAAS,gBAAAC,EAAc,qBAAAC,MAA+C,mBCUtE,IAAMC,EAAuB,OAAO,IAAI,2BAA2B,EAEtDC,EAAN,cAA8B,KAAM,CAKzC,YAAYC,EAAoCC,EAAY,GAAgBC,EAAiB,CAC3F,MAAMF,EAAS,OAAO,EADwB,eAAAC,EAA4B,YAAAC,EAE1E,KAAK,KAAO,kBACZ,KAAK,SAAWF,EAChB,KAAK,KAAOA,EAAS,IACvB,CATA,CAAUF,CAAoB,EAAI,GACzB,SACA,IAQX,EC4BO,IAAMK,EAAW,mCCfxB,eAAsBC,EAAeC,EAA+BC,EAA8C,CAChH,IAAMC,EAAS,IAAI,IACnB,OAAKF,GACL,MAAM,QAAQ,IAAIC,EAAI,IAAI,MAAOE,GAAO,CAClCC,EAAS,KAAKD,CAAE,GAAK,MAAMH,EAAM,OAAOG,CAAE,GAAGD,EAAO,IAAIC,CAAE,CAChE,CAAC,CAAC,EACKD,CACT,CCtDO,IAAMG,EAA+B,OAAO,IAAI,wBAAwB,EAuBzEC,EAAU,IAAI,IAEpB,SAASC,EAAMC,EAA0B,CACvC,IAAMC,EAAKD,EAAK,KAAK,GAAG,EAClBE,EAAWJ,EAAQ,IAAIG,CAAE,EAC/B,GAAIC,EAAU,OAAOA,EAErB,IAAMC,EAAS,OAAO,OAAO,CAAC,EAAG,CAAE,GAAAF,EAAI,CAACJ,CAAa,EAAGI,CAAG,CAAC,EACtDG,EAAQ,IAAI,MAAMD,EAAQ,CAC9B,IAAIE,EAAUC,EAAU,CACtB,GAAIA,IAAaT,EAAe,OAAOI,EACvC,GAAIK,IAAa,OAAO,YAAa,MAAO,aAC5C,GAAIA,IAAa,OACjB,OAAIA,IAAa,MAAQN,EAAK,QAAU,EAAUC,EAC3C,OAAOK,GAAa,SAAWP,EAAM,CAAC,GAAGC,EAAMM,CAAQ,CAAC,EAAI,MACrE,CACF,CAAC,EACD,OAAAR,EAAQ,IAAIG,EAAIG,CAAK,EACdA,CACT,CAEO,SAASG,EAAcC,EAAuB,CACnD,OAAOA,EAAIX,CAAa,CAC1B,CAGO,IAAMY,GAAWV,EAAM,CAAC,CAAC,ECkTzB,SAASW,GAA+C,CAC7D,MAAO,CAAE,MAAO,YAAa,KAAM,EAAM,CAC3C,CAEA,SAASC,EAAUC,EAAwB,CACzC,OAAIA,IAAU,OAAkB,YAC5BA,IAAU,MAAQ,OAAOA,GAAU,SAAiB,KAAK,UAAUA,CAAK,EACxE,MAAM,QAAQA,CAAK,EAAU,IAAIA,EAAM,IAAID,CAAS,EAAE,KAAK,GAAG,CAAC,IAC5D,IAAI,OAAO,KAAKC,CAAe,EACnC,KAAK,EACL,IAAKC,GAAQ,GAAG,KAAK,UAAUA,CAAG,CAAC,IAAIF,EAAWC,EAAkCC,CAAG,CAAC,CAAC,EAAE,EAC3F,KAAK,GAAG,CAAC,GACd,CAEO,SAASC,EAAgBF,EAAwB,CACtD,OAAOD,EAAUC,CAAK,CACxB,CAGO,SAASG,EACdC,EACAC,EACmB,CACnB,IAAMC,EAAY,SAAUD,EAAQA,EAAM,KAAO,OAC3CE,EAAc,SAAUH,EAAUA,EAAQ,KAAO,OACjDI,EAAOT,EAAUO,CAAS,IAAMP,EAAUQ,CAAW,EAAIA,EAAcD,EACvEG,EAAO,SAAUJ,EAAQA,EAAM,KAAO,GACtCK,EAAQ,CACZ,GAAI,UAAWL,GAASA,EAAM,QAAU,OAAY,CAAE,MAAOA,EAAM,KAAM,EAAI,CAAC,EAC9E,GAAI,aAAcA,GAASA,EAAM,WAAa,OAAY,CAAE,SAAUA,EAAM,QAAS,EAAI,CAAC,EAC1F,GAAI,aAAcA,GAASA,EAAM,WAAa,OAAY,CAAE,SAAUA,EAAM,QAAS,EAAI,CAAC,EAC1F,GAAI,cAAeA,GAASA,EAAM,YAAc,OAAY,CAAE,UAAWA,EAAM,SAAU,EAAI,CAAC,EAC9F,GAAI,cAAeA,GAASA,EAAM,YAAc,OAAY,CAAE,UAAWA,EAAM,SAAU,EAAI,CAAC,CAChG,EACIM,EACJ,OAAQN,EAAM,MAAO,CACnB,IAAK,WACHM,EAAO,CAAE,MAAO,WAAY,KAAAF,EAAM,KAAMD,EAAgB,IAAKH,EAAM,IAAK,KAAMA,EAAM,KAAM,GAAGK,CAAM,EACnG,MACF,IAAK,SACHC,EAAO,CAAE,MAAO,SAAU,KAAAF,EAAM,KAAMD,EAAgB,IAAKH,EAAM,IAAK,KAAMA,EAAM,KAAM,GAAGK,CAAM,EACjG,MACF,IAAK,UACHC,EAAOH,IAAS,OACZ,CAAE,MAAO,UAAW,KAAAC,EAAM,IAAKJ,EAAM,IAAK,OAAQA,EAAM,OAAQ,GAAGK,CAAM,EACzE,CAAE,MAAO,UAAW,KAAAD,EAAM,KAAMD,EAAgB,IAAKH,EAAM,IAAK,OAAQA,EAAM,OAAQ,GAAGK,CAAM,EACnG,MACF,IAAK,SACHC,EAAO,CAAE,MAAO,SAAU,KAAAF,EAAM,KAAMJ,EAAM,KAAM,IAAKA,EAAM,IAAK,GAAGK,CAAM,EAC3E,MACF,IAAK,YACHC,EAAO,CAAE,MAAO,YAAa,KAAM,EAAM,EACzC,MACF,IAAK,UAAW,CACd,IAAMC,EAA2B,CAC/B,KAAMP,EAAM,KACZ,OAAQA,EAAM,OACd,QAASA,EAAM,QACf,MAAOA,EAAM,KACf,EACIA,EAAM,OAAS,SAAWO,EAAQ,KAAOP,EAAM,MAC/CA,EAAM,OAAS,SAAWO,EAAQ,KAAOP,EAAM,MAC/CA,EAAM,UAAY,SAAWO,EAAQ,QAAUP,EAAM,SACzDM,EAAO,CAAE,MAAO,UAAW,KAAM,GAAO,QAAAC,CAAQ,EAChD,KACF,CACA,IAAK,QACHD,EAAOH,IAAS,OACZ,CAAE,MAAO,QAAS,KAAM,GAAO,GAAIH,EAAM,MAAQ,OAAY,CAAC,EAAI,CAAE,IAAKA,EAAM,GAAI,EAAI,MAAOA,EAAM,KAAM,EAC1G,CACE,MAAO,QACP,KAAM,GACN,KAAMG,EACN,GAAIH,EAAM,MAAQ,OAAY,CAAC,EAAI,CAAE,IAAKA,EAAM,GAAI,EACpD,MAAOA,EAAM,KACf,EACJ,KACJ,CACA,OAAON,EAAUK,CAAO,IAAML,EAAUY,CAAI,EAAIP,EAAUO,CAC5D,CLpYA,IAAME,EAAUC,EAAoC,IAAI,EAcxD,SAASC,EAAYC,EAAwCC,EAAkD,CAC7G,OAAID,IAASC,EAAc,GACvB,CAACD,GAAQ,CAACC,EAAc,GACrBD,EAAK,QAAUC,EAAM,OACvBD,EAAK,YAAcC,EAAM,WACzBD,EAAK,gBAAkBC,EAAM,aACpC,CAGA,SAASC,EAAWC,EAAuDC,EAAqD,CAC9H,MAAO,CACL,MAAOA,EAAQ,MACf,GAAIA,EAAQ,YAAc,OAAY,CAAC,EAAI,CAAE,UAAWA,EAAQ,SAAU,EAC1E,GAAIA,EAAQ,gBAAkB,OAAY,CAAC,EAAI,CAAE,cAAeA,EAAQ,aAAc,EACtF,YAAcC,GAAQF,EAAO,SAAS,cAAcE,CAAG,GAAK,CAAC,EAC7D,SAAU,CAACC,EAAOD,IAAQF,EAAO,SAAS,WAAWG,EAAOD,CAAG,GAAK,EACtE,CACF,CAGO,SAASE,GAAiB,CAAE,OAAQC,EAAU,IAAAC,EAAK,GAAAC,EAAI,MAAAC,EAAO,WAAAC,EAAY,MAAAC,EAAO,QAAAC,EAAS,SAAAC,CAAS,EAA8C,CACtJ,GAAIL,IAAO,SAAcC,IAAU,QAAaC,IAAe,QAC7D,MAAM,IAAI,UAAU,4EAA4E,EAOlG,IAAMI,EAAW,CAACR,EAAUC,EAAKC,EAAIC,EAAOC,EAAYC,CAAK,EACvDI,EAAgBC,EAAyCJ,CAAO,EACtEG,EAAc,QAAUH,EACxB,IAAMK,EAAOD,EAA4G,MAAS,EAClI,GAAI,CAACC,EAAK,SACLA,EAAK,QAAQ,SAAS,KAAK,CAACC,EAAMC,IAAUD,IAASJ,EAASK,CAAK,CAAC,GACpE,CAACtB,EAAYoB,EAAK,QAAQ,QAASL,CAAO,EAAG,CAChD,IAAMQ,EAASR,IAAY,OAAY,OAAYZ,EAAWe,EAAeH,CAAO,EACpFK,EAAK,QAAU,CACb,SAAAH,EACA,QAAAF,EACA,OAAQN,GAAYe,EAAab,IAAO,OAAY,CAAE,IAAKD,EAAM,GAAAC,EAAI,MAAAG,EAAO,QAASS,CAAO,EAAI,CAAE,IAAKb,EAAM,MAAAE,EAAO,WAAAC,EAAY,MAAAC,EAAO,QAASS,CAAO,CAAC,CAC1J,CACF,CACA,IAAME,EAASL,EAAK,QAAQ,OACtB,CAACM,EAAQC,CAAS,EAAIC,EAAS,CAAE,OAAAH,EAAQ,SAAUA,EAAO,QAAS,CAAC,EACpEI,EAAWH,EAAO,SAAWD,EAASC,EAAO,SAAWD,EAAO,SAC/DK,EAAeX,EAAqF,MAAS,EAEnH,OAAAY,EAAU,IAAM,CAGVD,EAAa,SAAS,SAAWL,IACnC,aAAaK,EAAa,QAAQ,KAAK,EACvCA,EAAa,QAAU,QAEzB,IAAIE,EAAS,GACPC,EAAcR,EAAO,gBAAiBI,GAAa,CAAMG,GAAQL,EAAU,CAAE,OAAAF,EAAQ,SAAAI,CAAS,CAAC,CAAG,CAAC,EACzG,OAAAF,EAAU,CAAE,OAAAF,EAAQ,SAAUA,EAAO,QAAS,CAAC,EAC1CA,EAAO,SAAS,EAAE,MAAM,IAAG,EAAY,EACrC,IAAM,CACXO,EAAS,GACTC,EAAY,EAEPxB,IAAUqB,EAAa,QAAU,CAAE,OAAAL,EAAQ,MAAO,WAAW,IAAMA,EAAO,MAAM,EAAG,CAAC,CAAE,EAC7F,CACF,EAAG,CAACA,EAAQhB,CAAQ,CAAC,EAEdyB,EAAA,cAACpC,EAAQ,SAAR,CAAiB,MAAO,CAAE,OAAA2B,EAAQ,SAAAI,CAAS,GAAIb,CAAS,CAClE,CAEA,SAASmB,GAA0B,CACjC,IAAMC,EAAQC,EAAWvC,CAAO,EAChC,GAAI,CAACsC,EAAO,MAAM,IAAI,MAAM,2CAA2C,EACvE,OAAOA,CACT,CAGO,SAASE,IAAoC,CAClD,OAAOH,EAAS,EAAE,MACpB,CAEO,SAASI,IAAsC,CACpD,OAAOJ,EAAS,EAAE,QACpB,CAEO,SAASK,IAAiC,CAC/C,OAAOL,EAAS,EAAE,SAAS,UAC7B,CAOO,SAASM,GACdC,EACAC,EACAtC,EAA6C,CAAC,EACrB,CACzB,GAAM,CAAE,OAAAoB,EAAQ,SAAAI,CAAS,EAAIM,EAAS,EAChCS,EAAUC,EAAgBF,CAAI,EAC9BG,EAAaC,EAAQ,IAAMJ,EAAM,CAACC,CAAO,CAAC,EAC1CI,EAAO3C,EAAQ,OAAS,GAC9B,GAAI,CAAC2C,GAAQ3C,EAAQ,MAAO,MAAM,IAAI,UAAU,6CAA6C,EAC7F,IAAM4C,EAAYC,EAAcR,CAAG,EAC7BzB,EAAW8B,EACf,KAAO,CAAE,OAAAtB,EAAQ,IAAK,GAAGI,EAAS,cAAc,IAAIoB,CAAS,IAAIL,CAAO,IAAII,CAAI,IAAI3C,EAAQ,QAAU,EAAI,EAAG,GAC7G,CAACoB,EAAQI,EAAS,eAAgBoB,EAAWL,EAASI,EAAM3C,EAAQ,KAAK,CAC3E,EACM,CAAC8C,EAASC,CAAQ,EAAIC,EAC1B,CACEC,EACAC,KACI,CACJ,SAAUA,EAAO,SACjB,KAAMC,EACJF,EAAQ,WAAaC,EAAO,SAAWD,EAAQ,KAAOG,EAA0B,EAChFF,EAAO,KACT,CACF,GACA,CAAE,SAAAtC,EAAU,KAAMwC,EAA0B,CAAE,CAChD,EACMC,EAAQP,EAAQ,WAAalC,EAAWkC,EAAQ,KAAOM,EAA0B,EAEvF,OAAA1B,EAAU,IAAM,CACd,IAAIC,EAAS,GACb,GAAIgB,EAAM,CACR,IAAMW,EAAelC,EAAO,UAAUiB,EAAKI,EAAac,GAAa,CAC/D5B,GAAQoB,EAAS,CAAE,SAAAnC,EAAU,MAAO2C,CAAS,CAAC,CACpD,EAAG,CAAE,MAAOvD,EAAQ,KAAM,CAAC,EAC3B,MAAO,IAAM,CACX2B,EAAS,GACT2B,EAAa,MAAM,CACrB,CACF,CACA,IAAIE,EACJ,OAAM,SAAY,CAChB,IAAIC,EAAU,IACd,KAAO9B,GACL,GAAI,CACF,IAAM+B,EAAS,MAAMtC,EAAO,MAAMiB,EAAKI,CAAU,EAC7Cd,GAAQoB,EAAS,CAAE,SAAAnC,EAAU,MAAO8C,CAAO,CAAC,EAChD,MACF,OAASC,EAAO,CAEd,GADI,CAAChC,GACDiC,EAAkBD,CAAK,GAAKA,EAAM,OAAS,oBAAqB,OACpE,GAAIC,EAAkBD,CAAK,GAAKA,EAAM,UAAW,CAC/C,MAAM,IAAI,QAAeE,GAAY,CAAEL,EAAQ,WAAWK,EAASJ,CAAO,CAAG,CAAC,EAC9EA,EAAU,KAAK,IAAIA,EAAU,EAAG,GAAK,EACrC,QACF,CACA,IAAMK,EAAWF,EAAkBD,CAAK,EACpCA,EAAM,SACN,CAAE,KAAM,WAAY,OAAQ,QAAS,QAAS,OAAOA,CAAK,EAAG,MAAO,eAAgB,EACxFZ,EAAS,CAAE,SAAAnC,EAAU,MAAO,CAAE,MAAO,UAAW,KAAM,GAAO,GAAGkD,CAAS,CAAE,CAAC,EAC5E,MACF,CAEJ,GAAG,EACI,IAAM,CACXnC,EAAS,GACL6B,IAAU,QAAW,aAAaA,CAAK,CAC7C,CACF,EAAG,CAAC5C,EAAU6B,CAAU,CAAC,EAElBC,EAAQ,KAAO,CAAE,GAAGW,EAAO,WAAY7B,EAAS,UAAW,GAAI,CAAC6B,EAAO7B,EAAS,UAAU,CAAC,CACpG,CAEO,SAASuC,GACd1B,EACAC,EACyB,CACzB,OAAOF,GAASC,EAAKC,CAAI,CAC3B,CAgCA,SAAS0B,EAAeC,EAAoC,CAC1D,GAAM,CAAE,OAAA7C,EAAQ,SAAAI,CAAS,EAAIM,EAAS,EAChCc,EAAYqB,IAAO,OAAY,OAAYpB,EAAcoB,CAAE,EAC3DrD,EAAW8B,EAAQ,KAAO,CAAC,GAAI,CAACtB,EAAQI,EAAS,eAAgBoB,CAAS,CAAC,EAC3E,CAACS,EAAOa,CAAQ,EAAI3C,EAAkG,CAAE,QAAS,CAAC,EAAG,MAAO,GAAO,MAAO,QAAS,CAAC,EAC1KG,EAAU,IAAM,CACd,IAAIC,EAAS,GACTwC,EAAQ,EACNvC,EAAcR,EAAO,OAAO,UAAWgD,GAAY,CACvD,IAAMnB,EAAU,EAAEkB,EACZE,EAAU,CAACC,EAAgBC,IAA6B,CACxD5C,GAAUsB,IAAYkB,GAAOD,EAAS,CAAE,SAAAtD,EAAU,QAAAwD,EAAS,MAAAE,EAAO,MAAAC,CAAM,CAAC,CAC/E,EACK,QAAQ,IAAI,CAACnD,EAAO,OAAO,MAAM6C,CAAE,EAAG7C,EAAO,OAAO,MAAM,CAAC,CAAC,EAC9D,KAAK,CAAC,CAACkD,EAAOC,CAAK,IAAMF,EAAQC,EAAOC,CAAK,EAAG,IAAMF,EAAQ,GAAO,MAAM,CAAC,CACjF,CAAC,EACD,MAAO,IAAM,CAAE1C,EAAS,GAAOC,EAAY,CAAG,CAChD,EAAG,CAAChB,CAAQ,CAAC,EACb,IAAM4D,EAAUC,EAAaC,GAAmBtD,EAAO,OAAO,QAAQsD,CAAM,EAAG,CAACtD,CAAM,CAAC,EACvF,OAAOsB,EAAQ,KAAO,CAAE,QAASW,EAAM,WAAazC,EAAWyC,EAAM,QAAU,CAAC,EAC9E,MAAOA,EAAM,WAAazC,GAAYyC,EAAM,MAC5C,MAAOA,EAAM,WAAazC,EAAWyC,EAAM,MAAQ,SAAU,QAAAmB,CAAQ,GAAI,CAACnB,EAAOzC,EAAU4D,CAAO,CAAC,CACvG,CAEO,SAASG,IAA6B,CAC3C,OAAOX,EAAe,CACxB,CAEO,SAASY,GAA0BvC,EAAwD,CAChG,GAAM,CAAE,OAAAjB,EAAQ,SAAAI,CAAS,EAAIM,EAAS,EAChCc,EAAYC,EAAcR,CAAG,EAC7BzB,EAAW8B,EAAQ,KAAO,CAAE,OAAAtB,EAAQ,WAAYI,EAAS,eAAgB,UAAAoB,CAAU,GAAI,CAACxB,EAAQI,EAAS,eAAgBoB,CAAS,CAAC,EACnIiC,EAAiB/D,EAAOF,CAAQ,EACtCiE,EAAe,QAAUjE,EACzB,GAAM,CAAE,QAAAwD,EAAS,MAAAE,CAAM,EAAIN,EAAe3B,CAAG,EACvCyC,EAAUpC,EACd,IAAM0B,EAAQ,OAAQW,GAAUA,EAAM,KAAOnC,GAAamC,EAAM,QAAU,QAAQ,EAClF,CAACX,EAASxB,CAAS,CACrB,EACM,CAACE,EAASkC,CAAU,EAAIzD,EAK3B,CAAE,SAAAX,EAAU,QAAS,CAAE,CAAC,EACrBqE,EAAUnC,EAAQ,WAAalC,EACjCkC,EACA,CAAE,SAAAlC,EAAU,QAAS,CAAE,EACrBsE,EAAMT,EAAY,MAAOnC,GAA2B,CACxD,IAAMX,EAAS,IAAMkD,EAAe,UAAYjE,GAAYQ,EAAO,SAAS,iBAAmBR,EAAS,WACxG,GAAI,CAACe,EAAO,EAAG,OAAOP,EAAO,SAASiB,EAAKC,CAAI,EAC/C0C,EAAY/B,GAAYA,EAAQ,WAAarC,EACzC,CAAE,GAAGqC,EAAS,QAASA,EAAQ,QAAU,CAAE,EAC3C,CAAE,SAAArC,EAAU,QAAS,CAAE,CAAC,EAC5B,GAAI,CACF,IAAM8C,EAAS,MAAMtC,EAAO,SAASiB,EAAKC,CAAI,EAC9C,OAAIX,EAAO,GAAGqD,EAAY/B,IAAa,CACrC,SAAArC,EACA,QAASqC,EAAQ,WAAarC,EAAWqC,EAAQ,QAAU,EAC3D,KAAMS,CACR,EAAE,EACKA,CACT,OAASC,EAAO,CACd,MAAIhC,EAAO,GAAKiC,EAAkBD,CAAK,GAAKA,EAAM,WAChDqB,EAAY/B,GAAYA,EAAQ,WAAarC,EACzC,CAAE,GAAGqC,EAAS,UAAWU,CAAM,EAC/B,CAAE,SAAA/C,EAAU,QAAS,EAAG,UAAW+C,CAAM,CAAC,EAE1CA,CACR,QAAE,CACIhC,EAAO,GAAGqD,EAAY/B,GAAYA,EAAQ,WAAarC,EACvD,CAAE,GAAGqC,EAAS,QAAS,KAAK,IAAI,EAAGA,EAAQ,QAAU,CAAC,CAAE,EACxDA,CAAO,CACb,CACF,EAAG,CAACrC,CAAQ,CAAC,EACb,OAAO8B,EACL,KAAO,CAAE,IAAAwC,EAAK,SAAUD,EAAQ,QAAU,EAAG,QAAAH,EAAS,MAAAR,EAAO,KAAMW,EAAQ,KAAM,UAAWA,EAAQ,SAAU,GAC9G,CAACC,EAAKD,EAAQ,QAASH,EAASR,EAAOW,EAAQ,KAAMA,EAAQ,SAAS,CACxE,CACF,CAgBO,SAASE,IAAyB,CACvC,GAAM,CAAE,OAAA/D,EAAQ,SAAAI,CAAS,EAAIM,EAAS,EAChCsD,EAAetE,EAAO,CAAC,EACvBuE,EAAevE,EAAOM,CAAM,EAC9BiE,EAAa,UAAYjE,IAC3BiE,EAAa,QAAUjE,EACvBgE,EAAa,SAAW,GAG1B,GAAM,CAACtC,EAASkC,CAAU,EAAIzD,EAA2E,CACvG,OAAAH,EACA,WAAYI,EAAS,eACrB,MAAO,CAAE,MAAO,YAAa,MAAOA,EAAS,KAAM,CACrD,CAAC,EACK6B,EAAQP,EAAQ,SAAW1B,GAAU0B,EAAQ,aAAetB,EAAS,eACvEsB,EAAQ,MACR,CAAE,MAAO,YAAsB,MAAOtB,EAAS,KAAM,EACnD8D,EAAKb,EAAY,SAAY,CACjC,IAAMc,EAAa/D,EAAS,eACtBgE,EAAQ,EAAEJ,EAAa,QAC7B,GAAI,CACF,IAAMrD,EAAQ,MAAMX,EAAO,KAAK,GAAG,EACnC,OAAIoE,IAAUJ,EAAa,SACtBC,EAAa,UAAYjE,GACzBA,EAAO,SAAS,iBAAmBmE,GACtCP,EAAW,CAAE,OAAA5D,EAAQ,WAAAmE,EAAY,MAAO,CAAE,MAAO,gBAAiB,GAAGxD,EAAO,MAAOX,EAAO,SAAS,KAAM,CAAE,CAAC,EAEvGW,CACT,OAAS4B,EAAO,CACd,GAAI6B,IAAUJ,EAAa,SACtBC,EAAa,UAAYjE,GACzBA,EAAO,SAAS,iBAAmBmE,GACtC,GAAI3B,EAAkBD,CAAK,IACrBA,EAAM,OAAS,UAAYA,EAAM,OAAS,mBAAqBA,EAAM,OAAS,kBAClFqB,EAAW,CAAE,OAAA5D,EAAQ,WAAAmE,EAAY,MAAO,CAAE,MAAO,YAAa,QAAS5B,EAAM,QAAS,CAAE,CAAC,UAChF,CAACC,EAAkBD,CAAK,GAAKA,EAAM,OAAS,oBAAqB,CAC1E,IAAM8B,EAAU7B,EAAkBD,CAAK,EAAIA,EAAM,SAAW,OAC5DqB,EAAW,CAAE,OAAA5D,EAAQ,WAAAmE,EAAY,MAAO,CAAE,MAAO,UAAW,QAAAE,EAAS,MAAOrE,EAAO,SAAS,KAAM,CAAE,CAAC,CACvG,EAEF,MAAMuC,CACR,CACF,EAAG,CAACvC,EAAQI,EAAS,cAAc,CAAC,EAEpCE,EAAU,KACH4D,EAAG,EAAE,MAAM,IAAG,EAAY,EACxB,IAAM,CACXF,EAAa,SAAW,CAC1B,GACC,CAACE,CAAE,CAAC,EAEP,IAAMI,EAAYjB,EAAY,MAAOvB,GAC5BA,EAAO,EACb,CAAC,CAAC,EAEL,MAAO,CACL,GAAGG,EACH,MAAO7B,EAAS,MAChB,OAASmE,GAAUD,EAAU,IAAMtE,EAAO,KAAK,OAAOuE,CAAK,CAAC,EAC5D,MAAQA,GAAUD,EAAU,IAAMtE,EAAO,KAAK,MAAMuE,CAAK,CAAC,EAC1D,MAAO,IAAMD,EAAU,IAAMtE,EAAO,KAAK,MAAM,CAAC,EAChD,OAAQ,SAAY,CAClBgE,EAAa,SAAW,EACxB,IAAM1B,EAAS,MAAMtC,EAAO,KAAK,OAAO,EACxC,GAAIiE,EAAa,UAAYjE,EAAQ,CACnCgE,EAAa,SAAW,EACxB,IAAMG,EAAanE,EAAO,SAAS,eACnC4D,EAAW,CAAE,OAAA5D,EAAQ,WAAAmE,EAAY,MAAO,CAAE,MAAO,WAAY,CAAE,CAAC,CAClE,CACA,OAAO7B,CACT,EACA,GAAA4B,CACF,CACF,CAEO,SAASM,GAAoCvD,EAAgCC,EAA6C,CAC/H,GAAM,CAAE,OAAAlB,EAAQ,SAAAI,CAAS,EAAIM,EAAS,EAChCS,EAAUC,EAAgBF,CAAI,EACpC,OAAOI,EACL,IAAMtB,EAAO,QAAuBiB,EAAKC,CAAI,EAC7C,CAAClB,EAAQI,EAAS,eAAgBqB,EAAcR,CAAG,EAAGE,CAAO,CAC/D,CACF,CAYO,SAASsD,GACdC,EACA9F,EAA2B,CAAC,EACb,CACf,GAAM,CAAE,OAAAoB,EAAQ,SAAAI,CAAS,EAAIM,EAAS,EAGhCiE,EAAMvD,EAAgB,CAC1B,OAAQ,MAAM,QAAQsD,CAAW,EAC7BA,EAAY,IAAKE,GAAUA,EAAM,EAAE,EACnCF,GAAe,KAAO,KAAQA,EAA4B,GAC9D,MAAO9F,EAAQ,MACf,QAASA,EAAQ,UAAY,GAC7B,SAAUA,EAAQ,WAAa,GAC/B,eAAgBwB,EAAS,cAC3B,CAAC,EACKyE,EAAcvD,EAAQ,IAAMoD,EAAa,CAACC,CAAG,CAAC,EAC9CnF,EAAW8B,EAAQ,KAAO,CAAE,OAAAtB,EAAQ,IAAA2E,CAAI,GAAI,CAAC3E,EAAQ2E,CAAG,CAAC,EACzD,CAACG,EAAUC,CAAW,EAAI5E,EAA+D,CAAE,SAAAX,CAAS,CAAC,EACrGmB,EAAQmE,EAAS,WAAatF,EAAWsF,EAAS,MAAQ,OAChE,OAAAxE,EAAU,IAAM,CACd,IAAIC,EAAS,GACPyE,EAAoB,CAAC,EACrBC,EAA+B,MAAM,QAAQJ,CAAW,EAC1DA,EACAA,GAAe,KAAO,CAAC,EAAI,CAACA,CAA0B,EAI1D,OAAM,SAAY,CAChB,MAAM7E,EAAO,MACb,IAAMkF,EAAQlF,EAAO,WACfmF,EAAa,OAAO,IAAI,iBAAoB,WAC5CxF,EAAO,MAAM,QAAQ,IAAIsF,EAAK,IAAKL,GAAUO,EAAaD,GAAO,IAAIN,EAAM,GAAIhG,EAAQ,OAAS,IAAI,EAAI,MAAS,CAAC,EAClHwG,EAAUH,EAAK,OAAO,CAACI,EAAQxF,IAAUF,EAAKE,CAAK,IAAM,MAAS,EAClEyF,EAAS,MAAMC,EAAeL,EAAOE,EAAQ,IAAKR,GAAUA,EAAM,EAAE,CAAC,EACrEY,EAAWJ,EAAQ,OAAQR,GAAU,CAACU,EAAO,IAAIV,EAAM,EAAE,CAAC,EAE1Da,EAAS,MAAM,QAAQ,IAAID,EAAS,IAAKZ,GAAU5E,EAAO,SAAS4E,EAAOhG,CAAO,CAAC,CAAC,EACnF8G,EAAa,IAAI,IAAIF,EAAS,IAAI,CAACZ,EAAO/E,IAAU,CAAC+E,EAAM,GAAIa,EAAO5F,CAAK,CAAC,CAAC,CAAC,EAC9E8F,EAAOV,EAAK,IAAI,CAACL,EAAO/E,IAAU,CACtC,IAAM+F,EAAOjG,EAAKE,CAAK,EACvB,GAAI,CAAC+F,EAAM,OAAOF,EAAW,IAAId,EAAM,EAAE,EACzC,IAAM3F,EAAM,IAAI,gBAAgB2G,CAAI,EACpC,OAAAZ,EAAQ,KAAK/F,CAAG,EACTA,CACT,CAAC,EACD,GAAI,CAACsB,EAAQ,CACX,QAAWtB,KAAO+F,EAAS,IAAI,gBAAgB/F,CAAG,EAClD,MACF,CACA8F,EAAY,CAAE,SAAAvF,EAAU,MAAO,MAAM,QAAQqF,CAAW,EAAIc,EAAOA,EAAK,CAAC,CAAE,CAAC,CAC9E,GAAG,EAAE,MAAM,IAAG,EAAY,EACnB,IAAM,CACXpF,EAAS,GACT,QAAWtB,KAAO+F,EAAS,IAAI,gBAAgB/F,CAAG,CACpD,CACF,EAAG,CAACO,EAAUqF,EAAajG,EAAQ,MAAOA,EAAQ,QAASA,EAAQ,QAAQ,CAAC,EACrE+B,CACT",
6
+ "names": ["React", "createContext", "useCallback", "useContext", "useEffect", "useMemo", "useReducer", "useRef", "useState", "createClient", "isSnapbackRefusal", "snapbackRefusalBrand", "SnapbackRefusal", "envelope", "retryable", "status", "ASSET_ID", "viewedAssetIds", "cache", "ids", "viewed", "id", "ASSET_ID", "referencePath", "proxies", "proxy", "path", "op", "existing", "target", "value", "_current", "property", "referenceName", "ref", "api", "initialQueryState", "canonical", "value", "key", "stableSerialize", "reduceQueryState", "current", "event", "eventData", "currentData", "data", "live", "facts", "next", "refusal", "Context", "createContext", "sameOffline", "left", "right", "throughRef", "latest", "options", "row", "table", "SnapbackProvider", "supplied", "url", "as", "token", "tokenStore", "fetch", "offline", "children", "identity", "latestOffline", "useRef", "held", "item", "index", "stable", "createClient", "client", "status", "setStatus", "useState", "snapshot", "pendingClose", "useEffect", "active", "unsubscribe", "React", "provider", "value", "useContext", "useSnapbackClient", "useSnapbackSnapshot", "useConnection", "useQuery", "ref", "args", "argsKey", "stableSerialize", "stableArgs", "useMemo", "live", "operation", "referenceName", "machine", "dispatch", "useReducer", "current", "action", "reduceQueryState", "initialQueryState", "state", "subscription", "delivery", "timer", "backoff", "result", "error", "isSnapbackRefusal", "resolve", "envelope", "useSubscription", "useOutboxState", "op", "setState", "probe", "entries", "publish", "ready", "fence", "dismiss", "useCallback", "intent", "useOutbox", "useMutation", "activeIdentity", "pending", "entry", "setMachine", "visible", "run", "useAuth", "requestEpoch", "activeClient", "me", "generation", "epoch", "refusal", "establish", "input", "useChannel", "useAssetUrl", "assetOrList", "key", "asset", "stableAsset", "resolved", "setResolved", "created", "list", "cache", "objectUrls", "missing", "_asset", "viewed", "viewedAssetIds", "mintable", "minted", "mintedById", "urls", "blob"]
7
+ }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { type ImageProps } from "react-native";
3
+ import { type VideoViewProps } from "expo-video";
4
+ import { SnapbackRefusal } from "../client.js";
5
+ import type { ImageAsset, VideoAsset } from "../types.js";
6
+ export type AssetImageProps = Omit<ImageProps, "source"> & {
7
+ asset: ImageAsset;
8
+ width?: number;
9
+ height?: number;
10
+ onRefusal?: (refusal: SnapbackRefusal) => void;
11
+ onSourceChange?: (uri: string | undefined, settled: boolean) => void;
12
+ };
13
+ export declare function AssetImage({ asset, width, height, style, onLayout, onRefusal, onSourceChange, ...props }: AssetImageProps): React.ReactElement;
14
+ export type AssetVideoProps = Omit<VideoViewProps, "player"> & {
15
+ autoPlay?: boolean;
16
+ onPlaybackStart?: () => void;
17
+ asset: VideoAsset;
18
+ width?: number;
19
+ height?: number;
20
+ onRefusal?: (refusal: SnapbackRefusal) => void;
21
+ onSourceChange?: (uri: string | undefined, settled: boolean) => void;
22
+ };
23
+ export declare function AssetVideo({ asset, width, height, style, onRefusal, onSourceChange, onPlaybackStart, autoPlay, ...props }: AssetVideoProps): React.ReactElement;
@@ -0,0 +1,43 @@
1
+ import type { AssetCache, ByteDelta } from "../store/byte-cache.js";
2
+ import type { PersistedRangeState, RangeStore, RangeStoreBackend } from "../store/range-store.js";
3
+ import type { SnapbackClient } from "../types.js";
4
+ export interface NativeFileSystem {
5
+ readonly root: string;
6
+ list(directory: string): Promise<string[]>;
7
+ exists?(uri: string): Promise<boolean>;
8
+ write(uri: string, bytes: Uint8Array): Promise<void>;
9
+ remove(uri: string): Promise<void>;
10
+ }
11
+ export declare function attachAssetFiles(store: RangeStore, files: NativeAssetFiles): void;
12
+ export declare function nativeAssetFiles(client: SnapbackClient): NativeAssetFiles | undefined;
13
+ /** Encoded even for adversarial principal strings; no partition can escape its directory. */
14
+ export declare function partitionDirectory(root: string, partition: string): string;
15
+ export declare class NativeAssetFiles {
16
+ readonly fs: NativeFileSystem;
17
+ private partition?;
18
+ private state?;
19
+ private revision;
20
+ private custody;
21
+ private readonly assetRevisions;
22
+ private readonly listeners;
23
+ private readonly materialized;
24
+ private tail;
25
+ constructor(fs: NativeFileSystem);
26
+ getRevision: () => number;
27
+ getAssetRevision: (id: string) => string;
28
+ getCustody: () => number;
29
+ available: () => boolean;
30
+ subscribe: (listener: () => void) => (() => void);
31
+ private changed;
32
+ private enqueue;
33
+ private remove;
34
+ private path;
35
+ private eligible;
36
+ get(cache: AssetCache, assetId: string, width: number | null): Promise<string | undefined>;
37
+ /** Only backend success is a commit notification; never call this from beforeCommit. */
38
+ opened(partition: string, state: PersistedRangeState | undefined): Promise<void>;
39
+ replaced(state: PersistedRangeState, bytes: ByteDelta): Promise<void>;
40
+ wiped(partition: string): Promise<void>;
41
+ closed(): void;
42
+ }
43
+ export declare function fileBackend(backend: RangeStoreBackend, files: NativeAssetFiles): RangeStoreBackend;
@@ -0,0 +1,16 @@
1
+ import { SnapbackRefusal } from "../client.js";
2
+ import type { AssetRecord } from "../types.js";
3
+ import type { AssetBytes } from "../store/byte-cache.js";
4
+ export declare function bytesDataUri(value: AssetBytes): string;
5
+ /** Cached ordinary media only. View-once is held by its viewing component, never a file. */
6
+ export declare function useAssetFile(asset: AssetRecord | null | undefined, options?: {
7
+ width?: number;
8
+ }): string | undefined;
9
+ export declare function useNativeSource(asset: AssetRecord | null | undefined, options?: {
10
+ width?: number;
11
+ enabled?: boolean;
12
+ }): {
13
+ settled: boolean;
14
+ uri: string | undefined;
15
+ refusal: SnapbackRefusal | undefined;
16
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./react-core.js";
2
+ export { AssetImage, AssetVideo } from "./react-native/components.js";
3
+ export { useAssetFile } from "./react-native/source.js";
4
+ export type { AssetImageProps, AssetVideoProps } from "./react-native/components.js";
@@ -0,0 +1,2 @@
1
+ export*from"snapback2/react-core";import w,{useEffect as S,useMemo as L,useRef as N,useState as _}from"react";import{Image as W,PixelRatio as Y,View as J}from"react-native";import{VideoView as H,useVideoPlayer as Q}from"expo-video";import{useSnapbackClient as X,useSnapbackSnapshot as Z}from"snapback2/react-core";import{SnapbackRefusal as $}from"snapback2/client";import{useEffect as v,useMemo as b,useRef as B,useState as I,useSyncExternalStore as M}from"react";import{useSnapbackClient as V,useSnapbackSnapshot as F}from"snapback2/react-core";import{isSnapbackRefusal as D,SnapbackRefusal as U}from"snapback2/client";var z=Symbol.for("snapback2.native.assetFiles");function E(e){return e.assetCache?.store?.[z]}var O=()=>"0",q=()=>()=>{},j=e=>D(e)?e:new U({code:"E_OFFLINE_CARRIER",family:"offline",message:String(e),site:"useAssetFile",rewrite:"open a writable Expo cache directory and retry",guide:"offline"});function G(e){let n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r=[];for(let d=0;d<e.bytes.length;d+=3){let t=e.bytes[d],f=e.bytes[d+1],u=e.bytes[d+2],i=t<<16|(f??0)<<8|(u??0);r.push(n[i>>>18],n[i>>>12&63],f===void 0?"=":n[i>>>6&63],u===void 0?"=":n[i&63])}return`data:${e.type.replace(/[;,\r\n]/g,"")};base64,${r.join("")}`}function T(e,n){let r=V(),d=F(),t=E(r),f=b(()=>t&&e?()=>t.getAssetRevision(e.id):O,[t,e?.id]),u=M(t?.subscribe??q,f,O),i=b(()=>({client:r,id:e?.id,width:n,auth:d.authGeneration,files:t,revision:u}),[r,e?.id,n,d.authGeneration,t,u]),[s,o]=I();return v(()=>{let y=!0;if(!e||!t||!r.assetCache){o({identity:i});return}return t.get(r.assetCache,e.id,n??null).then(a=>{y&&o({identity:i,uri:a})},a=>{y&&o({identity:i,error:a})}),()=>{y=!1}},[i]),{files:t,revision:u,custody:t?.getCustody()??0,checked:s?.identity===i,uri:s?.identity===i?s.uri:void 0,error:s?.identity===i?s.error:void 0}}function K(e,n={}){return T(e,n.width).uri}function k(e,n={}){let r=V(),d=F(),t=T(e,n.width),f=b(()=>t.error===void 0?void 0:j(t.error),[t.error]),u=B({}).current,i=b(()=>({client:r,id:e?.id,auth:d.authGeneration,custody:t.custody}),[r,e?.id,d.authGeneration,t.custody]),[s,o]=I(),y=B(void 0),a=B(i);return a.current=i,v(()=>(a.current=i,()=>{a.current=void 0}),[i]),v(()=>{if(!e||n.enabled===!1||!t.checked||f||t.uri||y.current?.identity===i&&(y.current.width===n.width||s?.identity!==i)||t.files&&!t.files.available())return;y.current={identity:i,width:n.width};let c=()=>a.current===i&&r.snapshot.authGeneration===i.auth&&(t.files?.getCustody()??0)===i.custody;(async()=>{if(await r.ready,!c())return;if(await r.assetCache?.viewed(e.id)){c()&&s?.identity!==i&&o({identity:i,revision:t.files?.getAssetRevision(e.id)??"0"});return}if(!c())return;let l=await r.assetUrl(e,{width:n.width});if(!l||!c())return;let h=await r.assetBytes(l,{viewing:u});c()&&(r.assetCache&&!await r.assetCache.canServe(e.id,u)||c()&&o({identity:i,revision:t.files?.getAssetRevision(e.id)??"0",uri:G(h)}))})().catch(l=>{if(a.current!==i)return;let h=j(l);o({identity:i,revision:t.files?.getAssetRevision(e.id)??"0",refusal:h})})},[i,s,t.checked,t.uri,n.width,n.enabled,f]),v(()=>{if(!e||!t.files||s?.identity!==i||!s.uri||s.revision===t.revision)return;let c=!0,l=t.custody;return r.assetCache?.canServe(e.id,u).then(h=>{c&&a.current===i&&t.files?.getCustody()===l&&o({...s,revision:t.revision,uri:h?s.uri:void 0})}).catch(()=>{c&&a.current===i&&o({...s,revision:t.revision,uri:void 0})}),()=>{c=!1}},[s,i,t.revision]),{settled:t.checked&&(!!t.uri||s?.identity===i),uri:t.uri??(s?.identity===i&&s.revision===t.revision?s.uri:void 0),refusal:f??(s?.identity===i?s.refusal:void 0)}}function ee({asset:e,width:n,height:r,style:d,onLayout:t,onRefusal:f,onSourceChange:u,...i}){let[s,o]=_(),y=Math.max(1,Math.ceil((s??n??e.width)*Y.get())),{uri:a,refusal:c,settled:l}=k(e,{width:y}),h=N(f);return h.current=f,w.useLayoutEffect(()=>{u?.(a,l)},[a,l,u]),S(()=>{c&&h.current?.(c)},[c]),w.createElement(W,{...i,onLayout:p=>{p.nativeEvent.layout.width>0&&o(p.nativeEvent.layout.width),t?.(p)},source:a?{uri:a}:void 0,style:[{width:n??e.width,height:r??(n===void 0?e.height:void 0),aspectRatio:e.width/e.height},d]})}function te({uri:e,asset:n,play:r,onPlay:d,onRefusal:t,onSourceChange:f,autoPlay:u,onPlaybackStart:i,...s}){let o=Q(e);return S(()=>{let y=o.addListener("statusChange",({status:c,error:l})=>{c==="error"&&t?.(new $({code:"E_ASSET_TYPE",family:"input",site:"AssetVideo",message:l?.message??"native video decoder refused the source",rewrite:"use a video source supported by the native player",guide:"assets/client"}))}),a=o.addListener("playingChange",({isPlaying:c})=>{c&&d()});return r&&o.play(),()=>{a.remove(),y.remove()}},[o,d,r]),w.createElement(H,{...s,player:o,nativeControls:s.nativeControls??!0})}function ie({asset:e,width:n,height:r,style:d,onRefusal:t,onSourceChange:f,onPlaybackStart:u,autoPlay:i=!1,...s}){let o=X(),y=Z(),a=L(()=>({client:o,id:e.id,auth:y.authGeneration}),[o,e.id,y.authGeneration]),[c,l]=_(),h=N(t);h.current=t,S(()=>{let A=!0;return(async()=>{await o.ready;let P=!!(await o.assetCache?.viewOnce(e.id)||await o.assetCache?.viewed(e.id));A&&l({identity:a,once:P})})().catch(P=>{A&&h.current?.(P)}),()=>{A=!1}},[a]);let g=c?.identity===a?c.once:void 0,p=L(()=>g?new $({code:"E_ASSET_TYPE",family:"input",site:"AssetVideo",message:"view-once video is not played on native in v1: the native player cannot open an in-memory source and a view-once clip never touches disk",rewrite:"send the clip as an ordinary attachment, or wait for the native view-once video ruling",guide:"assets/client"}):void 0,[g]),{uri:m,refusal:R,settled:C}=k(g===!1?e:null,{enabled:g===!1});w.useLayoutEffect(()=>{f?.(m,C)},[m,C,f]),S(()=>{(p??R)&&h.current?.(p??R)},[p,R]);let x=[{width:n??e.width,height:r??(n===void 0?e.height:void 0),aspectRatio:e.width/e.height},d];return m?w.createElement(te,{...s,asset:e,uri:m,onRefusal:t,play:i,onPlay:u??(()=>{}),style:x}):w.createElement(J,{testID:s.testID,style:x})}export{ee as AssetImage,ie as AssetVideo,K as useAssetFile};
2
+ //# sourceMappingURL=react-native.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/react-native.tsx", "../src/react-native/components.tsx", "../src/react-native/source.ts", "../src/react-native/files.ts"],
4
+ "sourcesContent": ["export * from \"./react-core.js\";\nexport { AssetImage, AssetVideo } from \"./react-native/components.js\";\nexport { useAssetFile } from \"./react-native/source.js\";\nexport type { AssetImageProps, AssetVideoProps } from \"./react-native/components.js\";\n", "import React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Image, PixelRatio, View, type ImageProps, type LayoutChangeEvent } from \"react-native\";\nimport { VideoView, useVideoPlayer, type VideoViewProps } from \"expo-video\";\nimport { useSnapbackClient, useSnapbackSnapshot } from \"../react-core.js\";\nimport { SnapbackRefusal } from \"../client.js\";\nimport type { ImageAsset, VideoAsset } from \"../types.js\";\nimport { useNativeSource } from \"./source.js\";\n\nexport type AssetImageProps = Omit<ImageProps, \"source\"> & {\n asset: ImageAsset; width?: number; height?: number;\n onRefusal?: (refusal: SnapbackRefusal) => void;\n onSourceChange?: (uri: string | undefined, settled: boolean) => void;\n};\nexport function AssetImage({ asset, width, height, style, onLayout, onRefusal, onSourceChange, ...props }: AssetImageProps): React.ReactElement {\n const [measured, setMeasured] = useState<number>();\n const requested = Math.max(1, Math.ceil((measured ?? width ?? asset.width) * PixelRatio.get()));\n const { uri, refusal, settled } = useNativeSource(asset, { width: requested });\n const callback = useRef(onRefusal);\n callback.current = onRefusal;\n React.useLayoutEffect(() => { onSourceChange?.(uri, settled); }, [uri, settled, onSourceChange]);\n useEffect(() => { if (refusal) callback.current?.(refusal); }, [refusal]);\n const layout = (event: LayoutChangeEvent) => {\n if (event.nativeEvent.layout.width > 0) setMeasured(event.nativeEvent.layout.width);\n onLayout?.(event);\n };\n return <Image {...props} onLayout={layout} source={uri ? { uri } : undefined}\n style={[{ width: width ?? asset.width, height: height ?? (width === undefined ? asset.height : undefined), aspectRatio: asset.width / asset.height }, style]} />;\n}\n\nexport type AssetVideoProps = Omit<VideoViewProps, \"player\"> & {\n autoPlay?: boolean;\n onPlaybackStart?: () => void;\n asset: VideoAsset; width?: number; height?: number;\n onRefusal?: (refusal: SnapbackRefusal) => void;\n onSourceChange?: (uri: string | undefined, settled: boolean) => void;\n};\nfunction Player({ uri, asset: _asset, play, onPlay, onRefusal, onSourceChange: _onSourceChange, autoPlay: _autoPlay, onPlaybackStart: _onPlaybackStart, ...props }: AssetVideoProps & { uri: string; play: boolean; onPlay(): void }) {\n const player = useVideoPlayer(uri);\n useEffect(() => {\n const errors = player.addListener(\"statusChange\", ({ status, error }) => {\n if (status === \"error\") onRefusal?.(new SnapbackRefusal({ code: \"E_ASSET_TYPE\", family: \"input\",\n site: \"AssetVideo\", message: error?.message ?? \"native video decoder refused the source\",\n rewrite: \"use a video source supported by the native player\", guide: \"assets/client\" }));\n });\n const subscription = player.addListener(\"playingChange\", ({ isPlaying }) => {\n if (isPlaying) onPlay();\n });\n if (play) player.play();\n return () => { subscription.remove(); errors.remove(); };\n }, [player, onPlay, play]);\n return <VideoView {...props} player={player} nativeControls={props.nativeControls ?? true} />;\n}\n\n// Native v1 refuses view-once clips before fetching: AVPlayer cannot open a\n// memory source, and these bytes must never be materialized as a file.\nexport function AssetVideo({ asset, width, height, style, onRefusal, onSourceChange, onPlaybackStart, autoPlay = false, ...props }: AssetVideoProps): React.ReactElement {\n const client = useSnapbackClient();\n const snapshot = useSnapbackSnapshot();\n const identity = useMemo(() => ({ client, id: asset.id, auth: snapshot.authGeneration }), [client, asset.id, snapshot.authGeneration]);\n const [policy, setPolicy] = useState<{ identity: typeof identity; once: boolean }>();\n const callback = useRef(onRefusal);\n callback.current = onRefusal;\n useEffect(() => {\n let active = true;\n void (async () => {\n await client.ready;\n const once = Boolean(await client.assetCache?.viewOnce(asset.id) || await client.assetCache?.viewed(asset.id));\n if (active) setPolicy({ identity, once });\n })().catch((error) => { if (active) callback.current?.(error); });\n return () => { active = false; };\n }, [identity]);\n const once = policy?.identity === identity ? policy.once : undefined;\n const refused = useMemo(() => once ? new SnapbackRefusal({ code: \"E_ASSET_TYPE\", family: \"input\", site: \"AssetVideo\",\n message: \"view-once video is not played on native in v1: the native player cannot open an in-memory source and a view-once clip never touches disk\",\n rewrite: \"send the clip as an ordinary attachment, or wait for the native view-once video ruling\", guide: \"assets/client\" }) : undefined, [once]);\n const { uri, refusal, settled } = useNativeSource(once === false ? asset : null, { enabled: once === false });\n React.useLayoutEffect(() => { onSourceChange?.(uri, settled); }, [uri, settled, onSourceChange]);\n useEffect(() => { if (refused ?? refusal) callback.current?.((refused ?? refusal)!); }, [refused, refusal]);\n const size = [{ width: width ?? asset.width, height: height ?? (width === undefined ? asset.height : undefined), aspectRatio: asset.width / asset.height }, style];\n if (uri) return <Player {...props} asset={asset} uri={uri} onRefusal={onRefusal} play={autoPlay} onPlay={onPlaybackStart ?? (() => {})} style={size} />;\n return <View testID={props.testID} style={size} />;\n}\n", "import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport { useSnapbackClient, useSnapbackSnapshot } from \"../react-core.js\";\nimport { isSnapbackRefusal, SnapbackRefusal } from \"../client.js\";\nimport type { AssetRecord } from \"../types.js\";\nimport type { AssetBytes } from \"../store/byte-cache.js\";\nimport { nativeAssetFiles } from \"./files.js\";\n\nconst none = () => \"0\";\nconst noSubscription = () => () => {};\nconst fileRefusal = (error: unknown): SnapbackRefusal => isSnapbackRefusal(error) ? error : new SnapbackRefusal({\n code: \"E_OFFLINE_CARRIER\", family: \"offline\", message: String(error), site: \"useAssetFile\",\n rewrite: \"open a writable Expo cache directory and retry\", guide: \"offline\",\n});\nexport function bytesDataUri(value: AssetBytes): string {\n // No Buffer, Blob, or huge argument list on Hermes.\n const alphabet = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n const output: string[] = [];\n for (let i = 0; i < value.bytes.length; i += 3) {\n const a = value.bytes[i], b = value.bytes[i + 1], c = value.bytes[i + 2];\n const n = (a << 16) | ((b ?? 0) << 8) | (c ?? 0);\n output.push(alphabet[n >>> 18], alphabet[(n >>> 12) & 63], b === undefined ? \"=\" : alphabet[(n >>> 6) & 63], c === undefined ? \"=\" : alphabet[n & 63]);\n }\n return `data:${value.type.replace(/[;,\\r\\n]/g, \"\")};base64,${output.join(\"\")}`;\n}\n\nfunction useFile(asset: AssetRecord | null | undefined, width?: number) {\n const client = useSnapbackClient();\n const snapshot = useSnapbackSnapshot();\n const files = nativeAssetFiles(client);\n const getRevision = useMemo(() => files && asset ? () => files.getAssetRevision(asset.id) : none, [files, asset?.id]);\n const revision = useSyncExternalStore(files?.subscribe ?? noSubscription, getRevision, none);\n const identity = useMemo(() => ({ client, id: asset?.id, width, auth: snapshot.authGeneration, files, revision }),\n [client, asset?.id, width, snapshot.authGeneration, files, revision]);\n const [result, setResult] = useState<{ identity: typeof identity; uri?: string; error?: unknown }>();\n useEffect(() => {\n let active = true;\n if (!asset || !files || !client.assetCache) {\n setResult({ identity });\n return;\n }\n void files.get(client.assetCache, asset.id, width ?? null).then(\n (uri) => { if (active) setResult({ identity, uri }); },\n (error) => { if (active) setResult({ identity, error }); },\n );\n return () => { active = false; };\n }, [identity]);\n return { files, revision, custody: files?.getCustody() ?? 0, checked: result?.identity === identity,\n uri: result?.identity === identity ? result.uri : undefined, error: result?.identity === identity ? result.error : undefined };\n}\n\n/** Cached ordinary media only. View-once is held by its viewing component, never a file. */\nexport function useAssetFile(asset: AssetRecord | null | undefined, options: { width?: number } = {}): string | undefined {\n return useFile(asset, options.width).uri;\n}\n\nexport function useNativeSource(asset: AssetRecord | null | undefined, options: { width?: number; enabled?: boolean } = {}) {\n const client = useSnapbackClient();\n const snapshot = useSnapbackSnapshot();\n const file = useFile(asset, options.width);\n const failure = useMemo(() => file.error === undefined ? undefined : fileRefusal(file.error), [file.error]);\n const viewing = useRef({}).current;\n // Layout selects a derivative; it never starts another viewing session.\n const identity = useMemo(() => ({ client, id: asset?.id, auth: snapshot.authGeneration, custody: file.custody }),\n [client, asset?.id, snapshot.authGeneration, file.custody]);\n const [memory, setMemory] = useState<{ identity: typeof identity; revision?: string; uri?: string; refusal?: SnapbackRefusal }>();\n const attempted = useRef<{ identity: typeof identity; width?: number } | undefined>(undefined);\n const current = useRef<typeof identity | undefined>(identity);\n current.current = identity;\n useEffect(() => {\n current.current = identity;\n return () => { current.current = undefined; };\n }, [identity]);\n useEffect(() => {\n if (!asset || options.enabled === false || !file.checked || failure || file.uri || (attempted.current?.identity === identity && (attempted.current.width === options.width || memory?.identity !== identity)) || (file.files && !file.files.available())) return;\n attempted.current = { identity, width: options.width };\n const active = () => current.current === identity && client.snapshot.authGeneration === identity.auth\n && (file.files?.getCustody() ?? 0) === identity.custody;\n void (async () => {\n await client.ready;\n if (!active()) return;\n // A new viewing instance cannot borrow another instance's allowance.\n if (await client.assetCache?.viewed(asset.id)) {\n if (active() && memory?.identity !== identity) setMemory({ identity, revision: file.files?.getAssetRevision(asset.id) ?? \"0\" });\n return;\n }\n if (!active()) return;\n const url = await client.assetUrl(asset, { width: options.width });\n if (!url || !active()) return;\n const value = await client.assetBytes(url, { viewing });\n if (!active()) return;\n if (client.assetCache && !await client.assetCache.canServe(asset.id, viewing)) return;\n if (active()) setMemory({ identity, revision: file.files?.getAssetRevision(asset.id) ?? \"0\", uri: bytesDataUri(value) });\n })().catch((error) => {\n if (current.current !== identity) return;\n const refusal = fileRefusal(error);\n setMemory({ identity, revision: file.files?.getAssetRevision(asset!.id) ?? \"0\", refusal });\n });\n }, [identity, memory, file.checked, file.uri, options.width, options.enabled, failure]);\n useEffect(() => {\n if (!asset || !file.files || memory?.identity !== identity || !memory.uri || memory.revision === file.revision) return;\n let active = true;\n const custody = file.custody;\n void client.assetCache?.canServe(asset.id, viewing).then((held) => {\n if (active && current.current === identity && file.files?.getCustody() === custody)\n setMemory({ ...memory, revision: file.revision, uri: held ? memory.uri : undefined });\n }).catch(() => {\n if (active && current.current === identity) setMemory({ ...memory, revision: file.revision, uri: undefined });\n });\n return () => { active = false; };\n }, [memory, identity, file.revision]);\n // Fetch completion is fenced by the client's authority and by identity at render.\n return { settled: file.checked && (Boolean(file.uri) || memory?.identity === identity), uri: file.uri ?? (memory?.identity === identity && memory.revision === file.revision ? memory.uri : undefined), refusal: failure ?? (memory?.identity === identity ? memory.refusal : undefined) };\n}\n", "// @ref LLP 1013#2-decisions \u2014 native files are derivatives; permission is always the cache's.\nimport type { AssetCache, ByteCache, ByteDelta } from \"../store/byte-cache.js\";\nimport { ASSET_ID, assetIdsIn, byteKey } from \"../store/byte-cache.js\";\nimport type { PersistedRangeState, RangeStore, RangeStoreBackend } from \"../store/range-store.js\";\nimport type { SnapbackClient } from \"../types.js\";\n\nexport interface NativeFileSystem {\n readonly root: string;\n list(directory: string): Promise<string[]>;\n exists?(uri: string): Promise<boolean>;\n write(uri: string, bytes: Uint8Array): Promise<void>;\n remove(uri: string): Promise<void>;\n}\nconst FILES = Symbol.for(\"snapback2.native.assetFiles\");\ntype FileStore = RangeStore & { [FILES]?: NativeAssetFiles };\n\nexport function attachAssetFiles(store: RangeStore, files: NativeAssetFiles): void {\n Object.defineProperty(store, FILES, { value: files });\n}\nexport function nativeAssetFiles(client: SnapbackClient): NativeAssetFiles | undefined {\n return ((client.assetCache as ByteCache | undefined)?.store as FileStore | undefined)?.[FILES];\n}\n\n/** Encoded even for adversarial principal strings; no partition can escape its directory. */\nexport function partitionDirectory(root: string, partition: string): string {\n return `${root.replace(/\\/$/, \"\")}/${encodeURIComponent(partition).replace(/\\./g, \"%2E\")}/`;\n}\n\nexport class NativeAssetFiles {\n private partition?: string;\n private state?: PersistedRangeState;\n private revision = 0;\n private custody = 0;\n private readonly assetRevisions = new Map<string, number>();\n private readonly listeners = new Set<() => void>();\n private readonly materialized = new Set<string>();\n // Serialize file writes and removals; a late write cannot resurrect an evicted file.\n private tail: Promise<unknown> = Promise.resolve();\n constructor(readonly fs: NativeFileSystem) {}\n getRevision = (): number => this.revision;\n getAssetRevision = (id: string): string => `${this.custody}:${this.assetRevisions.get(id) ?? 0}`;\n getCustody = (): number => this.custody;\n available = (): boolean => this.partition !== undefined && !this.state?.sealed;\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener);\n return () => { this.listeners.delete(listener); };\n };\n private changed(custody = false): void {\n this.revision++;\n if (custody) this.custody++;\n for (const listener of this.listeners) {\n try { listener(); } catch { /* Observers cannot undo a committed carrier swap. */ }\n }\n }\n private enqueue<T>(body: () => Promise<T>): Promise<T> {\n const result = this.tail.then(body);\n this.tail = result.catch(() => undefined);\n return result;\n }\n private remove(uri: string): Promise<void> {\n this.materialized.delete(uri);\n // A failed derivative delete must not turn an already committed SQLite swap\n // into a failed swap. It remains garbage, refused below and swept next open.\n return this.fs.remove(uri).catch(() => undefined);\n }\n private path(key: string, partition = this.partition): string | undefined {\n return partition === undefined ? undefined : partitionDirectory(this.fs.root, partition) + key;\n }\n private eligible(assetId: string, width: number | null): boolean {\n return this.partition !== undefined && !this.state?.sealed && this.state?.assets[byteKey(assetId, width)] !== undefined;\n }\n\n async get(cache: AssetCache, assetId: string, width: number | null): Promise<string | undefined> {\n if (!ASSET_ID.test(assetId)) return undefined;\n if (width !== null && (!Number.isSafeInteger(width) || width <= 0)) throw new TypeError(\"asset width must be a positive integer\");\n const revision = this.getAssetRevision(assetId);\n const uri = this.path(byteKey(assetId, width));\n if (!uri) return undefined;\n // The carrier read checks seal, lease, viewed set, payload existence and length.\n const once = await cache.viewOnce(assetId) || await cache.viewed(assetId);\n const held = once ? undefined : await cache.getBytes(assetId, width);\n return this.enqueue(async () => {\n // Recheck after acquiring the queue: viewed custody can move while a\n // preceding materialization holds it, even without a byte delta.\n if (!held || revision !== this.getAssetRevision(assetId) || !this.eligible(assetId, width)\n || await cache.viewOnce(assetId) || await cache.viewed(assetId)) {\n await this.remove(uri);\n return undefined;\n }\n if (!this.materialized.has(uri) || !await this.fs.exists?.(uri)) await this.fs.write(uri, held.bytes);\n if (revision !== this.getAssetRevision(assetId) || !this.eligible(assetId, width)\n || await cache.viewOnce(assetId) || await cache.viewed(assetId)) {\n await this.remove(uri);\n return undefined;\n }\n this.materialized.add(uri);\n return uri;\n });\n }\n\n /** Only backend success is a commit notification; never call this from beforeCommit. */\n async opened(partition: string, state: PersistedRangeState | undefined): Promise<void> {\n this.materialized.clear();\n this.partition = partition;\n this.state = state;\n this.changed(true);\n const directory = partitionDirectory(this.fs.root, partition);\n await this.enqueue(async () => {\n for (const name of await this.fs.list(directory)) {\n if (!state?.assets[name]) await this.remove(directory + name);\n }\n });\n }\n async replaced(state: PersistedRangeState, bytes: ByteDelta): Promise<void> {\n const sealed = this.state?.sealed !== state.sealed;\n const before = assetLeases(this.state), after = assetLeases(state);\n const changed = new Set([...before.keys(), ...after.keys()].filter((id) => before.get(id) !== after.get(id)));\n for (const key of [...bytes.remove, ...bytes.put.map((put) => put.key)]) changed.add(key.split(\".w\")[0]);\n for (const id of changed) this.assetRevisions.set(id, (this.assetRevisions.get(id) ?? 0) + 1);\n this.state = state;\n this.changed(sealed);\n const partition = this.partition;\n // A queued get rechecks the carrier, whose swap is awaiting this notification.\n // Publish custody now; cleanup follows asynchronously, never holding that swap.\n void this.enqueue(async () => {\n for (const key of [...bytes.remove, ...bytes.put.map((put) => put.key)]) {\n const uri = this.path(key, partition);\n if (uri) await this.remove(uri);\n }\n }).catch(() => undefined);\n }\n async wiped(partition: string): Promise<void> {\n if (this.partition === partition) this.closed();\n await this.enqueue(() => this.remove(partitionDirectory(this.fs.root, partition)));\n }\n closed(): void {\n this.materialized.clear();\n this.partition = undefined;\n this.state = undefined;\n this.changed(true);\n }\n}\n\n/** A message/grant/watermark swap that leaves a clip's lease unchanged keeps its player. */\nfunction assetLeases(state: PersistedRangeState | undefined): Map<string, string> {\n const refs = new Map<string, string[]>();\n for (const [key, row] of Object.entries(state?.rows ?? {})) {\n for (const id of assetIdsIn(row.row)) {\n const leases = refs.get(id) ?? [];\n leases.push(JSON.stringify([key, row.row, Boolean(state?.viewed[key])]));\n refs.set(id, leases);\n }\n }\n return new Map([...refs].map(([id, leases]) => [id, leases.sort().join(\"\\n\")]));\n}\n\nexport function fileBackend(backend: RangeStoreBackend, files: NativeAssetFiles): RangeStoreBackend {\n return {\n carrier: backend.carrier,\n async open(partition) {\n const state = await backend.open(partition);\n // Sweep errors leave unusable derivatives, never a substituted memory carrier.\n try { await files.opened(partition, state); }\n catch (error) { files.closed(); await backend.close(); throw error; }\n return state;\n },\n async replace(state, bytes) {\n await backend.replace(state, bytes);\n await files.replaced(state, bytes);\n },\n readBytes: backend.readBytes ? (key) => backend.readBytes!(key) : undefined,\n async wipe(partition) {\n await backend.wipe(partition); // Wiping the open partition also closes SQLite.\n await files.wiped(partition);\n },\n async close() {\n files.closed();\n await backend.close();\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["React", "useEffect", "useMemo", "useRef", "useState", "Image", "PixelRatio", "View", "VideoView", "useVideoPlayer", "useSnapbackClient", "useSnapbackSnapshot", "SnapbackRefusal", "useEffect", "useMemo", "useRef", "useState", "useSyncExternalStore", "useSnapbackClient", "useSnapbackSnapshot", "isSnapbackRefusal", "SnapbackRefusal", "FILES", "nativeAssetFiles", "client", "FILES", "none", "noSubscription", "fileRefusal", "error", "isSnapbackRefusal", "SnapbackRefusal", "bytesDataUri", "value", "alphabet", "output", "i", "a", "b", "c", "n", "useFile", "asset", "width", "client", "useSnapbackClient", "snapshot", "useSnapbackSnapshot", "files", "nativeAssetFiles", "getRevision", "useMemo", "revision", "useSyncExternalStore", "identity", "result", "setResult", "useState", "useEffect", "active", "uri", "useAssetFile", "options", "useNativeSource", "file", "failure", "viewing", "useRef", "memory", "setMemory", "attempted", "current", "url", "refusal", "custody", "held", "AssetImage", "asset", "width", "height", "style", "onLayout", "onRefusal", "onSourceChange", "props", "measured", "setMeasured", "useState", "requested", "PixelRatio", "uri", "refusal", "settled", "useNativeSource", "callback", "useRef", "React", "useEffect", "Image", "event", "Player", "_asset", "play", "onPlay", "_onSourceChange", "_autoPlay", "_onPlaybackStart", "player", "useVideoPlayer", "errors", "status", "error", "SnapbackRefusal", "subscription", "isPlaying", "VideoView", "AssetVideo", "onPlaybackStart", "autoPlay", "client", "useSnapbackClient", "snapshot", "useSnapbackSnapshot", "identity", "useMemo", "policy", "setPolicy", "active", "once", "refused", "size", "View"]
7
+ }
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { type SnapbackRefusal } from "./client.js";
3
+ import type { ImageAsset, VideoAsset } from "./types.js";
4
+ export * from "./react-core.js";
5
+ export { indexedDbRangeStore } from "./store/indexeddb.js";
6
+ export type AssetImageProps = Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "width" | "height"> & {
7
+ asset: ImageAsset;
8
+ width?: number | string;
9
+ height?: number | string;
10
+ };
11
+ export declare function AssetImage({ asset, width, height, style, ...props }: AssetImageProps): React.ReactElement;
12
+ export type AssetVideoProps = Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "src" | "width" | "height"> & {
13
+ asset: VideoAsset;
14
+ onRefusal?: (refusal: SnapbackRefusal) => void;
15
+ width?: number | string;
16
+ height?: number | string;
17
+ };
18
+ export declare function AssetVideo({ asset, width, height, style, controls, onPlay, onRefusal, ...props }: AssetVideoProps): React.ReactElement;
19
+ export declare function DevOverlay(): React.ReactElement;