tempest-react-sdk 0.81.0 → 0.83.0

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 (194) hide show
  1. package/README.md +9 -10
  2. package/bin/create-tempest-app.e2e.test.mjs +31 -0
  3. package/bin/create-tempest-app.mjs +11 -1
  4. package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
  5. package/bin/lib/doctor/vitest.mjs +120 -0
  6. package/bin/lib/doctor/vitest.test.mjs +139 -0
  7. package/bin/lib/fs/contain.mjs +7 -0
  8. package/bin/lib/openapi/escape.mjs +68 -4
  9. package/bin/lib/openapi/generate.hostile.test.mjs +228 -0
  10. package/bin/lib/openapi/generate.mjs +119 -15
  11. package/bin/lib/openapi/pattern-risk.mjs +134 -0
  12. package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
  13. package/bin/tempest-gen.test.mjs +100 -0
  14. package/bin/tempest.mjs +24 -8
  15. package/dist/access/use-can.cjs +1 -1
  16. package/dist/access/use-can.cjs.map +1 -1
  17. package/dist/access/use-can.js +25 -28
  18. package/dist/access/use-can.js.map +1 -1
  19. package/dist/auth/create-tempest-auth.cjs +1 -1
  20. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  21. package/dist/auth/create-tempest-auth.js +189 -143
  22. package/dist/auth/create-tempest-auth.js.map +1 -1
  23. package/dist/auth/session-identity.cjs +2 -0
  24. package/dist/auth/session-identity.cjs.map +1 -0
  25. package/dist/auth/session-identity.js +12 -0
  26. package/dist/auth/session-identity.js.map +1 -0
  27. package/dist/br/boleto.cjs.map +1 -1
  28. package/dist/br/boleto.js.map +1 -1
  29. package/dist/components/AuthForm/AuthLinks.cjs +1 -1
  30. package/dist/components/AuthForm/AuthLinks.cjs.map +1 -1
  31. package/dist/components/AuthForm/AuthLinks.js +18 -15
  32. package/dist/components/AuthForm/AuthLinks.js.map +1 -1
  33. package/dist/components/AuthPage/AuthPage.cjs +1 -1
  34. package/dist/components/AuthPage/AuthPage.cjs.map +1 -1
  35. package/dist/components/AuthPage/AuthPage.js +58 -52
  36. package/dist/components/AuthPage/AuthPage.js.map +1 -1
  37. package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
  38. package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
  39. package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
  40. package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
  41. package/dist/components/Button/Button.cjs +1 -1
  42. package/dist/components/Button/Button.cjs.map +1 -1
  43. package/dist/components/Button/Button.js +41 -40
  44. package/dist/components/Button/Button.js.map +1 -1
  45. package/dist/components/Chat/ChatAttachments.cjs +1 -1
  46. package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
  47. package/dist/components/Chat/ChatAttachments.js +38 -38
  48. package/dist/components/Form/Form.cjs +1 -1
  49. package/dist/components/Form/Form.cjs.map +1 -1
  50. package/dist/components/Form/Form.js +30 -32
  51. package/dist/components/Form/Form.js.map +1 -1
  52. package/dist/components/Layout/Layout.cjs +1 -1
  53. package/dist/components/Layout/Layout.cjs.map +1 -1
  54. package/dist/components/Layout/Layout.js +25 -24
  55. package/dist/components/Layout/Layout.js.map +1 -1
  56. package/dist/components/Layout/gap.cjs +2 -0
  57. package/dist/components/Layout/gap.cjs.map +1 -0
  58. package/dist/components/Layout/gap.js +15 -0
  59. package/dist/components/Layout/gap.js.map +1 -0
  60. package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
  61. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  62. package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
  63. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  64. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  65. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  66. package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
  67. package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
  68. package/dist/components/Sidebar/SidebarEntry.js +30 -29
  69. package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
  70. package/dist/data/create-data-provider.cjs +1 -1
  71. package/dist/data/create-data-provider.cjs.map +1 -1
  72. package/dist/data/create-data-provider.js +1 -2
  73. package/dist/data/create-data-provider.js.map +1 -1
  74. package/dist/forms/validate-form.cjs +1 -1
  75. package/dist/forms/validate-form.cjs.map +1 -1
  76. package/dist/forms/validate-form.js +6 -1
  77. package/dist/forms/validate-form.js.map +1 -1
  78. package/dist/forms/zod-resolver.cjs +1 -1
  79. package/dist/forms/zod-resolver.cjs.map +1 -1
  80. package/dist/forms/zod-resolver.js +8 -3
  81. package/dist/forms/zod-resolver.js.map +1 -1
  82. package/dist/http/api-client.cjs +1 -1
  83. package/dist/http/api-client.cjs.map +1 -1
  84. package/dist/http/api-client.js +90 -72
  85. package/dist/http/api-client.js.map +1 -1
  86. package/dist/http/credential-scope.cjs +1 -1
  87. package/dist/http/credential-scope.cjs.map +1 -1
  88. package/dist/http/credential-scope.js +33 -9
  89. package/dist/http/credential-scope.js.map +1 -1
  90. package/dist/http/presigned-post.cjs.map +1 -1
  91. package/dist/http/presigned-post.js.map +1 -1
  92. package/dist/http/resumable-upload.cjs +1 -1
  93. package/dist/http/resumable-upload.cjs.map +1 -1
  94. package/dist/http/resumable-upload.js +167 -112
  95. package/dist/http/resumable-upload.js.map +1 -1
  96. package/dist/http/upload-with-progress.cjs +1 -1
  97. package/dist/http/upload-with-progress.cjs.map +1 -1
  98. package/dist/http/upload-with-progress.js +100 -44
  99. package/dist/http/upload-with-progress.js.map +1 -1
  100. package/dist/oauth/GoogleSignIn.cjs +1 -1
  101. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  102. package/dist/oauth/GoogleSignIn.js +5 -4
  103. package/dist/oauth/GoogleSignIn.js.map +1 -1
  104. package/dist/offline/create-offline-sync.cjs +1 -1
  105. package/dist/offline/create-offline-sync.cjs.map +1 -1
  106. package/dist/offline/create-offline-sync.js +87 -69
  107. package/dist/offline/create-offline-sync.js.map +1 -1
  108. package/dist/query/offline-persistence.cjs +1 -1
  109. package/dist/query/offline-persistence.cjs.map +1 -1
  110. package/dist/query/offline-persistence.js +67 -44
  111. package/dist/query/offline-persistence.js.map +1 -1
  112. package/dist/sse/create-event-stream.cjs +1 -1
  113. package/dist/sse/create-event-stream.cjs.map +1 -1
  114. package/dist/sse/create-event-stream.js +48 -41
  115. package/dist/sse/create-event-stream.js.map +1 -1
  116. package/dist/sse/ticket-event-stream.cjs +1 -1
  117. package/dist/sse/ticket-event-stream.cjs.map +1 -1
  118. package/dist/sse/ticket-event-stream.js +48 -19
  119. package/dist/sse/ticket-event-stream.js.map +1 -1
  120. package/dist/sse/use-event-stream.cjs +1 -1
  121. package/dist/sse/use-event-stream.cjs.map +1 -1
  122. package/dist/sse/use-event-stream.js +19 -14
  123. package/dist/sse/use-event-stream.js.map +1 -1
  124. package/dist/sw/background-sync.cjs +1 -1
  125. package/dist/sw/background-sync.cjs.map +1 -1
  126. package/dist/sw/background-sync.js +10 -8
  127. package/dist/sw/background-sync.js.map +1 -1
  128. package/dist/sw/cache.cjs +1 -1
  129. package/dist/sw/cache.cjs.map +1 -1
  130. package/dist/sw/cache.js +111 -55
  131. package/dist/sw/cache.js.map +1 -1
  132. package/dist/sw/create-push-handler.cjs +1 -1
  133. package/dist/sw/create-push-handler.cjs.map +1 -1
  134. package/dist/sw/create-push-handler.js +20 -10
  135. package/dist/sw/create-push-handler.js.map +1 -1
  136. package/dist/sw.d.ts +65 -14
  137. package/dist/tabular/compact.cjs +1 -1
  138. package/dist/tabular/compact.cjs.map +1 -1
  139. package/dist/tabular/compact.js +35 -16
  140. package/dist/tabular/compact.js.map +1 -1
  141. package/dist/tabular/manifest.cjs +1 -1
  142. package/dist/tabular/manifest.cjs.map +1 -1
  143. package/dist/tabular/manifest.js +33 -15
  144. package/dist/tabular/manifest.js.map +1 -1
  145. package/dist/tabular.d.ts +11 -1
  146. package/dist/tempest-react-sdk.cjs +1 -1
  147. package/dist/tempest-react-sdk.d.ts +543 -70
  148. package/dist/tempest-react-sdk.js +224 -224
  149. package/dist/theme/initial-theme.cjs +4 -4
  150. package/dist/theme/initial-theme.cjs.map +1 -1
  151. package/dist/theme/initial-theme.js +8 -5
  152. package/dist/theme/initial-theme.js.map +1 -1
  153. package/dist/utils/compressed-storage.cjs +1 -1
  154. package/dist/utils/compressed-storage.cjs.map +1 -1
  155. package/dist/utils/compressed-storage.js +13 -7
  156. package/dist/utils/compressed-storage.js.map +1 -1
  157. package/dist/utils/objects.cjs +1 -1
  158. package/dist/utils/objects.cjs.map +1 -1
  159. package/dist/utils/objects.js +13 -7
  160. package/dist/utils/objects.js.map +1 -1
  161. package/dist/utils/safe-url.cjs +1 -1
  162. package/dist/utils/safe-url.cjs.map +1 -1
  163. package/dist/utils/safe-url.js +4 -1
  164. package/dist/utils/safe-url.js.map +1 -1
  165. package/dist/vite/base-url.cjs +1 -1
  166. package/dist/vite/base-url.cjs.map +1 -1
  167. package/dist/vite/base-url.js +5 -1
  168. package/dist/vite/base-url.js.map +1 -1
  169. package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
  170. package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
  171. package/dist/vite/tempest-pwa-dev-sw.js +96 -31
  172. package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
  173. package/dist/vite/tempest-pwa-manifest.cjs +2 -2
  174. package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
  175. package/dist/vite/tempest-pwa-manifest.js +32 -16
  176. package/dist/vite/tempest-pwa-manifest.js.map +1 -1
  177. package/dist/vite.cjs +1 -1
  178. package/dist/vite.d.ts +63 -12
  179. package/dist/vite.js +4 -4
  180. package/dist/webrtc/peer-mesh.cjs +1 -1
  181. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  182. package/dist/webrtc/peer-mesh.js +7 -1
  183. package/dist/webrtc/peer-mesh.js.map +1 -1
  184. package/dist/ws/create-web-socket.cjs +1 -1
  185. package/dist/ws/create-web-socket.cjs.map +1 -1
  186. package/dist/ws/create-web-socket.js +77 -61
  187. package/dist/ws/create-web-socket.js.map +1 -1
  188. package/dist/ws/use-web-socket.cjs +1 -1
  189. package/dist/ws/use-web-socket.cjs.map +1 -1
  190. package/dist/ws/use-web-socket.js +4 -3
  191. package/dist/ws/use-web-socket.js.map +1 -1
  192. package/package.json +1 -1
  193. package/template-pwa/src/pwa.tsx +42 -0
  194. package/template-pwa/src/sw.ts +5 -4
@@ -1 +1 @@
1
- {"version":3,"file":"upload-with-progress.js","names":["parseErrorBody","raw","uploadWithProgress","options","signal","xhr","method","url","withCredentials","token","getToken","sentRequestId","requestId","randomId","finalHeaders","headers","credentialOrigin","isTrustedCredentialTarget","trustedOrigins","reportSuppressedCredential","csrfHeaders","key","value","onProgress","event","handleAbort","isSuccess","contentType","errorBody","TempestApiError","buildApiError","name","parser","err","reject","body"],"sources":["../../src/http/upload-with-progress.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — XMLHttpRequest is used instead of fetch precisely\n * because it reports upload progress, and its API is event-based: the body wires\n * load, error, abort, timeout and progress against one request object, plus the\n * AbortSignal bridge.\n */\nimport { randomId } from \"../utils\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport type { CsrfOptions } from \"./csrf\";\nimport { buildApiError, TempestApiError } from \"./errors\";\n\nexport interface UploadProgressEvent {\n /** Bytes already uploaded. */\n loaded: number;\n /** Total payload size in bytes — only meaningful when `lengthComputable` is true. */\n total: number;\n /** Fraction between 0 and 1, or null when total is unknown. */\n fraction: number | null;\n lengthComputable: boolean;\n}\n\nexport interface UploadWithProgressOptions {\n url: string;\n body: FormData | Blob | File;\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n headers?: Record<string, string>;\n /** Returns the current bearer token. */\n getToken?: () => string | null | undefined;\n /**\n * Origin the `getToken` credential belongs to, normally the API's.\n *\n * Unlike `createApiClient`, this helper has no base URL to infer a scope\n * from — the caller names the destination on every call. Set this when the\n * `url` can come from somewhere other than your own code (a signed URL the\n * API handed back, a value from a response body) and the token must not\n * follow it off-origin. Left unset, the header goes wherever `url` points,\n * which is the behaviour every version before 0.66.0 had.\n */\n credentialOrigin?: string;\n /**\n * Extra origins allowed to receive the credential, on top of\n * {@link credentialOrigin}. Ignored when that one is unset.\n */\n trustedOrigins?: readonly string[];\n /** Send cookies. Defaults to false. */\n withCredentials?: boolean;\n /**\n * Send a CSRF token with the upload, as `createApiClient`'s `csrf` does.\n * Off by default; `true` reads the `csrf_token` cookie into `X-CSRF-Token`.\n *\n * Scoped to {@link credentialOrigin} plus {@link trustedOrigins} — and,\n * unlike the bearer token, scoped even when `credentialOrigin` is unset:\n * then the scope is the page's own origin. The bearer default stays open\n * only for compatibility with every version before 0.66.0; this option is\n * new, so it starts closed. An API on another origin sets\n * `credentialOrigin` to receive the token.\n */\n csrf?: boolean | CsrfOptions;\n /** Called on every `progress` event from the XHR upload channel. */\n onProgress?: (event: UploadProgressEvent) => void;\n /** Abort the request. */\n signal?: AbortSignal;\n /** Override the JSON parser. Defaults to `JSON.parse`. */\n parser?: (raw: string) => unknown;\n /**\n * Per-request correlation id sent as `X-Request-ID` (Tempest convention).\n * Defaults to a generated id. Return an empty string to disable.\n */\n requestId?: () => string;\n}\n\nfunction parseErrorBody(raw: string): unknown {\n if (!raw) return null;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Upload a file (or any payload) with byte-level progress reporting.\n *\n * `fetch` cannot report upload progress in browsers, so this helper falls\n * back to `XMLHttpRequest`. It mirrors the error contract used by\n * {@link createApiClient}: non-2xx responses reject with a `TempestApiError`.\n *\n * @returns The parsed JSON response, or the raw text when the response is not JSON.\n */\nexport function uploadWithProgress<T = unknown>(options: UploadWithProgressOptions): Promise<T> {\n const {\n url,\n body,\n method = \"POST\",\n headers = {},\n getToken,\n credentialOrigin,\n trustedOrigins,\n withCredentials = false,\n csrf,\n onProgress,\n signal,\n parser = JSON.parse,\n requestId,\n } = options;\n\n return new Promise<T>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const xhr = new XMLHttpRequest();\n xhr.open(method, url);\n xhr.withCredentials = withCredentials;\n\n const token = getToken?.();\n const sentRequestId = requestId ? requestId() : randomId();\n const finalHeaders: Record<string, string> = { ...headers };\n if (token && !(\"Authorization\" in finalHeaders)) {\n if (\n credentialOrigin === undefined ||\n isTrustedCredentialTarget(url, credentialOrigin, trustedOrigins)\n ) {\n finalHeaders.Authorization = `Bearer ${token}`;\n } else {\n reportSuppressedCredential(url, credentialOrigin);\n }\n }\n Object.assign(\n finalHeaders,\n csrfHeaders({\n method,\n url,\n reference: credentialOrigin ?? globalThis.location?.origin ?? url,\n csrf,\n trustedOrigins,\n headers,\n }),\n );\n if (sentRequestId && !(\"X-Request-ID\" in finalHeaders)) {\n finalHeaders[\"X-Request-ID\"] = sentRequestId;\n }\n for (const [key, value] of Object.entries(finalHeaders)) {\n xhr.setRequestHeader(key, value);\n }\n\n if (onProgress) {\n xhr.upload.onprogress = (event) => {\n onProgress({\n loaded: event.loaded,\n total: event.total,\n fraction: event.lengthComputable ? event.loaded / event.total : null,\n lengthComputable: event.lengthComputable,\n });\n };\n }\n\n function handleAbort(): void {\n xhr.abort();\n }\n signal?.addEventListener(\"abort\", handleAbort);\n\n xhr.onload = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n const isSuccess = xhr.status >= 200 && xhr.status < 300;\n const contentType = xhr.getResponseHeader(\"content-type\") ?? \"\";\n\n if (!isSuccess) {\n const errorBody = parseErrorBody(xhr.responseText);\n reject(\n new TempestApiError(\n buildApiError(\n xhr.status,\n errorBody,\n { get: (name) => xhr.getResponseHeader(name) },\n sentRequestId,\n ),\n ),\n );\n return;\n }\n\n if (xhr.status === 204 || !xhr.responseText) {\n resolve(undefined as T);\n return;\n }\n\n if (contentType.includes(\"application/json\")) {\n try {\n resolve(parser(xhr.responseText) as T);\n } catch (err) {\n reject(err);\n }\n } else {\n resolve(xhr.responseText as unknown as T);\n }\n };\n\n xhr.onerror = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: sentRequestId || undefined,\n }),\n );\n };\n\n xhr.onabort = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n\n xhr.send(body);\n });\n}\n"],"mappings":";;;;;AAwEA,SAASA,EAAe,GAAsB;CAC1C,IAAI,CAACC,GAAK,OAAO;CACjB,IAAI;EACA,OAAO,KAAK,MAAMA,CAAG;CACzB,QAAQ;EACJ,OAAOA;CACX;AACJ;AAWA,SAAgBC,EAAgC,GAAgD;CAC5F,IAAM,EACF,QACA,SACA,YAAS,QACT,aAAU,CAAC,GACX,aACA,qBACA,mBACA,qBAAkB,IAClB,SACA,eACA,WACA,YAAS,KAAK,OACd,iBACAC;CAEJ,OAAO,IAAI,SAAY,GAAS,MAAW;EACvC,IAAIC,GAAQ,SAAS;GACjB,EAAO,IAAI,aAAa,WAAW,YAAY,CAAC;GAChD;EACJ;EAEA,IAAMC,IAAM,IAAI,eAAe;EAE/B,AADAA,EAAI,KAAKC,GAAQC,CAAG,GACpB,EAAI,kBAAkBC;EAEtB,IAAMC,IAAQC,IAAW,GACnBC,IAAgBC,IAAYA,EAAU,IAAIC,EAAS,GACnDC,IAAuC,EAAE,GAAGC,EAAQ;EAsB1D,AArBIN,KAAS,EAAE,mBAAmBK,OAE1BE,MAAqB,KAAA,KACrBC,EAA0BV,GAAKS,GAAkBE,CAAc,IAE/D,EAAa,gBAAgB,UAAUT,MAEvCU,EAA2BZ,GAAKS,CAAgB,IAGxD,OAAO,OACHF,GACAM,EAAY;GACR;GACA;GACA,WAAWJ,KAAoB,WAAW,UAAU,UAAUT;GAC9D;GACA;GACA;EACJ,CAAC,CACL,GACII,KAAiB,EAAE,kBAAkBG,OACrC,EAAa,kBAAkBH;EAEnC,KAAK,IAAM,CAACU,GAAKC,MAAU,OAAO,QAAQR,CAAY,GAClD,EAAI,iBAAiBO,GAAKC,CAAK;EAGnC,AAAIC,MACA,EAAI,OAAO,cAAc,MAAU;GAC/B,EAAW;IACP,QAAQC,EAAM;IACd,OAAOA,EAAM;IACb,UAAUA,EAAM,mBAAmBA,EAAM,SAASA,EAAM,QAAQ;IAChE,kBAAkBA,EAAM;GAC5B,CAAC;EACL;EAGJ,SAASC,IAAoB;GACzB,EAAI,MAAM;EACd;EAuDA,AAtDArB,GAAQ,iBAAiB,SAASqB,CAAW,GAE7C,EAAI,eAAe;GACf,GAAQ,oBAAoB,SAASA,CAAW;GAChD,IAAMC,IAAYrB,EAAI,UAAU,OAAOA,EAAI,SAAS,KAC9CsB,IAActB,EAAI,kBAAkB,cAAc,KAAK;GAE7D,IAAI,CAACqB,GAAW;IACZ,IAAME,IAAY5B,EAAeK,EAAI,YAAY;IACjD,EACI,IAAIwB,EACAC,EACIzB,EAAI,QACJuB,GACA,EAAE,MAAM,MAASvB,EAAI,kBAAkB0B,CAAI,EAAE,GAC7CpB,CACJ,CACJ,CACJ;GAEJ,OAEA,IAAIN,EAAI,WAAW,OAAO,CAACA,EAAI,cAC3B,EAAQ,KAAA,CAAc;QAI1B,IAAIsB,EAAY,SAAS,kBAAkB,GACvC,IAAI;IACA,EAAQK,EAAO3B,EAAI,YAAY,CAAM;GACzC,SAAS4B,GAAK;IACV,EAAOA,CAAG;GACd;QAEA,EAAQ5B,EAAI,YAA4B;EAEhD,GAEA,EAAI,gBAAgB;GAEhB,AADAD,GAAQ,oBAAoB,SAASqB,CAAW,GAChDS,EACI,IAAIL,EAAgB;IAChB,QAAQ;IACR,QAAQ;IACR,WAAWlB,KAAiB,KAAA;GAChC,CAAC,CACL;EACJ,GAEA,EAAI,gBAAgB;GAEhB,AADAP,GAAQ,oBAAoB,SAASqB,CAAW,GAChDS,EAAO,IAAI,aAAa,WAAW,YAAY,CAAC;EACpD,GAEA7B,EAAI,KAAK8B,CAAI;CACjB,CAAC;AACL"}
1
+ {"version":3,"file":"upload-with-progress.js","names":["UPLOAD_REDIRECT_REFUSED","parseErrorBody","raw","settle","answer","TempestApiError","buildApiError","requestId","parser","originOf","resolved","resolveRequestUrl","url","sendWithoutRedirects","response","init","name","uploadWithProgress","options","signal","token","getToken","sentRequestId","randomId","finalHeaders","headers","scope","credentialOrigin","isTrustedCredentialTarget","trustedOrigins","reportSuppressedCredential","csrfHeaders","redirect","sized","body","total","onProgress","xhr","method","withCredentials","key","value","event","handleAbort","landed","err","reject"],"sources":["../../src/http/upload-with-progress.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — XMLHttpRequest is used instead of fetch precisely\n * because it reports upload progress, and its API is event-based: the body wires\n * load, error, abort, timeout and progress against one request object, plus the\n * AbortSignal bridge.\n */\nimport { randomId } from \"../utils\";\nimport {\n isTrustedCredentialTarget,\n reportSuppressedCredential,\n resolveRequestUrl,\n} from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport type { CsrfOptions } from \"./csrf\";\nimport { buildApiError, TempestApiError } from \"./errors\";\n\nexport interface UploadProgressEvent {\n /** Bytes already uploaded. */\n loaded: number;\n /** Total payload size in bytes — only meaningful when `lengthComputable` is true. */\n total: number;\n /** Fraction between 0 and 1, or null when total is unknown. */\n fraction: number | null;\n lengthComputable: boolean;\n}\n\nexport interface UploadWithProgressOptions {\n url: string;\n body: FormData | Blob | File;\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n headers?: Record<string, string>;\n /** Returns the current bearer token. */\n getToken?: () => string | null | undefined;\n /**\n * Origin the `getToken` credential belongs to, normally the API's.\n * Defaults to the page's own origin (`location.origin`).\n *\n * Unlike `createApiClient`, this helper has no base URL to infer a scope\n * from — the caller names the destination on every call, and the `url` can\n * come from somewhere other than your own code (a signed URL the API handed\n * back, a value from a response body). Until 0.82.0 an unset scope sent the\n * token wherever `url` pointed; it now starts closed, like the CSRF token\n * always did, and an API on another origin sets this to receive it (#530).\n *\n * Outside a browsing context there is no page origin, and the scope falls\n * back to `url` itself.\n */\n credentialOrigin?: string;\n /**\n * Extra origins allowed to receive the credential, on top of\n * {@link credentialOrigin} (or the page origin it defaults to).\n */\n trustedOrigins?: readonly string[];\n /** Send cookies. Defaults to false. */\n withCredentials?: boolean;\n /**\n * Send a CSRF token with the upload, as `createApiClient`'s `csrf` does.\n * Off by default; `true` reads the `csrf_token` cookie into `X-CSRF-Token`.\n *\n * Scoped exactly like the bearer token: to {@link credentialOrigin} (the\n * page's own origin when unset) plus {@link trustedOrigins}. An API on\n * another origin sets `credentialOrigin` to receive the token.\n */\n csrf?: boolean | CsrfOptions;\n /** Called on every `progress` event from the XHR upload channel. */\n onProgress?: (event: UploadProgressEvent) => void;\n /** Abort the request. */\n signal?: AbortSignal;\n /** Override the JSON parser. Defaults to `JSON.parse`. */\n parser?: (raw: string) => unknown;\n /**\n * Per-request correlation id sent as `X-Request-ID` (Tempest convention).\n * Defaults to a generated id. Return an empty string to disable.\n */\n requestId?: () => string;\n /**\n * What to do when the server answers with a redirect. Default `\"follow\"`.\n *\n * - `\"follow\"` — the `XMLHttpRequest` path, with byte-level progress. The\n * browser follows every redirect and offers no way to stop it: measured\n * in Chrome 150 (#570), a same-origin upload answered `301`/`302`/`303`/\n * `307`/`308` towards another origin delivered `X-Api-Key` from\n * {@link headers} and the SDK's own `X-CSRF-Token` there on all five, and\n * the body on `307`/`308`; only `Authorization` was stripped. What this\n * path does enforce (since 0.83.0): an answer from another origin than\n * the one the request was sent to is never the result — it rejects with\n * `code: \"REDIRECT_REFUSED\"`, so that host cannot hand the app a URL to\n * store. Until 0.82.0 it resolved with the other host's body.\n * - `\"error\"` — goes out over `fetch` with `redirect: \"manual\"`, and a\n * redirect rejects with `code: \"REDIRECT_REFUSED\"` before anything is\n * sent to the other host. The cost: `fetch` reports no upload progress,\n * so `onProgress` fires once, when the server confirms the whole body. A\n * streamed `fetch` body would report progress, but Chrome refuses one\n * over HTTP/1.1 (`TypeError: Failed to fetch`, measured) and Firefox and\n * Safari do not support it.\n *\n * Use `\"error\"` when {@link headers} carry a credential (an `X-Api-Key`) and\n * the server behind `url` could redirect; sending the bearer through\n * {@link getToken} keeps `\"follow\"` safe for it, because the browser strips\n * `Authorization` on a cross-origin redirect.\n */\n redirect?: \"follow\" | \"error\";\n}\n\n/**\n * The `code` of the {@link TempestApiError} an upload rejects with when a\n * redirect is refused, or when the answer came from another origin — the code\n * `createApiClient` and `createResumableUpload` use for the same refusal.\n */\nconst UPLOAD_REDIRECT_REFUSED = \"REDIRECT_REFUSED\";\n\n/** What a finished upload answered, whichever transport carried it. */\ninterface UploadAnswer {\n status: number;\n text: string;\n header: (name: string) => string | null;\n}\n\nfunction parseErrorBody(raw: string): unknown {\n if (!raw) return null;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Turn a finished upload's answer into the promised value, or the error the\n * contract promises. Shared by both transports, so `redirect: \"follow\"` and\n * `redirect: \"error\"` resolve and reject the same way.\n *\n * @param answer - Status, raw text and a header reader.\n * @param parser - The JSON parser.\n * @param requestId - The `X-Request-ID` sent, for the error.\n * @returns The parsed JSON, the raw text, or `undefined` for an empty answer.\n * @throws {TempestApiError} On a non-2xx status.\n */\nfunction settle<T>(answer: UploadAnswer, parser: (raw: string) => unknown, requestId: string): T {\n if (answer.status < 200 || answer.status >= 300) {\n throw new TempestApiError(\n buildApiError(\n answer.status,\n parseErrorBody(answer.text),\n { get: answer.header },\n requestId,\n ),\n );\n }\n if (answer.status === 204 || !answer.text) return undefined as T;\n if ((answer.header(\"content-type\") ?? \"\").includes(\"application/json\")) {\n return parser(answer.text) as T;\n }\n return answer.text as unknown as T;\n}\n\n/**\n * The origin a URL resolves to, or `null` when it does not resolve.\n *\n * @param url - Absolute or relative URL.\n * @returns The origin.\n */\nfunction originOf(url: string): string | null {\n const resolved = resolveRequestUrl(url);\n return resolved === null ? null : new URL(resolved).origin;\n}\n\n/** What {@link sendWithoutRedirects} sends. */\ninterface ManualUpload {\n url: string;\n method: string;\n headers: Record<string, string>;\n body: FormData | Blob;\n withCredentials: boolean;\n signal?: AbortSignal;\n requestId: string;\n}\n\n/**\n * Send the upload over `fetch` with `redirect: \"manual\"` — the\n * `redirect: \"error\"` path of {@link uploadWithProgress}.\n *\n * A browser hands the stopped redirect back as an `opaqueredirect` with\n * status `0`; a server runtime hands back the real `3xx`. Both are refused;\n * `304` is a cache answer, not a redirect.\n *\n * @param init - Where, how, and what to send.\n * @returns The answer.\n * @throws {TempestApiError} `code: \"REDIRECT_REFUSED\"` on a redirect, status\n * `0` on a network failure.\n */\nasync function sendWithoutRedirects(init: ManualUpload): Promise<UploadAnswer> {\n let response: Response;\n try {\n response = await fetch(init.url, {\n method: init.method,\n headers: init.headers,\n body: init.body,\n credentials: init.withCredentials ? \"include\" : \"same-origin\",\n redirect: \"manual\",\n signal: init.signal,\n });\n } catch {\n if (init.signal?.aborted) throw new DOMException(\"Aborted\", \"AbortError\");\n throw new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: init.requestId || undefined,\n });\n }\n if (\n response.type === \"opaqueredirect\" ||\n (response.status >= 300 && response.status < 400 && response.status !== 304)\n ) {\n throw new TempestApiError({\n status: response.status,\n code: UPLOAD_REDIRECT_REFUSED,\n detail: \"Redirect não seguido no upload.\",\n requestId: init.requestId || undefined,\n });\n }\n return {\n status: response.status,\n text: await response.text(),\n header: (name) => response.headers.get(name),\n };\n}\n\n/**\n * Upload a file (or any payload) with byte-level progress reporting.\n *\n * `fetch` cannot report upload progress in browsers, so this helper falls\n * back to `XMLHttpRequest`. It mirrors the error contract used by\n * {@link createApiClient}: non-2xx responses reject with a `TempestApiError`.\n *\n * An `XMLHttpRequest` cannot refuse a redirect; see\n * {@link UploadWithProgressOptions.redirect} for what follows one, what the\n * helper still refuses, and the `fetch` path that trades progress for never\n * following.\n *\n * @returns The parsed JSON response, or the raw text when the response is not JSON.\n */\nexport function uploadWithProgress<T = unknown>(options: UploadWithProgressOptions): Promise<T> {\n const {\n url,\n body,\n method = \"POST\",\n headers = {},\n getToken,\n credentialOrigin,\n trustedOrigins,\n withCredentials = false,\n csrf,\n onProgress,\n signal,\n parser = JSON.parse,\n requestId,\n redirect = \"follow\",\n } = options;\n\n if (signal?.aborted) return Promise.reject(new DOMException(\"Aborted\", \"AbortError\"));\n\n const token = getToken?.();\n const sentRequestId = requestId ? requestId() : randomId();\n const finalHeaders: Record<string, string> = { ...headers };\n const scope = credentialOrigin ?? globalThis.location?.origin ?? url;\n if (token && !(\"Authorization\" in finalHeaders)) {\n if (isTrustedCredentialTarget(url, scope, trustedOrigins)) {\n finalHeaders.Authorization = `Bearer ${token}`;\n } else {\n reportSuppressedCredential(url, scope);\n }\n }\n Object.assign(\n finalHeaders,\n csrfHeaders({\n method,\n url,\n reference: scope,\n csrf,\n trustedOrigins,\n headers,\n }),\n );\n if (sentRequestId && !(\"X-Request-ID\" in finalHeaders)) {\n finalHeaders[\"X-Request-ID\"] = sentRequestId;\n }\n\n if (redirect === \"error\") {\n return sendWithoutRedirects({\n url,\n method,\n headers: finalHeaders,\n body,\n withCredentials,\n signal,\n requestId: sentRequestId,\n }).then((answer) => {\n const sized = body instanceof Blob;\n const total = sized ? body.size : 0;\n onProgress?.({\n loaded: total,\n total,\n fraction: sized ? 1 : null,\n lengthComputable: sized,\n });\n return settle<T>(answer, parser, sentRequestId);\n });\n }\n\n return new Promise<T>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(method, url);\n xhr.withCredentials = withCredentials;\n for (const [key, value] of Object.entries(finalHeaders)) {\n xhr.setRequestHeader(key, value);\n }\n\n if (onProgress) {\n xhr.upload.onprogress = (event) => {\n onProgress({\n loaded: event.loaded,\n total: event.total,\n fraction: event.lengthComputable ? event.loaded / event.total : null,\n lengthComputable: event.lengthComputable,\n });\n };\n }\n\n function handleAbort(): void {\n xhr.abort();\n }\n signal?.addEventListener(\"abort\", handleAbort);\n\n xhr.onload = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n const landed = xhr.responseURL ? originOf(xhr.responseURL) : null;\n if (landed !== null && landed !== originOf(url)) {\n reject(\n new TempestApiError({\n status: xhr.status,\n code: UPLOAD_REDIRECT_REFUSED,\n detail: \"O upload foi redirecionado para outra origem.\",\n requestId: sentRequestId || undefined,\n }),\n );\n return;\n }\n try {\n resolve(\n settle<T>(\n {\n status: xhr.status,\n text: xhr.responseText,\n header: (name) => xhr.getResponseHeader(name),\n },\n parser,\n sentRequestId,\n ),\n );\n } catch (err) {\n reject(err);\n }\n };\n\n xhr.onerror = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: sentRequestId || undefined,\n }),\n );\n };\n\n xhr.onabort = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n\n xhr.send(body);\n });\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- let e=require("react/jsx-runtime");function t({component:t,onSuccess:n,onError:r,locale:i,theme:a,text:o,shape:s,size:c,disableOneTap:l,width:u,className:d,style:f}){return(0,e.jsx)(`div`,{className:d,style:f,children:t({onSuccess:e=>{if(!e.credential)r?.({provider:`google`,message:`Google returned no credential`,raw:e});else return n({idToken:e.credential,provider:`google`,raw:e})},onError:()=>{r?.({provider:`google`,message:`Google login failed`})},locale:i,theme:a,text:o,shape:s,size:c,useOneTap:!l,width:u})})}exports.GoogleSignIn=t;
1
+ let e=require("react/jsx-runtime");function t({component:t,onSuccess:n,onError:r,locale:i,theme:a,text:o,shape:s,size:c,disableOneTap:l,width:u,nonce:d,className:f,style:p}){return(0,e.jsx)(`div`,{className:f,style:p,children:t({onSuccess:e=>{if(!e.credential)r?.({provider:`google`,message:`Google returned no credential`,raw:e});else return n({idToken:e.credential,provider:`google`,raw:e})},onError:()=>{r?.({provider:`google`,message:`Google login failed`})},locale:i,theme:a,text:o,shape:s,size:c,useOneTap:!l,width:u,nonce:d})})}exports.GoogleSignIn=t;
2
2
  //# sourceMappingURL=GoogleSignIn.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"GoogleSignIn.cjs","names":["react_jsx_runtime","GoogleSignIn","Component","response","onError","onSuccess","disableOneTap"],"sources":["../../src/oauth/GoogleSignIn.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — component, theme, text, shape, size and width are\n * Google's own button parameters, passed through with their names intact so the\n * reader can match them to Google's documentation;\n * onSuccess/onError/locale/disableOneTap are the wrapper's.\n */\nimport type { CSSProperties, ReactNode } from \"react\";\nimport type { OAuthCredential, OAuthError } from \"./types\";\n\nexport type GoogleSignInTheme = \"filled_blue\" | \"filled_black\" | \"outline\";\nexport type GoogleSignInText = \"signin_with\" | \"signup_with\" | \"continue_with\" | \"signin\";\nexport type GoogleSignInShape = \"rectangular\" | \"pill\" | \"circle\" | \"square\";\nexport type GoogleSignInSize = \"large\" | \"medium\" | \"small\";\n\nexport interface GoogleSignInProps {\n /**\n * `GoogleLogin` component from `@react-oauth/google`. The caller imports\n * it and passes it through so the SDK doesn't take `@react-oauth/google`\n * as a peer dep.\n */\n component: (props: Record<string, unknown>) => ReactNode;\n /** Fired with the validated credential on success. */\n onSuccess: (credential: OAuthCredential) => void | Promise<void>;\n /** Fired with a normalised error on failure. */\n onError?: (error: OAuthError) => void;\n /** Optional locale override (e.g. `\"pt-BR\"`). Falls back to browser locale. */\n locale?: string;\n /** Visual theme — passed through to `<GoogleLogin>`. */\n theme?: GoogleSignInTheme;\n /** Button text variant. */\n text?: GoogleSignInText;\n /** Button shape. */\n shape?: GoogleSignInShape;\n /** Button size. */\n size?: GoogleSignInSize;\n /** Disable Google's \"One Tap\" auto-prompt. Default `false`. */\n disableOneTap?: boolean;\n /** Optional `width` override (px) — Google's button is fixed-width. */\n width?: number;\n /** Optional className applied to the wrapper. */\n className?: string;\n /** Optional inline style applied to the wrapper. */\n style?: CSSProperties;\n}\n\n/**\n * Thin wrapper over `@react-oauth/google`'s `<GoogleLogin>` that:\n *\n * 1. Normalises the success payload into [[OAuthCredential]] (`idToken`,\n * `provider: \"google\"`, `raw`).\n * 2. Normalises errors into [[OAuthError]].\n * 3. Lets you pass `GoogleLogin` via the `component` prop, so the SDK does\n * not declare `@react-oauth/google` as a peer dep — apps that don't use\n * Google never pay for it.\n *\n * @example\n * import { GoogleLogin, GoogleOAuthProvider } from \"@react-oauth/google\";\n * import { GoogleSignIn } from \"tempest-react-sdk\";\n *\n * <GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>\n * <GoogleSignIn\n * component={GoogleLogin}\n * onSuccess={async ({ idToken }) => {\n * await api.post(\"/auth/google\", { body: { id_token: idToken } });\n * }}\n * onError={(err) => toast.error(err.message)}\n * />\n * </GoogleOAuthProvider>\n */\nexport function GoogleSignIn({\n component: Component,\n onSuccess,\n onError,\n locale,\n theme,\n text,\n shape,\n size,\n disableOneTap,\n width,\n className,\n style,\n}: GoogleSignInProps) {\n return (\n <div className={className} style={style}>\n {Component({\n onSuccess: (response: { credential?: string }) => {\n if (!response.credential) {\n onError?.({\n provider: \"google\",\n message: \"Google returned no credential\",\n raw: response,\n });\n return;\n }\n return onSuccess({\n idToken: response.credential,\n provider: \"google\",\n raw: response,\n });\n },\n onError: () => {\n onError?.({\n provider: \"google\",\n message: \"Google login failed\",\n });\n },\n locale,\n theme,\n text,\n shape,\n size,\n useOneTap: !disableOneTap,\n width,\n })}\n </div>\n );\n}\n"],"mappings":"A,mCAqEA,SAAgBC,EAAa,CACzB,UAAWC,EACX,YACA,UACA,SACA,QACA,OACA,QACA,OACA,gBACA,QACA,YACA,SACkB,CAClB,OACI,EAAAF,EAAA,IAAA,CAAC,MAAD,CAAgB,YAAkB,QAC7B,SAAAE,EAAU,CACP,UAAY,GAAsC,CAC9C,GAAI,CAACC,EAAS,WACVC,IAAU,CACN,SAAU,SACV,QAAS,gCACT,IAAKD,CACT,CAAC,OAGL,OAAOE,EAAU,CACb,QAASF,EAAS,WAClB,SAAU,SACV,IAAKA,CACT,CAAC,CACL,EACA,YAAe,CACXC,IAAU,CACN,SAAU,SACV,QAAS,qBACb,CAAC,CACL,EACA,SACA,QACA,OACA,QACA,OACA,UAAW,CAACE,EACZ,OACJ,CAAC,CACA,CAAA,CAEb,C"}
1
+ {"version":3,"file":"GoogleSignIn.cjs","names":["react_jsx_runtime","GoogleSignIn","Component","response","onError","onSuccess","disableOneTap"],"sources":["../../src/oauth/GoogleSignIn.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — component, theme, text, shape, size and width are\n * Google's own button parameters, and nonce Google's ID-token parameter, passed\n * through with their names intact so the reader can match them to Google's\n * documentation; onSuccess/onError/locale/disableOneTap are the wrapper's.\n */\nimport type { CSSProperties, ReactNode } from \"react\";\nimport type { OAuthCredential, OAuthError } from \"./types\";\n\nexport type GoogleSignInTheme = \"filled_blue\" | \"filled_black\" | \"outline\";\nexport type GoogleSignInText = \"signin_with\" | \"signup_with\" | \"continue_with\" | \"signin\";\nexport type GoogleSignInShape = \"rectangular\" | \"pill\" | \"circle\" | \"square\";\nexport type GoogleSignInSize = \"large\" | \"medium\" | \"small\";\n\nexport interface GoogleSignInProps {\n /**\n * `GoogleLogin` component from `@react-oauth/google`. The caller imports\n * it and passes it through so the SDK doesn't take `@react-oauth/google`\n * as a peer dep.\n */\n component: (props: Record<string, unknown>) => ReactNode;\n /** Fired with the validated credential on success. */\n onSuccess: (credential: OAuthCredential) => void | Promise<void>;\n /** Fired with a normalised error on failure. */\n onError?: (error: OAuthError) => void;\n /** Optional locale override (e.g. `\"pt-BR\"`). Falls back to browser locale. */\n locale?: string;\n /** Visual theme — passed through to `<GoogleLogin>`. */\n theme?: GoogleSignInTheme;\n /** Button text variant. */\n text?: GoogleSignInText;\n /** Button shape. */\n shape?: GoogleSignInShape;\n /** Button size. */\n size?: GoogleSignInSize;\n /** Disable Google's \"One Tap\" auto-prompt. Default `false`. */\n disableOneTap?: boolean;\n /** Optional `width` override (px) — Google's button is fixed-width. */\n width?: number;\n /**\n * Server-issued nonce Google copies into the ID token's `nonce` claim.\n *\n * The backend issues one per sign-in attempt, keeps it, and rejects an\n * `id_token` whose claim does not match — which is what stops a token\n * captured elsewhere from being replayed against your API. Until 0.82.0\n * the wrapper dropped it, so an app could not bind the token through\n * `<GoogleSignIn>` at all (#530).\n */\n nonce?: string;\n /** Optional className applied to the wrapper. */\n className?: string;\n /** Optional inline style applied to the wrapper. */\n style?: CSSProperties;\n}\n\n/**\n * Thin wrapper over `@react-oauth/google`'s `<GoogleLogin>` that:\n *\n * 1. Normalises the success payload into [[OAuthCredential]] (`idToken`,\n * `provider: \"google\"`, `raw`).\n * 2. Normalises errors into [[OAuthError]].\n * 3. Lets you pass `GoogleLogin` via the `component` prop, so the SDK does\n * not declare `@react-oauth/google` as a peer dep — apps that don't use\n * Google never pay for it.\n *\n * @example\n * import { GoogleLogin, GoogleOAuthProvider } from \"@react-oauth/google\";\n * import { GoogleSignIn } from \"tempest-react-sdk\";\n *\n * <GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>\n * <GoogleSignIn\n * component={GoogleLogin}\n * onSuccess={async ({ idToken }) => {\n * await api.post(\"/auth/google\", { body: { id_token: idToken } });\n * }}\n * onError={(err) => toast.error(err.message)}\n * />\n * </GoogleOAuthProvider>\n */\nexport function GoogleSignIn({\n component: Component,\n onSuccess,\n onError,\n locale,\n theme,\n text,\n shape,\n size,\n disableOneTap,\n width,\n nonce,\n className,\n style,\n}: GoogleSignInProps) {\n return (\n <div className={className} style={style}>\n {Component({\n onSuccess: (response: { credential?: string }) => {\n if (!response.credential) {\n onError?.({\n provider: \"google\",\n message: \"Google returned no credential\",\n raw: response,\n });\n return;\n }\n return onSuccess({\n idToken: response.credential,\n provider: \"google\",\n raw: response,\n });\n },\n onError: () => {\n onError?.({\n provider: \"google\",\n message: \"Google login failed\",\n });\n },\n locale,\n theme,\n text,\n shape,\n size,\n useOneTap: !disableOneTap,\n width,\n nonce,\n })}\n </div>\n );\n}\n"],"mappings":"A,mCA+EA,SAAgBC,EAAa,CACzB,UAAWC,EACX,YACA,UACA,SACA,QACA,OACA,QACA,OACA,gBACA,QACA,QACA,YACA,SACkB,CAClB,OACI,EAAAF,EAAA,IAAA,CAAC,MAAD,CAAgB,YAAkB,QAC7B,SAAAE,EAAU,CACP,UAAY,GAAsC,CAC9C,GAAI,CAACC,EAAS,WACVC,IAAU,CACN,SAAU,SACV,QAAS,gCACT,IAAKD,CACT,CAAC,OAGL,OAAOE,EAAU,CACb,QAASF,EAAS,WAClB,SAAU,SACV,IAAKA,CACT,CAAC,CACL,EACA,YAAe,CACXC,IAAU,CACN,SAAU,SACV,QAAS,qBACb,CAAC,CACL,EACA,SACA,QACA,OACA,QACA,OACA,UAAW,CAACE,EACZ,QACA,OACJ,CAAC,CACA,CAAA,CAEb,C"}
@@ -1,9 +1,9 @@
1
1
  import { jsx as e } from "react/jsx-runtime";
2
2
  //#region src/oauth/GoogleSignIn.tsx
3
- function t({ component: t, onSuccess: n, onError: r, locale: i, theme: a, text: o, shape: s, size: c, disableOneTap: l, width: u, className: d, style: f }) {
3
+ function t({ component: t, onSuccess: n, onError: r, locale: i, theme: a, text: o, shape: s, size: c, disableOneTap: l, width: u, nonce: d, className: f, style: p }) {
4
4
  return /* @__PURE__ */ e("div", {
5
- className: d,
6
- style: f,
5
+ className: f,
6
+ style: p,
7
7
  children: t({
8
8
  onSuccess: (e) => {
9
9
  if (!e.credential) r?.({
@@ -29,7 +29,8 @@ function t({ component: t, onSuccess: n, onError: r, locale: i, theme: a, text:
29
29
  shape: s,
30
30
  size: c,
31
31
  useOneTap: !l,
32
- width: u
32
+ width: u,
33
+ nonce: d
33
34
  })
34
35
  });
35
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GoogleSignIn.js","names":["GoogleSignIn","Component","response","onSuccess","disableOneTap"],"sources":["../../src/oauth/GoogleSignIn.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — component, theme, text, shape, size and width are\n * Google's own button parameters, passed through with their names intact so the\n * reader can match them to Google's documentation;\n * onSuccess/onError/locale/disableOneTap are the wrapper's.\n */\nimport type { CSSProperties, ReactNode } from \"react\";\nimport type { OAuthCredential, OAuthError } from \"./types\";\n\nexport type GoogleSignInTheme = \"filled_blue\" | \"filled_black\" | \"outline\";\nexport type GoogleSignInText = \"signin_with\" | \"signup_with\" | \"continue_with\" | \"signin\";\nexport type GoogleSignInShape = \"rectangular\" | \"pill\" | \"circle\" | \"square\";\nexport type GoogleSignInSize = \"large\" | \"medium\" | \"small\";\n\nexport interface GoogleSignInProps {\n /**\n * `GoogleLogin` component from `@react-oauth/google`. The caller imports\n * it and passes it through so the SDK doesn't take `@react-oauth/google`\n * as a peer dep.\n */\n component: (props: Record<string, unknown>) => ReactNode;\n /** Fired with the validated credential on success. */\n onSuccess: (credential: OAuthCredential) => void | Promise<void>;\n /** Fired with a normalised error on failure. */\n onError?: (error: OAuthError) => void;\n /** Optional locale override (e.g. `\"pt-BR\"`). Falls back to browser locale. */\n locale?: string;\n /** Visual theme — passed through to `<GoogleLogin>`. */\n theme?: GoogleSignInTheme;\n /** Button text variant. */\n text?: GoogleSignInText;\n /** Button shape. */\n shape?: GoogleSignInShape;\n /** Button size. */\n size?: GoogleSignInSize;\n /** Disable Google's \"One Tap\" auto-prompt. Default `false`. */\n disableOneTap?: boolean;\n /** Optional `width` override (px) — Google's button is fixed-width. */\n width?: number;\n /** Optional className applied to the wrapper. */\n className?: string;\n /** Optional inline style applied to the wrapper. */\n style?: CSSProperties;\n}\n\n/**\n * Thin wrapper over `@react-oauth/google`'s `<GoogleLogin>` that:\n *\n * 1. Normalises the success payload into [[OAuthCredential]] (`idToken`,\n * `provider: \"google\"`, `raw`).\n * 2. Normalises errors into [[OAuthError]].\n * 3. Lets you pass `GoogleLogin` via the `component` prop, so the SDK does\n * not declare `@react-oauth/google` as a peer dep — apps that don't use\n * Google never pay for it.\n *\n * @example\n * import { GoogleLogin, GoogleOAuthProvider } from \"@react-oauth/google\";\n * import { GoogleSignIn } from \"tempest-react-sdk\";\n *\n * <GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>\n * <GoogleSignIn\n * component={GoogleLogin}\n * onSuccess={async ({ idToken }) => {\n * await api.post(\"/auth/google\", { body: { id_token: idToken } });\n * }}\n * onError={(err) => toast.error(err.message)}\n * />\n * </GoogleOAuthProvider>\n */\nexport function GoogleSignIn({\n component: Component,\n onSuccess,\n onError,\n locale,\n theme,\n text,\n shape,\n size,\n disableOneTap,\n width,\n className,\n style,\n}: GoogleSignInProps) {\n return (\n <div className={className} style={style}>\n {Component({\n onSuccess: (response: { credential?: string }) => {\n if (!response.credential) {\n onError?.({\n provider: \"google\",\n message: \"Google returned no credential\",\n raw: response,\n });\n return;\n }\n return onSuccess({\n idToken: response.credential,\n provider: \"google\",\n raw: response,\n });\n },\n onError: () => {\n onError?.({\n provider: \"google\",\n message: \"Google login failed\",\n });\n },\n locale,\n theme,\n text,\n shape,\n size,\n useOneTap: !disableOneTap,\n width,\n })}\n </div>\n );\n}\n"],"mappings":";;AAqEA,SAAgBA,EAAa,EACzB,WAAWC,GACX,cACA,YACA,WACA,UACA,SACA,UACA,SACA,kBACA,UACA,cACA,YACkB;CAClB,OACI,kBAAC,OAAD;EAAgB;EAAkB;EAC7B,UAAAA,EAAU;GACP,YAAY,MAAsC;IAC9C,IAAI,CAACC,EAAS,YACV,IAAU;KACN,UAAU;KACV,SAAS;KACT,KAAKA;IACT,CAAC;SAGL,OAAOC,EAAU;KACb,SAASD,EAAS;KAClB,UAAU;KACV,KAAKA;IACT,CAAC;GACL;GACA,eAAe;IACX,IAAU;KACN,UAAU;KACV,SAAS;IACb,CAAC;GACL;GACA;GACA;GACA;GACA;GACA;GACA,WAAW,CAACE;GACZ;EACJ,CAAC;CACA,CAAA;AAEb"}
1
+ {"version":3,"file":"GoogleSignIn.js","names":["GoogleSignIn","Component","response","onSuccess","disableOneTap"],"sources":["../../src/oauth/GoogleSignIn.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — component, theme, text, shape, size and width are\n * Google's own button parameters, and nonce Google's ID-token parameter, passed\n * through with their names intact so the reader can match them to Google's\n * documentation; onSuccess/onError/locale/disableOneTap are the wrapper's.\n */\nimport type { CSSProperties, ReactNode } from \"react\";\nimport type { OAuthCredential, OAuthError } from \"./types\";\n\nexport type GoogleSignInTheme = \"filled_blue\" | \"filled_black\" | \"outline\";\nexport type GoogleSignInText = \"signin_with\" | \"signup_with\" | \"continue_with\" | \"signin\";\nexport type GoogleSignInShape = \"rectangular\" | \"pill\" | \"circle\" | \"square\";\nexport type GoogleSignInSize = \"large\" | \"medium\" | \"small\";\n\nexport interface GoogleSignInProps {\n /**\n * `GoogleLogin` component from `@react-oauth/google`. The caller imports\n * it and passes it through so the SDK doesn't take `@react-oauth/google`\n * as a peer dep.\n */\n component: (props: Record<string, unknown>) => ReactNode;\n /** Fired with the validated credential on success. */\n onSuccess: (credential: OAuthCredential) => void | Promise<void>;\n /** Fired with a normalised error on failure. */\n onError?: (error: OAuthError) => void;\n /** Optional locale override (e.g. `\"pt-BR\"`). Falls back to browser locale. */\n locale?: string;\n /** Visual theme — passed through to `<GoogleLogin>`. */\n theme?: GoogleSignInTheme;\n /** Button text variant. */\n text?: GoogleSignInText;\n /** Button shape. */\n shape?: GoogleSignInShape;\n /** Button size. */\n size?: GoogleSignInSize;\n /** Disable Google's \"One Tap\" auto-prompt. Default `false`. */\n disableOneTap?: boolean;\n /** Optional `width` override (px) — Google's button is fixed-width. */\n width?: number;\n /**\n * Server-issued nonce Google copies into the ID token's `nonce` claim.\n *\n * The backend issues one per sign-in attempt, keeps it, and rejects an\n * `id_token` whose claim does not match — which is what stops a token\n * captured elsewhere from being replayed against your API. Until 0.82.0\n * the wrapper dropped it, so an app could not bind the token through\n * `<GoogleSignIn>` at all (#530).\n */\n nonce?: string;\n /** Optional className applied to the wrapper. */\n className?: string;\n /** Optional inline style applied to the wrapper. */\n style?: CSSProperties;\n}\n\n/**\n * Thin wrapper over `@react-oauth/google`'s `<GoogleLogin>` that:\n *\n * 1. Normalises the success payload into [[OAuthCredential]] (`idToken`,\n * `provider: \"google\"`, `raw`).\n * 2. Normalises errors into [[OAuthError]].\n * 3. Lets you pass `GoogleLogin` via the `component` prop, so the SDK does\n * not declare `@react-oauth/google` as a peer dep — apps that don't use\n * Google never pay for it.\n *\n * @example\n * import { GoogleLogin, GoogleOAuthProvider } from \"@react-oauth/google\";\n * import { GoogleSignIn } from \"tempest-react-sdk\";\n *\n * <GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>\n * <GoogleSignIn\n * component={GoogleLogin}\n * onSuccess={async ({ idToken }) => {\n * await api.post(\"/auth/google\", { body: { id_token: idToken } });\n * }}\n * onError={(err) => toast.error(err.message)}\n * />\n * </GoogleOAuthProvider>\n */\nexport function GoogleSignIn({\n component: Component,\n onSuccess,\n onError,\n locale,\n theme,\n text,\n shape,\n size,\n disableOneTap,\n width,\n nonce,\n className,\n style,\n}: GoogleSignInProps) {\n return (\n <div className={className} style={style}>\n {Component({\n onSuccess: (response: { credential?: string }) => {\n if (!response.credential) {\n onError?.({\n provider: \"google\",\n message: \"Google returned no credential\",\n raw: response,\n });\n return;\n }\n return onSuccess({\n idToken: response.credential,\n provider: \"google\",\n raw: response,\n });\n },\n onError: () => {\n onError?.({\n provider: \"google\",\n message: \"Google login failed\",\n });\n },\n locale,\n theme,\n text,\n shape,\n size,\n useOneTap: !disableOneTap,\n width,\n nonce,\n })}\n </div>\n );\n}\n"],"mappings":";;AA+EA,SAAgBA,EAAa,EACzB,WAAWC,GACX,cACA,YACA,WACA,UACA,SACA,UACA,SACA,kBACA,UACA,UACA,cACA,YACkB;CAClB,OACI,kBAAC,OAAD;EAAgB;EAAkB;EAC7B,UAAAA,EAAU;GACP,YAAY,MAAsC;IAC9C,IAAI,CAACC,EAAS,YACV,IAAU;KACN,UAAU;KACV,SAAS;KACT,KAAKA;IACT,CAAC;SAGL,OAAOC,EAAU;KACb,SAASD,EAAS;KAClB,UAAU;KACV,KAAKA;IACT,CAAC;GACL;GACA,eAAe;IACX,IAAU;KACN,UAAU;KACV,SAAS;IACb,CAAC;GACL;GACA;GACA;GACA;GACA;GACA;GACA,WAAW,CAACE;GACZ;GACA;EACJ,CAAC;CACA,CAAA;AAEb"}
@@ -1,2 +1,2 @@
1
- const e=require("../utils/ids.cjs"),t=require("./create-offline-store.cjs");function n(e){let t=typeof localStorage<`u`;return{get:()=>t?localStorage.getItem(e):null,set:n=>{t&&localStorage.setItem(e,n)},clear:()=>{t&&localStorage.removeItem(e)}}}function r(r){let i=t.createOfflineStore({databaseName:r.databaseName,version:r.version??1,tableName:r.tableName??`outbox`,indexes:`&id, recordId, enqueuedAt`,keyPath:`id`}),a=`get`in r.watermark?r.watermark:n(r.watermark.storageKey),o=r.idPrefix??`outbox`,s=r.isOnline??(()=>typeof navigator<`u`?navigator.onLine:!0),c=null,l=0,u=new Set,d={phase:`idle`,pending:0,lastSummary:null,lastError:null,lastSyncedAt:null},f=r.crossTab===!0,p=r.broadcastChannelName??`tempest-sync:${r.databaseName}`,m=f&&typeof BroadcastChannel<`u`?new BroadcastChannel(p):null,h=!1;function g(){for(let e of u)e(d)}function _(e){d={...d,...e},g(),m&&!h&&m.postMessage(d)}m&&(m.onmessage=e=>{h||(d=e.data,g())});async function v(){_({pending:await i.count()})}function y(){l+=1,c&&(c=null,_({phase:`idle`}))}async function b(e,t){let n=await i.list(void 0,{orderBy:`enqueuedAt`}),a=new Set;for(let o of n){if(a.has(o.recordId)){e.deferred+=1;continue}let n=await i.get(o.id);if(l!==t)return!1;if(n)try{if(await r.deliver(o),await i.delete(o.id),e.succeeded+=1,l!==t)return!1;await r.onEntryDelivered?.(o)}catch(n){let s=n instanceof Error?n.message:`delivery failed`;if(await i.update(o.id,{attempts:o.attempts+1,lastError:s}),a.add(o.recordId),e.failed+=1,e.lastError=s,l!==t)return!1;await r.onEntryFailed?.(o,n)}}return l===t}async function x(e){let t=a.get(),n=null,i;do{let a=await r.pullPage(t,n);for(let t of a.items){if(l!==e)return!1;await r.applyRemote(t)}if(l!==e)return!1;n=a.nextCursor,i=a.serverTime}while(n);return i&&a.set(i),!0}async function S(e,t){let n=Date.now(),r={trigger:e,succeeded:0,failed:0,deferred:0,durationMs:0,skipped:!1,lastError:null};return s()?(await b(r,t)&&await x(t)||(r.interrupted=!0),r.durationMs=Date.now()-n,r):(r.skipped=!0,r.durationMs=Date.now()-n,r)}async function C(e,t){let n=typeof navigator<`u`?navigator.locks:void 0;return!f||!n?S(e,t):await n.request(`${p}:flush`,{ifAvailable:!0},async n=>n?S(e,t):null)??{trigger:e,succeeded:0,failed:0,deferred:0,durationMs:0,skipped:!1,lastError:null}}v();let w=0;function T(){let e=Date.now();return w=e>w?e:w+1,w}return{async enqueue(t,n,r){let a={id:e.randomId(o),op:t,recordId:n,enqueuedAt:T(),attempts:0,payload:r};return await i.put(a),await v(),a.id},flush(e=`manual`){if(!c){_({phase:`syncing`});let t=l,n=C(e,t).then(async e=>(await v(),l===t&&_({phase:e.skipped?`offline`:e.failed>0?`error`:`idle`,lastSummary:e,lastError:e.lastError,lastSyncedAt:e.skipped?d.lastSyncedAt:Date.now()}),e)).finally(()=>{c===n&&(c=null)});c=n}return c},pendingCount(){return i.count()},listPending(){return i.list(void 0,{orderBy:`enqueuedAt`})},async clearOutbox(){y(),await i.clear(),await v()},resetWatermark(){y(),a.clear()},getState(){return d},subscribe(e){return u.add(e),()=>{u.delete(e)}},dispose(){h=!0,m?.close()}}}exports.createOfflineSync=r;
1
+ const e=require("../utils/ids.cjs"),t=require("../utils/owner-fingerprint.cjs"),n=require("./create-offline-store.cjs");function r(e){let t=typeof localStorage<`u`;return{get:()=>t?localStorage.getItem(e):null,set:n=>{t&&localStorage.setItem(e,n)},clear:()=>{t&&localStorage.removeItem(e)}}}function i(i){let a=n.createOfflineStore({databaseName:i.databaseName,version:i.version??1,tableName:i.tableName??`outbox`,indexes:`&id, recordId, enqueuedAt`,keyPath:`id`}),o=`get`in i.watermark?i.watermark:r(i.watermark.storageKey),s=i.idPrefix??`outbox`,c=i.isOnline??(()=>typeof navigator<`u`?navigator.onLine:!0),{getPrincipal:l}=i;function u(e){try{return t.ownerFingerprint(`p`,e())}catch{return null}}function d(e){if(!l||e.owner===void 0)return`deliver`;let t=u(l);return t===e.owner?`deliver`:t===null?`hold`:`drop`}let f=null,p=0,m=new Set,h={phase:`idle`,pending:0,lastSummary:null,lastError:null,lastSyncedAt:null},g=i.crossTab===!0,_=i.broadcastChannelName??`tempest-sync:${i.databaseName}`,v=g&&typeof BroadcastChannel<`u`?new BroadcastChannel(_):null,y=!1;function b(){for(let e of m)e(h)}function x(e){h={...h,...e},b(),v&&!y&&v.postMessage(h)}v&&(v.onmessage=e=>{y||(h=e.data,b())});async function S(){x({pending:await a.count()})}function C(){p+=1,f&&(f=null,x({phase:`idle`}))}async function w(e,t){let n=await a.list(void 0,{orderBy:`enqueuedAt`}),r=new Set;for(let o of n){if(r.has(o.recordId)){e.deferred+=1;continue}let n=await a.get(o.id);if(p!==t)return!1;if(!n)continue;let s=d(o);if(s===`hold`)e.held=(e.held??0)+1;else if(s===`drop`)await a.delete(o.id),e.dropped=(e.dropped??0)+1;else try{if(await i.deliver(o),await a.delete(o.id),e.succeeded+=1,p!==t)return!1;await i.onEntryDelivered?.(o)}catch(n){let s=n instanceof Error?n.message:`delivery failed`;if(await a.update(o.id,{attempts:o.attempts+1,lastError:s}),r.add(o.recordId),e.failed+=1,e.lastError=s,p!==t)return!1;await i.onEntryFailed?.(o,n)}}return p===t}async function T(e){let t=o.get(),n=null,r;do{let a=await i.pullPage(t,n);for(let t of a.items){if(p!==e)return!1;await i.applyRemote(t)}if(p!==e)return!1;n=a.nextCursor,r=a.serverTime}while(n);return r&&o.set(r),!0}async function E(e,t){let n=Date.now(),r={trigger:e,succeeded:0,failed:0,deferred:0,durationMs:0,skipped:!1,lastError:null};return c()?(await w(r,t)&&await T(t)||(r.interrupted=!0),r.durationMs=Date.now()-n,r):(r.skipped=!0,r.durationMs=Date.now()-n,r)}async function D(e,t){let n=typeof navigator<`u`?navigator.locks:void 0;return!g||!n?E(e,t):await n.request(`${_}:flush`,{ifAvailable:!0},async n=>n?E(e,t):null)??{trigger:e,succeeded:0,failed:0,deferred:0,durationMs:0,skipped:!1,lastError:null}}S();let O=0;function k(){let e=Date.now();return O=e>O?e:O+1,O}return{async enqueue(t,n,r){let i={id:e.randomId(s),op:t,recordId:n,enqueuedAt:k(),attempts:0,payload:r};return l&&(i.owner=u(l)),await a.put(i),await S(),i.id},flush(e=`manual`){if(!f){x({phase:`syncing`});let t=p,n=D(e,t).then(async e=>(await S(),p===t&&x({phase:e.skipped?`offline`:e.failed>0?`error`:`idle`,lastSummary:e,lastError:e.lastError,lastSyncedAt:e.skipped?h.lastSyncedAt:Date.now()}),e)).finally(()=>{f===n&&(f=null)});f=n}return f},pendingCount(){return a.count()},listPending(){return a.list(void 0,{orderBy:`enqueuedAt`})},async clearOutbox(){C(),await a.clear(),await S()},resetWatermark(){C(),o.clear()},getState(){return h},subscribe(e){return m.add(e),()=>{m.delete(e)}},dispose(){y=!0,v?.close()}}}exports.createOfflineSync=i;
2
2
  //# sourceMappingURL=create-offline-sync.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-offline-sync.cjs","names":["createOfflineStore","randomId","require_ids","require_create_offline_store","localStorageWatermark","available","key","value","createOfflineSync","outbox","config","watermark","idPrefix","isOnline","inflight","generation","listeners","state","crossTab","channelName","channel","closed","notify","listener","setState","patch","event","refreshPending","endSession","push","entries","blocked","entry","stillQueued","runGeneration","cause","message","pull","since","cursor","serverTime","page","item","runOnce","startedAt","summary","runGuarded","locks","trigger","lock","lastEnqueuedAt","nextEnqueuedAt","now","run"],"sources":["../../src/offline/create-offline-sync.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — outbox drain, pull-with-watermark,\n * conflict resolution and the single-flight guard are one loop by necessity: a pull\n * that lands mid-drain must not advance the watermark past records the outbox has\n * not pushed yet. Splitting the phases hands that ordering to whoever calls them in\n * the right sequence.\n */\nimport { randomId } from \"../utils/ids\";\nimport { createOfflineStore } from \"./create-offline-store\";\n\n/**\n * Mutation kinds queued in the outbox.\n *\n * `create`/`update` carry a snapshot of the record; `delete` needs only the\n * record id. The distinction between `create` and `update` is advisory — the\n * engine treats both as \"deliver this record\" and leaves the create-vs-update\n * decision to the app's `deliver` callback (a `PUT` upsert usually ignores it).\n */\nexport type OutboxOp = \"create\" | \"update\" | \"delete\";\n\n/**\n * A single queued mutation.\n *\n * @typeParam TPayload - The record snapshot shape carried by\n * `create`/`update` entries (omitted for `delete`).\n */\nexport interface OutboxEntry<TPayload = unknown> {\n /** Stable per-entry id (generated with {@link randomId}). */\n id: string;\n /** The mutation kind. */\n op: OutboxOp;\n /** Primary key of the record the mutation targets. */\n recordId: string;\n /**\n * Epoch milliseconds when the mutation was queued, and the FIFO sort key for\n * `listPending()` and the delivery loop. Strictly increasing per engine\n * instance: entries queued inside the same millisecond are spaced by 1ms so\n * the order never depends on how the index breaks a tie.\n */\n enqueuedAt: number;\n /** How many delivery attempts have been made so far. */\n attempts: number;\n /** Last delivery error message, kept for UI/debug. */\n lastError?: string;\n /** Record snapshot for `create`/`update`. Omitted for `delete`. */\n payload?: TPayload;\n}\n\n/**\n * Why a sync run was triggered. The listed values are the common ones; any\n * string is accepted so apps can add their own telemetry labels.\n */\nexport type SyncTrigger =\n \"boot\" | \"online-event\" | \"after-mutation\" | \"manual\" | \"interval\" | (string & {});\n\n/**\n * One page of the server delta pull.\n *\n * @typeParam TRemote - The server-side item shape.\n */\nexport interface PullPage<TRemote> {\n /** Items changed since the watermark, in this page. */\n items: TRemote[];\n /** Cursor for the next page, or `null` when this is the last page. */\n nextCursor: string | null;\n /**\n * Server clock to persist as the next watermark once the whole delta is\n * applied. `null` leaves the watermark unchanged.\n */\n serverTime: string | null;\n}\n\n/** Outcome of a single {@link OfflineSync.flush} run. */\nexport interface SyncRunSummary {\n /** The trigger passed to `flush`. */\n trigger: SyncTrigger;\n /** Entries delivered to the server this run. */\n succeeded: number;\n /** Entries that failed and stay queued for the next run. */\n failed: number;\n /**\n * Entries left untried because an earlier entry for the **same record**\n * failed this run.\n *\n * They stay queued and are attempted next run, in order. Counted apart from\n * `failed` because they were never sent: reporting them as failures would\n * blame the server for a decision this engine made, and folding them in would\n * make `succeeded + failed` stop accounting for the queue.\n */\n deferred: number;\n /** Total wall-clock milliseconds the run took. */\n durationMs: number;\n /** `true` when the run was skipped because the device was offline. */\n skipped: boolean;\n /** Last delivery error message seen this run, or `null` when none failed. */\n lastError: string | null;\n /**\n * `true` when {@link OfflineSync.clearOutbox} or {@link OfflineSync.resetWatermark}\n * ended the session while this run was in flight, so it stopped early.\n *\n * The counts cover only what happened before the stop: a delivery already in\n * progress at logout is still counted, but no later entry was sent, no further\n * page was pulled or applied, and the watermark was not written. Absent on a\n * run that went to completion.\n */\n interrupted?: true;\n}\n\n/**\n * Coarse lifecycle phase of the sync engine, surfaced through\n * {@link OfflineSync.getState} / {@link OfflineSync.subscribe} for reactive UI.\n *\n * - `\"idle\"` — nothing in flight and the last run (if any) fully succeeded.\n * - `\"syncing\"` — a `flush` run is currently in progress.\n * - `\"offline\"` — the last run was skipped because the device was offline.\n * - `\"error\"` — the last run left at least one entry queued after a failure.\n */\nexport type SyncPhase = \"idle\" | \"syncing\" | \"offline\" | \"error\";\n\n/**\n * Immutable snapshot of the engine's reactive state. A fresh object is emitted\n * to subscribers on every change, so it is safe to use as a\n * `useSyncExternalStore` snapshot (reference equality signals \"no change\").\n */\nexport interface SyncState {\n /** Current lifecycle phase. */\n phase: SyncPhase;\n /** Number of mutations still queued in the outbox. */\n pending: number;\n /** Summary of the most recent `flush` run, or `null` before the first. */\n lastSummary: SyncRunSummary | null;\n /** Message of the most recent delivery failure, or `null`. */\n lastError: string | null;\n /** Epoch ms of the last non-skipped run, or `null` before the first. */\n lastSyncedAt: number | null;\n}\n\n/**\n * Pluggable persistence for the pull watermark (the \"changed since\" cursor).\n * Pass an object with a `storageKey` to use a `localStorage`-backed default.\n */\nexport interface WatermarkStore {\n /** Read the current watermark, or `null` when none is stored. */\n get: () => string | null;\n /** Persist a new watermark. */\n set: (value: string) => void;\n /** Drop the watermark (e.g. on logout / account switch). */\n clear: () => void;\n}\n\n/**\n * Configuration for {@link createOfflineSync}.\n *\n * The engine owns the outbox, the single-flight flush, the offline guard, the\n * paginated pull loop and the watermark; the three transport callbacks\n * (`deliver`, `pullPage`, `applyRemote`) are where the app plugs in its own\n * endpoints, record shape and conflict resolution.\n *\n * @typeParam TPayload - Record snapshot carried by outbox entries.\n * @typeParam TRemote - Server item shape returned by `pullPage`.\n */\nexport interface OfflineSyncConfig<TPayload, TRemote> {\n /** IndexedDB database name for the outbox (kept separate per queue). */\n databaseName: string;\n /** Outbox object-store name. Default `\"outbox\"`. */\n tableName?: string;\n /** Outbox schema version. Default `1`. */\n version?: number;\n /** Prefix for generated entry ids. Default `\"outbox\"`. */\n idPrefix?: string;\n /**\n * Deliver one queued mutation to the server. Throwing keeps the entry\n * queued (its `attempts`/`lastError` are bumped) for the next run.\n */\n deliver: (entry: OutboxEntry<TPayload>) => Promise<void>;\n /** Fetch one page of the server delta since `since`, from `cursor`. */\n pullPage: (since: string | null, cursor: string | null) => Promise<PullPage<TRemote>>;\n /**\n * Merge one pulled item into the local store. The app owns conflict\n * resolution here (e.g. last-write-wins, keeping newer local pending\n * edits, resolving tombstones and downloading blobs).\n */\n applyRemote: (item: TRemote) => Promise<void>;\n /** Watermark persistence, or `{ storageKey }` for a `localStorage` default. */\n watermark: WatermarkStore | { storageKey: string };\n /** Called after an entry is delivered and acked. */\n onEntryDelivered?: (entry: OutboxEntry<TPayload>) => void | Promise<void>;\n /** Called after an entry fails delivery (it stays queued). */\n onEntryFailed?: (entry: OutboxEntry<TPayload>, error: unknown) => void | Promise<void>;\n /**\n * Connectivity check. Default reads `navigator.onLine` (always online in\n * non-browser environments). When it returns `false`, `flush` is skipped.\n */\n isOnline?: () => boolean;\n /**\n * Broadcast {@link SyncState} across browser tabs of the same origin via a\n * `BroadcastChannel`, so every tab shows a coherent pending count / phase\n * (e.g. one tab flushing drops the badge to zero everywhere). The outbox is\n * already a shared IndexedDB; this only shares the in-memory state. Silently\n * ignored where `BroadcastChannel` is unavailable. Default `false`.\n *\n * When the Web Locks API is available, `flush` is additionally coordinated\n * *across* tabs: only one tab runs at a time and others skip while the lock\n * is held (they pick up the result via the broadcast). Delivery should still\n * be idempotent (upsert by client id). Without Web Locks, `flush` falls back\n * to per-tab single-flight.\n */\n crossTab?: boolean;\n /** Channel name when `crossTab` is on. Default `tempest-sync:${databaseName}`. */\n broadcastChannelName?: string;\n}\n\n/**\n * Offline-first sync engine: a durable outbox plus a paginated delta pull.\n *\n * @typeParam TPayload - Record snapshot carried by outbox entries.\n */\nexport interface OfflineSync<TPayload> {\n /**\n * Queue a mutation. Returns the generated entry id.\n *\n * @param op - The mutation kind.\n * @param recordId - Primary key of the affected record.\n * @param payload - Record snapshot (for `create`/`update`).\n */\n enqueue: (op: OutboxOp, recordId: string, payload?: TPayload) => Promise<string>;\n /**\n * Run a full sync (drain the outbox, then pull the delta). Concurrent\n * calls share one in-flight promise, so triggers never overlap.\n *\n * @param trigger - Label for why the run happened. Default `\"manual\"`.\n */\n flush: (trigger?: SyncTrigger) => Promise<SyncRunSummary>;\n /** Number of mutations still queued. */\n pendingCount: () => Promise<number>;\n /** The queued mutations in FIFO order. */\n listPending: () => Promise<OutboxEntry<TPayload>[]>;\n /**\n * Drop every queued mutation (e.g. on logout).\n *\n * Also ends the current session for any `flush` already in flight: that run\n * delivers no further entry, pulls and applies nothing more and writes no\n * watermark, and resolves with `interrupted: true`. A `flush` called after this\n * starts a fresh run instead of joining the abandoned one.\n */\n clearOutbox: () => Promise<void>;\n /**\n * Reset the pull watermark (e.g. on logout / account switch).\n *\n * Ends the current session for an in-flight `flush` exactly like\n * {@link OfflineSync.clearOutbox}, so the run cannot write the ended\n * session's watermark back after the reset.\n */\n resetWatermark: () => void;\n /** Read the current reactive {@link SyncState} snapshot synchronously. */\n getState: () => SyncState;\n /**\n * Subscribe to {@link SyncState} changes. The listener fires on every\n * enqueue, flush transition and outbox clear.\n *\n * @param listener - Called with the new snapshot on each change.\n * @returns An unsubscribe function.\n */\n subscribe: (listener: (state: SyncState) => void) => () => void;\n /**\n * Release resources held by the engine — currently the cross-tab\n * `BroadcastChannel` when `crossTab` is enabled. Safe to call when none was\n * opened. Optional in long-lived apps (the channel is reclaimed on tab\n * close); call it in tests and on teardown.\n */\n dispose: () => void;\n}\n\n/**\n * Build a `localStorage`-backed {@link WatermarkStore}. Reads/writes are no-ops\n * when `localStorage` is unavailable (SSR / non-browser).\n *\n * @param key - The `localStorage` key.\n * @returns A watermark store persisting to `localStorage`.\n */\nfunction localStorageWatermark(key: string): WatermarkStore {\n const available = typeof localStorage !== \"undefined\";\n return {\n get: () => (available ? localStorage.getItem(key) : null),\n set: (value: string) => {\n if (available) localStorage.setItem(key, value);\n },\n clear: () => {\n if (available) localStorage.removeItem(key);\n },\n };\n}\n\n/**\n * Create an offline-first sync engine over an IndexedDB outbox.\n *\n * The engine drains queued mutations to the server (`deliver`) and pulls the\n * server delta back (`pullPage` + `applyRemote`), advancing a watermark so each\n * run only fetches what changed. `flush` is single-flight and skips cleanly\n * while offline; failed deliveries stay queued with their attempt count bumped.\n *\n * Dexie is an **optional peer dependency** (via {@link createOfflineStore}) —\n * install it (`npm i dexie`) when you use this engine.\n *\n * @typeParam TPayload - Record snapshot carried by outbox entries.\n * @typeParam TRemote - Server item shape returned by `pullPage`.\n * @param config - Transport callbacks + outbox/watermark configuration.\n * @returns The sync handle (`enqueue`, `flush`, `pendingCount`, …).\n *\n * @example\n * const sync = createOfflineSync<Note, NoteDto>({\n * databaseName: \"NotesOutbox\",\n * watermark: { storageKey: \"notes.watermark\" },\n * deliver: async (entry) => {\n * if (entry.op === \"delete\") return api.remove(entry.recordId);\n * await api.upsert(entry.recordId, entry.payload!);\n * },\n * pullPage: async (since, cursor) => {\n * const page = await api.changes(since, cursor);\n * return { items: page.items, nextCursor: page.next, serverTime: page.now };\n * },\n * applyRemote: async (dto) => {\n * if (dto.deleted) return localStore.remove(dto.id);\n * await localStore.save(fromDto(dto));\n * },\n * });\n * await sync.enqueue(\"create\", note.id, note);\n * await sync.flush(\"after-mutation\");\n */\nexport function createOfflineSync<TPayload = unknown, TRemote = unknown>(\n config: OfflineSyncConfig<TPayload, TRemote>,\n): OfflineSync<TPayload> {\n const outbox = createOfflineStore<OutboxEntry<TPayload>, string>({\n databaseName: config.databaseName,\n version: config.version ?? 1,\n tableName: config.tableName ?? \"outbox\",\n indexes: \"&id, recordId, enqueuedAt\",\n keyPath: \"id\",\n });\n\n const watermark: WatermarkStore =\n \"get\" in config.watermark\n ? config.watermark\n : localStorageWatermark(config.watermark.storageKey);\n\n const idPrefix = config.idPrefix ?? \"outbox\";\n const isOnline =\n config.isOnline ?? (() => (typeof navigator !== \"undefined\" ? navigator.onLine : true));\n\n let inflight: Promise<SyncRunSummary> | null = null;\n let generation = 0;\n\n const listeners = new Set<(state: SyncState) => void>();\n let state: SyncState = {\n phase: \"idle\",\n pending: 0,\n lastSummary: null,\n lastError: null,\n lastSyncedAt: null,\n };\n\n const crossTab = config.crossTab === true;\n const channelName = config.broadcastChannelName ?? `tempest-sync:${config.databaseName}`;\n const channel: BroadcastChannel | null =\n crossTab && typeof BroadcastChannel !== \"undefined\"\n ? new BroadcastChannel(channelName)\n : null;\n let closed = false;\n\n function notify(): void {\n for (const listener of listeners) listener(state);\n }\n\n function setState(patch: Partial<SyncState>): void {\n state = { ...state, ...patch };\n notify();\n if (channel && !closed) channel.postMessage(state);\n }\n\n if (channel) {\n channel.onmessage = (event: MessageEvent<SyncState>) => {\n if (closed) return;\n state = event.data;\n notify();\n };\n }\n\n async function refreshPending(): Promise<void> {\n setState({ pending: await outbox.count() });\n }\n\n /**\n * End the current session: every run started before this call is fenced off.\n *\n * Runs capture `generation` when they start and re-check it after every\n * `await`, so the next step of a run started under the previous user — the\n * next `deliver`, the next `pullPage`, an `applyRemote`, the watermark write —\n * never executes under the next user's session. The await already pending (a\n * `deliver` in flight) cannot be recalled; it was issued under the old session\n * and is only bookkept.\n *\n * Detaching `inflight` is what lets the next session's `flush` start its own\n * run instead of joining the abandoned one, which would hand the new user a\n * summary of the old user's queue and leave their own entries waiting. The\n * phase drops back to `\"idle\"` because nothing of the current session is\n * syncing any more.\n */\n function endSession(): void {\n generation += 1;\n if (inflight) {\n inflight = null;\n setState({ phase: \"idle\" });\n }\n }\n\n /**\n * Drain the outbox in `enqueuedAt` order, one record at a time.\n *\n * Once an entry for a record fails, the remaining entries **for that record**\n * are left untried this run. `enqueuedAt` is a FIFO guarantee the engine goes\n * out of its way to keep (see `nextEnqueuedAt`), precisely because delivering\n * an `update` before the `create` it depends on is wrong — and continuing past\n * a failure broke exactly that guarantee. With a `PUT` upsert `deliver`, which\n * is the usual shape, the server would create the record from the *update*\n * payload and the retried `create` would then overwrite it with the older\n * snapshot: the user's edit disappears with nothing reported.\n *\n * Skipping is per record, not per run. Records are independent, so a record\n * the server is rejecting must not hold up every other pending change.\n *\n * The entry list is a snapshot, so before each `deliver` the run checks that\n * its session is still current (see `endSession`) and that the entry is still\n * in the outbox — another tab sharing the IndexedDB may have cleared it on\n * logout. A delivery already in progress when the session ended still has its\n * row deleted or its attempt bumped, because that outcome is true for the\n * server; its `onEntryDelivered`/`onEntryFailed` callback is not fired, since\n * it would act on the next user's local state.\n *\n * @param summary - Mutated with the per-entry outcome counts.\n * @param runGeneration - The session the run started under.\n * @returns `false` when the session ended mid-drain and the run must stop.\n */\n async function push(summary: SyncRunSummary, runGeneration: number): Promise<boolean> {\n const entries = await outbox.list(undefined, { orderBy: \"enqueuedAt\" });\n const blocked = new Set<string>();\n for (const entry of entries) {\n if (blocked.has(entry.recordId)) {\n summary.deferred += 1;\n continue;\n }\n const stillQueued = await outbox.get(entry.id);\n if (generation !== runGeneration) return false;\n if (!stillQueued) continue;\n try {\n await config.deliver(entry);\n await outbox.delete(entry.id);\n summary.succeeded += 1;\n if (generation !== runGeneration) return false;\n await config.onEntryDelivered?.(entry);\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : \"delivery failed\";\n await outbox.update(entry.id, {\n attempts: entry.attempts + 1,\n lastError: message,\n } as Partial<OutboxEntry<TPayload>>);\n blocked.add(entry.recordId);\n summary.failed += 1;\n summary.lastError = message;\n if (generation !== runGeneration) return false;\n await config.onEntryFailed?.(entry, cause);\n }\n }\n return generation === runGeneration;\n }\n\n /**\n * Pull the server delta page by page, then advance the watermark.\n *\n * Checks the session (see `endSession`) before every `applyRemote`, before the\n * next `pullPage` and before the watermark write: a page fetched under the\n * previous user is never merged into the local store after logout, the next\n * page is never requested with the previous user's cursor, and\n * `resetWatermark()` is never undone by the run it was meant to end.\n *\n * @param runGeneration - The session the run started under.\n * @returns `false` when the session ended mid-pull and the run stopped.\n */\n async function pull(runGeneration: number): Promise<boolean> {\n const since = watermark.get();\n let cursor: string | null = null;\n let serverTime: string | null;\n do {\n const page = await config.pullPage(since, cursor);\n for (const item of page.items) {\n if (generation !== runGeneration) return false;\n await config.applyRemote(item);\n }\n if (generation !== runGeneration) return false;\n cursor = page.nextCursor;\n serverTime = page.serverTime;\n } while (cursor);\n if (serverTime) watermark.set(serverTime);\n return true;\n }\n\n /**\n * One push-then-pull pass, stopped early when the session it belongs to ends.\n *\n * @param trigger - What asked for this run, carried into the summary.\n * @param runGeneration - The session captured when `flush` was called, not\n * when the pass starts: with Web Locks the pass can start after a logout that\n * happened while the lock was being granted, and must not adopt the new\n * session.\n * @returns The run summary, with `interrupted: true` when the session ended.\n */\n async function runOnce(trigger: SyncTrigger, runGeneration: number): Promise<SyncRunSummary> {\n const startedAt = Date.now();\n const summary: SyncRunSummary = {\n trigger,\n succeeded: 0,\n failed: 0,\n deferred: 0,\n durationMs: 0,\n skipped: false,\n lastError: null,\n };\n if (!isOnline()) {\n summary.skipped = true;\n summary.durationMs = Date.now() - startedAt;\n return summary;\n }\n const completed = (await push(summary, runGeneration)) && (await pull(runGeneration));\n if (!completed) summary.interrupted = true;\n summary.durationMs = Date.now() - startedAt;\n return summary;\n }\n\n /**\n * Run one sync pass, serialized across tabs when possible.\n *\n * With `crossTab` on and the Web Locks API available, the pass is wrapped in\n * an `ifAvailable` lock request: whichever tab holds the lock runs, and the\n * others get `null` back instead of duplicating the work — their `SyncState`\n * catches up through the `BroadcastChannel`. The no-op summary keeps\n * `skipped: false` on purpose: `skipped` means \"device offline\" (it maps to\n * `phase: \"offline\"`), which is not what happened here. Falls back to a plain\n * per-tab run when `crossTab` is off or Web Locks is missing.\n *\n * @param trigger - What asked for this run, carried into the summary.\n * @param runGeneration - The session the run belongs to (see `runOnce`).\n * @returns The run summary, or an empty summary when another tab held the lock.\n */\n async function runGuarded(\n trigger: SyncTrigger,\n runGeneration: number,\n ): Promise<SyncRunSummary> {\n const locks = typeof navigator !== \"undefined\" ? navigator.locks : undefined;\n if (!crossTab || !locks) return runOnce(trigger, runGeneration);\n const result = await locks.request(\n `${channelName}:flush`,\n { ifAvailable: true },\n async (lock) => (lock ? runOnce(trigger, runGeneration) : null),\n );\n return (\n result ?? {\n trigger,\n succeeded: 0,\n failed: 0,\n deferred: 0,\n durationMs: 0,\n skipped: false,\n lastError: null,\n }\n );\n }\n\n void refreshPending();\n\n let lastEnqueuedAt = 0;\n\n /**\n * Timestamp for a new outbox entry, guaranteed to be strictly greater than\n * the previous one.\n *\n * `enqueuedAt` is the sort key for both `listPending()` and the delivery loop\n * in `flush()`. A plain `Date.now()` ties whenever two mutations are queued\n * within the same millisecond — which is normal in a burst — and a tie leaves\n * the order to the index, so a `create` could be delivered *after* the\n * `update` that depends on it. Advancing by 1ms on a tie keeps the queue FIFO\n * at the cost of a timestamp that can run a few milliseconds ahead of the wall\n * clock during a burst.\n *\n * Scope: this is per engine instance. Two tabs enqueueing in the same\n * millisecond can still tie; cross-tab FIFO would need a persisted sequence,\n * and the delivery loop is already single-flight across tabs via Web Locks.\n */\n function nextEnqueuedAt(): number {\n const now = Date.now();\n lastEnqueuedAt = now > lastEnqueuedAt ? now : lastEnqueuedAt + 1;\n return lastEnqueuedAt;\n }\n\n return {\n async enqueue(op, recordId, payload) {\n const entry: OutboxEntry<TPayload> = {\n id: randomId(idPrefix),\n op,\n recordId,\n enqueuedAt: nextEnqueuedAt(),\n attempts: 0,\n payload,\n };\n await outbox.put(entry);\n await refreshPending();\n return entry.id;\n },\n flush(trigger: SyncTrigger = \"manual\") {\n if (!inflight) {\n setState({ phase: \"syncing\" });\n const runGeneration = generation;\n const run: Promise<SyncRunSummary> = runGuarded(trigger, runGeneration)\n .then(async (summary) => {\n await refreshPending();\n if (generation !== runGeneration) return summary;\n setState({\n phase: summary.skipped\n ? \"offline\"\n : summary.failed > 0\n ? \"error\"\n : \"idle\",\n lastSummary: summary,\n lastError: summary.lastError,\n lastSyncedAt: summary.skipped ? state.lastSyncedAt : Date.now(),\n });\n return summary;\n })\n .finally(() => {\n if (inflight === run) inflight = null;\n });\n inflight = run;\n }\n return inflight;\n },\n pendingCount() {\n return outbox.count();\n },\n listPending() {\n return outbox.list(undefined, { orderBy: \"enqueuedAt\" });\n },\n async clearOutbox() {\n endSession();\n await outbox.clear();\n await refreshPending();\n },\n resetWatermark() {\n endSession();\n watermark.clear();\n },\n getState() {\n return state;\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n dispose() {\n closed = true;\n channel?.close();\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-offline-sync.cjs","names":["createOfflineStore","ownerFingerprint","randomId","require_ids","require_owner_fingerprint","require_create_offline_store","localStorageWatermark","available","key","value","createOfflineSync","outbox","config","watermark","idPrefix","isOnline","currentOwner","principal","ownership","getPrincipal","entry","owner","inflight","generation","listeners","state","crossTab","channelName","channel","closed","notify","listener","setState","patch","event","refreshPending","endSession","push","entries","blocked","stillQueued","runGeneration","verdict","summary","cause","message","pull","since","cursor","serverTime","page","item","runOnce","startedAt","runGuarded","locks","trigger","lock","lastEnqueuedAt","nextEnqueuedAt","now","run"],"sources":["../../src/offline/create-offline-sync.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — outbox drain, pull-with-watermark,\n * conflict resolution and the single-flight guard are one loop by necessity: a pull\n * that lands mid-drain must not advance the watermark past records the outbox has\n * not pushed yet. Splitting the phases hands that ordering to whoever calls them in\n * the right sequence.\n */\nimport { randomId } from \"../utils/ids\";\nimport { ownerFingerprint } from \"../utils/owner-fingerprint\";\nimport { createOfflineStore } from \"./create-offline-store\";\n\n/**\n * Mutation kinds queued in the outbox.\n *\n * `create`/`update` carry a snapshot of the record; `delete` needs only the\n * record id. The distinction between `create` and `update` is advisory — the\n * engine treats both as \"deliver this record\" and leaves the create-vs-update\n * decision to the app's `deliver` callback (a `PUT` upsert usually ignores it).\n */\nexport type OutboxOp = \"create\" | \"update\" | \"delete\";\n\n/**\n * A single queued mutation.\n *\n * @typeParam TPayload - The record snapshot shape carried by\n * `create`/`update` entries (omitted for `delete`).\n */\nexport interface OutboxEntry<TPayload = unknown> {\n /** Stable per-entry id (generated with {@link randomId}). */\n id: string;\n /** The mutation kind. */\n op: OutboxOp;\n /** Primary key of the record the mutation targets. */\n recordId: string;\n /**\n * Epoch milliseconds when the mutation was queued, and the FIFO sort key for\n * `listPending()` and the delivery loop. Strictly increasing per engine\n * instance: entries queued inside the same millisecond are spaced by 1ms so\n * the order never depends on how the index breaks a tie.\n */\n enqueuedAt: number;\n /** How many delivery attempts have been made so far. */\n attempts: number;\n /** Last delivery error message, kept for UI/debug. */\n lastError?: string;\n /** Record snapshot for `create`/`update`. Omitted for `delete`. */\n payload?: TPayload;\n /**\n * One-way fingerprint of whoever was signed in when the entry was queued\n * (`null` for nobody), stamped only when the engine has\n * {@link OfflineSyncConfig.getPrincipal}. Absent on entries queued without\n * it, which are delivered as before.\n */\n owner?: string | null;\n}\n\n/**\n * Why a sync run was triggered. The listed values are the common ones; any\n * string is accepted so apps can add their own telemetry labels.\n */\nexport type SyncTrigger =\n \"boot\" | \"online-event\" | \"after-mutation\" | \"manual\" | \"interval\" | (string & {});\n\n/**\n * One page of the server delta pull.\n *\n * @typeParam TRemote - The server-side item shape.\n */\nexport interface PullPage<TRemote> {\n /** Items changed since the watermark, in this page. */\n items: TRemote[];\n /** Cursor for the next page, or `null` when this is the last page. */\n nextCursor: string | null;\n /**\n * Server clock to persist as the next watermark once the whole delta is\n * applied. `null` leaves the watermark unchanged.\n */\n serverTime: string | null;\n}\n\n/** Outcome of a single {@link OfflineSync.flush} run. */\nexport interface SyncRunSummary {\n /** The trigger passed to `flush`. */\n trigger: SyncTrigger;\n /** Entries delivered to the server this run. */\n succeeded: number;\n /** Entries that failed and stay queued for the next run. */\n failed: number;\n /**\n * Entries left untried because an earlier entry for the **same record**\n * failed this run.\n *\n * They stay queued and are attempted next run, in order. Counted apart from\n * `failed` because they were never sent: reporting them as failures would\n * blame the server for a decision this engine made, and folding them in would\n * make `succeeded + failed` stop accounting for the queue.\n */\n deferred: number;\n /** Total wall-clock milliseconds the run took. */\n durationMs: number;\n /** `true` when the run was skipped because the device was offline. */\n skipped: boolean;\n /** Last delivery error message seen this run, or `null` when none failed. */\n lastError: string | null;\n /**\n * `true` when {@link OfflineSync.clearOutbox} or {@link OfflineSync.resetWatermark}\n * ended the session while this run was in flight, so it stopped early.\n *\n * The counts cover only what happened before the stop: a delivery already in\n * progress at logout is still counted, but no later entry was sent, no further\n * page was pulled or applied, and the watermark was not written. Absent on a\n * run that went to completion.\n */\n interrupted?: true;\n /**\n * Entries left queued because nobody is signed in and they belong to\n * somebody (see {@link OfflineSyncConfig.getPrincipal}). Absent when zero.\n */\n held?: number;\n /**\n * Entries deleted unsent because they were queued under another principal —\n * or while signed out — and somebody else is signed in now (see\n * {@link OfflineSyncConfig.getPrincipal}). Absent when zero.\n */\n dropped?: number;\n}\n\n/**\n * Coarse lifecycle phase of the sync engine, surfaced through\n * {@link OfflineSync.getState} / {@link OfflineSync.subscribe} for reactive UI.\n *\n * - `\"idle\"` — nothing in flight and the last run (if any) fully succeeded.\n * - `\"syncing\"` — a `flush` run is currently in progress.\n * - `\"offline\"` — the last run was skipped because the device was offline.\n * - `\"error\"` — the last run left at least one entry queued after a failure.\n */\nexport type SyncPhase = \"idle\" | \"syncing\" | \"offline\" | \"error\";\n\n/**\n * Immutable snapshot of the engine's reactive state. A fresh object is emitted\n * to subscribers on every change, so it is safe to use as a\n * `useSyncExternalStore` snapshot (reference equality signals \"no change\").\n */\nexport interface SyncState {\n /** Current lifecycle phase. */\n phase: SyncPhase;\n /** Number of mutations still queued in the outbox. */\n pending: number;\n /** Summary of the most recent `flush` run, or `null` before the first. */\n lastSummary: SyncRunSummary | null;\n /** Message of the most recent delivery failure, or `null`. */\n lastError: string | null;\n /** Epoch ms of the last non-skipped run, or `null` before the first. */\n lastSyncedAt: number | null;\n}\n\n/**\n * Pluggable persistence for the pull watermark (the \"changed since\" cursor).\n * Pass an object with a `storageKey` to use a `localStorage`-backed default.\n */\nexport interface WatermarkStore {\n /** Read the current watermark, or `null` when none is stored. */\n get: () => string | null;\n /** Persist a new watermark. */\n set: (value: string) => void;\n /** Drop the watermark (e.g. on logout / account switch). */\n clear: () => void;\n}\n\n/**\n * Configuration for {@link createOfflineSync}.\n *\n * The engine owns the outbox, the single-flight flush, the offline guard, the\n * paginated pull loop and the watermark; the three transport callbacks\n * (`deliver`, `pullPage`, `applyRemote`) are where the app plugs in its own\n * endpoints, record shape and conflict resolution.\n *\n * @typeParam TPayload - Record snapshot carried by outbox entries.\n * @typeParam TRemote - Server item shape returned by `pullPage`.\n */\nexport interface OfflineSyncConfig<TPayload, TRemote> {\n /** IndexedDB database name for the outbox (kept separate per queue). */\n databaseName: string;\n /** Outbox object-store name. Default `\"outbox\"`. */\n tableName?: string;\n /** Outbox schema version. Default `1`. */\n version?: number;\n /** Prefix for generated entry ids. Default `\"outbox\"`. */\n idPrefix?: string;\n /**\n * Deliver one queued mutation to the server. Throwing keeps the entry\n * queued (its `attempts`/`lastError` are bumped) for the next run.\n */\n deliver: (entry: OutboxEntry<TPayload>) => Promise<void>;\n /** Fetch one page of the server delta since `since`, from `cursor`. */\n pullPage: (since: string | null, cursor: string | null) => Promise<PullPage<TRemote>>;\n /**\n * Merge one pulled item into the local store. The app owns conflict\n * resolution here (e.g. last-write-wins, keeping newer local pending\n * edits, resolving tombstones and downloading blobs).\n */\n applyRemote: (item: TRemote) => Promise<void>;\n /** Watermark persistence, or `{ storageKey }` for a `localStorage` default. */\n watermark: WatermarkStore | { storageKey: string };\n /** Called after an entry is delivered and acked. */\n onEntryDelivered?: (entry: OutboxEntry<TPayload>) => void | Promise<void>;\n /** Called after an entry fails delivery (it stays queued). */\n onEntryFailed?: (entry: OutboxEntry<TPayload>, error: unknown) => void | Promise<void>;\n /**\n * Connectivity check. Default reads `navigator.onLine` (always online in\n * non-browser environments). When it returns `false`, `flush` is skipped.\n */\n isOnline?: () => boolean;\n /**\n * Broadcast {@link SyncState} across browser tabs of the same origin via a\n * `BroadcastChannel`, so every tab shows a coherent pending count / phase\n * (e.g. one tab flushing drops the badge to zero everywhere). The outbox is\n * already a shared IndexedDB; this only shares the in-memory state. Silently\n * ignored where `BroadcastChannel` is unavailable. Default `false`.\n *\n * When the Web Locks API is available, `flush` is additionally coordinated\n * *across* tabs: only one tab runs at a time and others skip while the lock\n * is held (they pick up the result via the broadcast). Delivery should still\n * be idempotent (upsert by client id). Without Web Locks, `flush` falls back\n * to per-tab single-flight.\n */\n crossTab?: boolean;\n /** Channel name when `crossTab` is on. Default `tempest-sync:${databaseName}`. */\n broadcastChannelName?: string;\n /**\n * Who is signed in right now — a stable, non-secret id such as the user id;\n * `null`/`undefined` when nobody is. Read synchronously when an entry is\n * queued, which stamps it with a one-way fingerprint (`entry.owner`, never\n * the id itself), and again right before each delivery:\n *\n * - same principal → delivered;\n * - nobody signed in now → held: it stays queued and goes out when its owner\n * signs back in, so a flush fired by the `online` event before login loses\n * nothing;\n * - anybody else → deleted unsent and counted in `summary.dropped`, because\n * delivering it would send the previous account's mutation with the next\n * account's session.\n *\n * `clearOutbox()` on logout already empties the queue; this covers the app\n * that forgets it, or a session that ends without a logout (#531). Entries\n * queued before this option was set carry no stamp and are delivered as\n * before — they are the user's own unsynced edits, and holding them would\n * strand every queue on upgrade.\n *\n * A non-string, non-number value (the `Promise` of an `async` getter) or a\n * getter that throws reads as nobody. Default: no owner binding.\n */\n getPrincipal?: () => string | number | null | undefined;\n}\n\n/**\n * Offline-first sync engine: a durable outbox plus a paginated delta pull.\n *\n * @typeParam TPayload - Record snapshot carried by outbox entries.\n */\nexport interface OfflineSync<TPayload> {\n /**\n * Queue a mutation. Returns the generated entry id.\n *\n * @param op - The mutation kind.\n * @param recordId - Primary key of the affected record.\n * @param payload - Record snapshot (for `create`/`update`).\n */\n enqueue: (op: OutboxOp, recordId: string, payload?: TPayload) => Promise<string>;\n /**\n * Run a full sync (drain the outbox, then pull the delta). Concurrent\n * calls share one in-flight promise, so triggers never overlap.\n *\n * @param trigger - Label for why the run happened. Default `\"manual\"`.\n */\n flush: (trigger?: SyncTrigger) => Promise<SyncRunSummary>;\n /** Number of mutations still queued. */\n pendingCount: () => Promise<number>;\n /** The queued mutations in FIFO order. */\n listPending: () => Promise<OutboxEntry<TPayload>[]>;\n /**\n * Drop every queued mutation (e.g. on logout).\n *\n * Also ends the current session for any `flush` already in flight: that run\n * delivers no further entry, pulls and applies nothing more and writes no\n * watermark, and resolves with `interrupted: true`. A `flush` called after this\n * starts a fresh run instead of joining the abandoned one.\n */\n clearOutbox: () => Promise<void>;\n /**\n * Reset the pull watermark (e.g. on logout / account switch).\n *\n * Ends the current session for an in-flight `flush` exactly like\n * {@link OfflineSync.clearOutbox}, so the run cannot write the ended\n * session's watermark back after the reset.\n */\n resetWatermark: () => void;\n /** Read the current reactive {@link SyncState} snapshot synchronously. */\n getState: () => SyncState;\n /**\n * Subscribe to {@link SyncState} changes. The listener fires on every\n * enqueue, flush transition and outbox clear.\n *\n * @param listener - Called with the new snapshot on each change.\n * @returns An unsubscribe function.\n */\n subscribe: (listener: (state: SyncState) => void) => () => void;\n /**\n * Release resources held by the engine — currently the cross-tab\n * `BroadcastChannel` when `crossTab` is enabled. Safe to call when none was\n * opened. Optional in long-lived apps (the channel is reclaimed on tab\n * close); call it in tests and on teardown.\n */\n dispose: () => void;\n}\n\n/**\n * Build a `localStorage`-backed {@link WatermarkStore}. Reads/writes are no-ops\n * when `localStorage` is unavailable (SSR / non-browser).\n *\n * @param key - The `localStorage` key.\n * @returns A watermark store persisting to `localStorage`.\n */\nfunction localStorageWatermark(key: string): WatermarkStore {\n const available = typeof localStorage !== \"undefined\";\n return {\n get: () => (available ? localStorage.getItem(key) : null),\n set: (value: string) => {\n if (available) localStorage.setItem(key, value);\n },\n clear: () => {\n if (available) localStorage.removeItem(key);\n },\n };\n}\n\n/**\n * Create an offline-first sync engine over an IndexedDB outbox.\n *\n * The engine drains queued mutations to the server (`deliver`) and pulls the\n * server delta back (`pullPage` + `applyRemote`), advancing a watermark so each\n * run only fetches what changed. `flush` is single-flight and skips cleanly\n * while offline; failed deliveries stay queued with their attempt count bumped.\n *\n * Dexie is an **optional peer dependency** (via {@link createOfflineStore}) —\n * install it (`npm i dexie`) when you use this engine.\n *\n * @typeParam TPayload - Record snapshot carried by outbox entries.\n * @typeParam TRemote - Server item shape returned by `pullPage`.\n * @param config - Transport callbacks + outbox/watermark configuration.\n * @returns The sync handle (`enqueue`, `flush`, `pendingCount`, …).\n *\n * @example\n * const sync = createOfflineSync<Note, NoteDto>({\n * databaseName: \"NotesOutbox\",\n * watermark: { storageKey: \"notes.watermark\" },\n * deliver: async (entry) => {\n * if (entry.op === \"delete\") return api.remove(entry.recordId);\n * await api.upsert(entry.recordId, entry.payload!);\n * },\n * pullPage: async (since, cursor) => {\n * const page = await api.changes(since, cursor);\n * return { items: page.items, nextCursor: page.next, serverTime: page.now };\n * },\n * applyRemote: async (dto) => {\n * if (dto.deleted) return localStore.remove(dto.id);\n * await localStore.save(fromDto(dto));\n * },\n * });\n * await sync.enqueue(\"create\", note.id, note);\n * await sync.flush(\"after-mutation\");\n */\nexport function createOfflineSync<TPayload = unknown, TRemote = unknown>(\n config: OfflineSyncConfig<TPayload, TRemote>,\n): OfflineSync<TPayload> {\n const outbox = createOfflineStore<OutboxEntry<TPayload>, string>({\n databaseName: config.databaseName,\n version: config.version ?? 1,\n tableName: config.tableName ?? \"outbox\",\n indexes: \"&id, recordId, enqueuedAt\",\n keyPath: \"id\",\n });\n\n const watermark: WatermarkStore =\n \"get\" in config.watermark\n ? config.watermark\n : localStorageWatermark(config.watermark.storageKey);\n\n const idPrefix = config.idPrefix ?? \"outbox\";\n const isOnline =\n config.isOnline ?? (() => (typeof navigator !== \"undefined\" ? navigator.onLine : true));\n\n const { getPrincipal } = config;\n\n /**\n * Fingerprint whoever `getPrincipal` says is signed in.\n *\n * @param principal - The app's principal getter.\n * @returns The fingerprint, or `null` for nobody, a non-identity value or a\n * getter that throws.\n */\n function currentOwner(principal: () => unknown): string | null {\n try {\n return ownerFingerprint(\"p\", principal());\n } catch {\n return null;\n }\n }\n\n /**\n * What the current session may do with an entry, by its owner stamp.\n *\n * @param entry - The queued entry.\n * @returns `\"deliver\"`, `\"hold\"` (nobody signed in) or `\"drop\"` (someone else).\n */\n function ownership(entry: OutboxEntry<TPayload>): \"deliver\" | \"hold\" | \"drop\" {\n if (!getPrincipal || entry.owner === undefined) return \"deliver\";\n const owner = currentOwner(getPrincipal);\n if (owner === entry.owner) return \"deliver\";\n return owner === null ? \"hold\" : \"drop\";\n }\n\n let inflight: Promise<SyncRunSummary> | null = null;\n let generation = 0;\n\n const listeners = new Set<(state: SyncState) => void>();\n let state: SyncState = {\n phase: \"idle\",\n pending: 0,\n lastSummary: null,\n lastError: null,\n lastSyncedAt: null,\n };\n\n const crossTab = config.crossTab === true;\n const channelName = config.broadcastChannelName ?? `tempest-sync:${config.databaseName}`;\n const channel: BroadcastChannel | null =\n crossTab && typeof BroadcastChannel !== \"undefined\"\n ? new BroadcastChannel(channelName)\n : null;\n let closed = false;\n\n function notify(): void {\n for (const listener of listeners) listener(state);\n }\n\n function setState(patch: Partial<SyncState>): void {\n state = { ...state, ...patch };\n notify();\n if (channel && !closed) channel.postMessage(state);\n }\n\n if (channel) {\n channel.onmessage = (event: MessageEvent<SyncState>) => {\n if (closed) return;\n state = event.data;\n notify();\n };\n }\n\n async function refreshPending(): Promise<void> {\n setState({ pending: await outbox.count() });\n }\n\n /**\n * End the current session: every run started before this call is fenced off.\n *\n * Runs capture `generation` when they start and re-check it after every\n * `await`, so the next step of a run started under the previous user — the\n * next `deliver`, the next `pullPage`, an `applyRemote`, the watermark write —\n * never executes under the next user's session. The await already pending (a\n * `deliver` in flight) cannot be recalled; it was issued under the old session\n * and is only bookkept.\n *\n * Detaching `inflight` is what lets the next session's `flush` start its own\n * run instead of joining the abandoned one, which would hand the new user a\n * summary of the old user's queue and leave their own entries waiting. The\n * phase drops back to `\"idle\"` because nothing of the current session is\n * syncing any more.\n */\n function endSession(): void {\n generation += 1;\n if (inflight) {\n inflight = null;\n setState({ phase: \"idle\" });\n }\n }\n\n /**\n * Drain the outbox in `enqueuedAt` order, one record at a time.\n *\n * Once an entry for a record fails, the remaining entries **for that record**\n * are left untried this run. `enqueuedAt` is a FIFO guarantee the engine goes\n * out of its way to keep (see `nextEnqueuedAt`), precisely because delivering\n * an `update` before the `create` it depends on is wrong — and continuing past\n * a failure broke exactly that guarantee. With a `PUT` upsert `deliver`, which\n * is the usual shape, the server would create the record from the *update*\n * payload and the retried `create` would then overwrite it with the older\n * snapshot: the user's edit disappears with nothing reported.\n *\n * Skipping is per record, not per run. Records are independent, so a record\n * the server is rejecting must not hold up every other pending change.\n *\n * The entry list is a snapshot, so before each `deliver` the run checks that\n * its session is still current (see `endSession`) and that the entry is still\n * in the outbox — another tab sharing the IndexedDB may have cleared it on\n * logout. With `getPrincipal`, the entry's owner stamp is checked at the same\n * point (see `ownership`): another principal's entry is deleted unsent, and\n * one owned by somebody while nobody is signed in stays queued; neither blocks\n * later entries of the same record. A delivery already in progress when the\n * session ended still has its\n * row deleted or its attempt bumped, because that outcome is true for the\n * server; its `onEntryDelivered`/`onEntryFailed` callback is not fired, since\n * it would act on the next user's local state.\n *\n * @param summary - Mutated with the per-entry outcome counts.\n * @param runGeneration - The session the run started under.\n * @returns `false` when the session ended mid-drain and the run must stop.\n */\n async function push(summary: SyncRunSummary, runGeneration: number): Promise<boolean> {\n const entries = await outbox.list(undefined, { orderBy: \"enqueuedAt\" });\n const blocked = new Set<string>();\n for (const entry of entries) {\n if (blocked.has(entry.recordId)) {\n summary.deferred += 1;\n continue;\n }\n const stillQueued = await outbox.get(entry.id);\n if (generation !== runGeneration) return false;\n if (!stillQueued) continue;\n const verdict = ownership(entry);\n if (verdict === \"hold\") {\n summary.held = (summary.held ?? 0) + 1;\n continue;\n }\n if (verdict === \"drop\") {\n await outbox.delete(entry.id);\n summary.dropped = (summary.dropped ?? 0) + 1;\n continue;\n }\n try {\n await config.deliver(entry);\n await outbox.delete(entry.id);\n summary.succeeded += 1;\n if (generation !== runGeneration) return false;\n await config.onEntryDelivered?.(entry);\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : \"delivery failed\";\n await outbox.update(entry.id, {\n attempts: entry.attempts + 1,\n lastError: message,\n } as Partial<OutboxEntry<TPayload>>);\n blocked.add(entry.recordId);\n summary.failed += 1;\n summary.lastError = message;\n if (generation !== runGeneration) return false;\n await config.onEntryFailed?.(entry, cause);\n }\n }\n return generation === runGeneration;\n }\n\n /**\n * Pull the server delta page by page, then advance the watermark.\n *\n * Checks the session (see `endSession`) before every `applyRemote`, before the\n * next `pullPage` and before the watermark write: a page fetched under the\n * previous user is never merged into the local store after logout, the next\n * page is never requested with the previous user's cursor, and\n * `resetWatermark()` is never undone by the run it was meant to end.\n *\n * @param runGeneration - The session the run started under.\n * @returns `false` when the session ended mid-pull and the run stopped.\n */\n async function pull(runGeneration: number): Promise<boolean> {\n const since = watermark.get();\n let cursor: string | null = null;\n let serverTime: string | null;\n do {\n const page = await config.pullPage(since, cursor);\n for (const item of page.items) {\n if (generation !== runGeneration) return false;\n await config.applyRemote(item);\n }\n if (generation !== runGeneration) return false;\n cursor = page.nextCursor;\n serverTime = page.serverTime;\n } while (cursor);\n if (serverTime) watermark.set(serverTime);\n return true;\n }\n\n /**\n * One push-then-pull pass, stopped early when the session it belongs to ends.\n *\n * @param trigger - What asked for this run, carried into the summary.\n * @param runGeneration - The session captured when `flush` was called, not\n * when the pass starts: with Web Locks the pass can start after a logout that\n * happened while the lock was being granted, and must not adopt the new\n * session.\n * @returns The run summary, with `interrupted: true` when the session ended.\n */\n async function runOnce(trigger: SyncTrigger, runGeneration: number): Promise<SyncRunSummary> {\n const startedAt = Date.now();\n const summary: SyncRunSummary = {\n trigger,\n succeeded: 0,\n failed: 0,\n deferred: 0,\n durationMs: 0,\n skipped: false,\n lastError: null,\n };\n if (!isOnline()) {\n summary.skipped = true;\n summary.durationMs = Date.now() - startedAt;\n return summary;\n }\n const completed = (await push(summary, runGeneration)) && (await pull(runGeneration));\n if (!completed) summary.interrupted = true;\n summary.durationMs = Date.now() - startedAt;\n return summary;\n }\n\n /**\n * Run one sync pass, serialized across tabs when possible.\n *\n * With `crossTab` on and the Web Locks API available, the pass is wrapped in\n * an `ifAvailable` lock request: whichever tab holds the lock runs, and the\n * others get `null` back instead of duplicating the work — their `SyncState`\n * catches up through the `BroadcastChannel`. The no-op summary keeps\n * `skipped: false` on purpose: `skipped` means \"device offline\" (it maps to\n * `phase: \"offline\"`), which is not what happened here. Falls back to a plain\n * per-tab run when `crossTab` is off or Web Locks is missing.\n *\n * @param trigger - What asked for this run, carried into the summary.\n * @param runGeneration - The session the run belongs to (see `runOnce`).\n * @returns The run summary, or an empty summary when another tab held the lock.\n */\n async function runGuarded(\n trigger: SyncTrigger,\n runGeneration: number,\n ): Promise<SyncRunSummary> {\n const locks = typeof navigator !== \"undefined\" ? navigator.locks : undefined;\n if (!crossTab || !locks) return runOnce(trigger, runGeneration);\n const result = await locks.request(\n `${channelName}:flush`,\n { ifAvailable: true },\n async (lock) => (lock ? runOnce(trigger, runGeneration) : null),\n );\n return (\n result ?? {\n trigger,\n succeeded: 0,\n failed: 0,\n deferred: 0,\n durationMs: 0,\n skipped: false,\n lastError: null,\n }\n );\n }\n\n void refreshPending();\n\n let lastEnqueuedAt = 0;\n\n /**\n * Timestamp for a new outbox entry, guaranteed to be strictly greater than\n * the previous one.\n *\n * `enqueuedAt` is the sort key for both `listPending()` and the delivery loop\n * in `flush()`. A plain `Date.now()` ties whenever two mutations are queued\n * within the same millisecond — which is normal in a burst — and a tie leaves\n * the order to the index, so a `create` could be delivered *after* the\n * `update` that depends on it. Advancing by 1ms on a tie keeps the queue FIFO\n * at the cost of a timestamp that can run a few milliseconds ahead of the wall\n * clock during a burst.\n *\n * Scope: this is per engine instance. Two tabs enqueueing in the same\n * millisecond can still tie; cross-tab FIFO would need a persisted sequence,\n * and the delivery loop is already single-flight across tabs via Web Locks.\n */\n function nextEnqueuedAt(): number {\n const now = Date.now();\n lastEnqueuedAt = now > lastEnqueuedAt ? now : lastEnqueuedAt + 1;\n return lastEnqueuedAt;\n }\n\n return {\n async enqueue(op, recordId, payload) {\n const entry: OutboxEntry<TPayload> = {\n id: randomId(idPrefix),\n op,\n recordId,\n enqueuedAt: nextEnqueuedAt(),\n attempts: 0,\n payload,\n };\n if (getPrincipal) entry.owner = currentOwner(getPrincipal);\n await outbox.put(entry);\n await refreshPending();\n return entry.id;\n },\n flush(trigger: SyncTrigger = \"manual\") {\n if (!inflight) {\n setState({ phase: \"syncing\" });\n const runGeneration = generation;\n const run: Promise<SyncRunSummary> = runGuarded(trigger, runGeneration)\n .then(async (summary) => {\n await refreshPending();\n if (generation !== runGeneration) return summary;\n setState({\n phase: summary.skipped\n ? \"offline\"\n : summary.failed > 0\n ? \"error\"\n : \"idle\",\n lastSummary: summary,\n lastError: summary.lastError,\n lastSyncedAt: summary.skipped ? state.lastSyncedAt : Date.now(),\n });\n return summary;\n })\n .finally(() => {\n if (inflight === run) inflight = null;\n });\n inflight = run;\n }\n return inflight;\n },\n pendingCount() {\n return outbox.count();\n },\n listPending() {\n return outbox.list(undefined, { orderBy: \"enqueuedAt\" });\n },\n async clearOutbox() {\n endSession();\n await outbox.clear();\n await refreshPending();\n },\n resetWatermark() {\n endSession();\n watermark.clear();\n },\n getState() {\n return state;\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n dispose() {\n closed = true;\n channel?.close();\n },\n };\n}\n"],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import { randomId as e } from "../utils/ids.js";
2
- import { createOfflineStore as t } from "./create-offline-store.js";
2
+ import { ownerFingerprint as t } from "../utils/owner-fingerprint.js";
3
+ import { createOfflineStore as n } from "./create-offline-store.js";
3
4
  //#region src/offline/create-offline-sync.ts
4
- function n(e) {
5
+ function r(e) {
5
6
  let t = typeof localStorage < "u";
6
7
  return {
7
8
  get: () => t ? localStorage.getItem(e) : null,
@@ -13,75 +14,92 @@ function n(e) {
13
14
  }
14
15
  };
15
16
  }
16
- function r(r) {
17
- let i = t({
18
- databaseName: r.databaseName,
19
- version: r.version ?? 1,
20
- tableName: r.tableName ?? "outbox",
17
+ function i(i) {
18
+ let a = n({
19
+ databaseName: i.databaseName,
20
+ version: i.version ?? 1,
21
+ tableName: i.tableName ?? "outbox",
21
22
  indexes: "&id, recordId, enqueuedAt",
22
23
  keyPath: "id"
23
- }), a = "get" in r.watermark ? r.watermark : n(r.watermark.storageKey), o = r.idPrefix ?? "outbox", s = r.isOnline ?? (() => typeof navigator < "u" ? navigator.onLine : !0), c = null, l = 0, u = /* @__PURE__ */ new Set(), d = {
24
+ }), o = "get" in i.watermark ? i.watermark : r(i.watermark.storageKey), s = i.idPrefix ?? "outbox", c = i.isOnline ?? (() => typeof navigator < "u" ? navigator.onLine : !0), { getPrincipal: l } = i;
25
+ function u(e) {
26
+ try {
27
+ return t("p", e());
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ function d(e) {
33
+ if (!l || e.owner === void 0) return "deliver";
34
+ let t = u(l);
35
+ return t === e.owner ? "deliver" : t === null ? "hold" : "drop";
36
+ }
37
+ let f = null, p = 0, m = /* @__PURE__ */ new Set(), h = {
24
38
  phase: "idle",
25
39
  pending: 0,
26
40
  lastSummary: null,
27
41
  lastError: null,
28
42
  lastSyncedAt: null
29
- }, f = r.crossTab === !0, p = r.broadcastChannelName ?? `tempest-sync:${r.databaseName}`, m = f && typeof BroadcastChannel < "u" ? new BroadcastChannel(p) : null, h = !1;
30
- function g() {
31
- for (let e of u) e(d);
43
+ }, g = i.crossTab === !0, _ = i.broadcastChannelName ?? `tempest-sync:${i.databaseName}`, v = g && typeof BroadcastChannel < "u" ? new BroadcastChannel(_) : null, y = !1;
44
+ function b() {
45
+ for (let e of m) e(h);
32
46
  }
33
- function _(e) {
34
- d = {
35
- ...d,
47
+ function x(e) {
48
+ h = {
49
+ ...h,
36
50
  ...e
37
- }, g(), m && !h && m.postMessage(d);
51
+ }, b(), v && !y && v.postMessage(h);
38
52
  }
39
- m && (m.onmessage = (e) => {
40
- h || (d = e.data, g());
53
+ v && (v.onmessage = (e) => {
54
+ y || (h = e.data, b());
41
55
  });
42
- async function v() {
43
- _({ pending: await i.count() });
56
+ async function S() {
57
+ x({ pending: await a.count() });
44
58
  }
45
- function y() {
46
- l += 1, c && (c = null, _({ phase: "idle" }));
59
+ function C() {
60
+ p += 1, f && (f = null, x({ phase: "idle" }));
47
61
  }
48
- async function b(e, t) {
49
- let n = await i.list(void 0, { orderBy: "enqueuedAt" }), a = /* @__PURE__ */ new Set();
62
+ async function w(e, t) {
63
+ let n = await a.list(void 0, { orderBy: "enqueuedAt" }), r = /* @__PURE__ */ new Set();
50
64
  for (let o of n) {
51
- if (a.has(o.recordId)) {
65
+ if (r.has(o.recordId)) {
52
66
  e.deferred += 1;
53
67
  continue;
54
68
  }
55
- let n = await i.get(o.id);
56
- if (l !== t) return !1;
57
- if (n) try {
58
- if (await r.deliver(o), await i.delete(o.id), e.succeeded += 1, l !== t) return !1;
59
- await r.onEntryDelivered?.(o);
69
+ let n = await a.get(o.id);
70
+ if (p !== t) return !1;
71
+ if (!n) continue;
72
+ let s = d(o);
73
+ if (s === "hold") e.held = (e.held ?? 0) + 1;
74
+ else if (s === "drop") await a.delete(o.id), e.dropped = (e.dropped ?? 0) + 1;
75
+ else try {
76
+ if (await i.deliver(o), await a.delete(o.id), e.succeeded += 1, p !== t) return !1;
77
+ await i.onEntryDelivered?.(o);
60
78
  } catch (n) {
61
79
  let s = n instanceof Error ? n.message : "delivery failed";
62
- if (await i.update(o.id, {
80
+ if (await a.update(o.id, {
63
81
  attempts: o.attempts + 1,
64
82
  lastError: s
65
- }), a.add(o.recordId), e.failed += 1, e.lastError = s, l !== t) return !1;
66
- await r.onEntryFailed?.(o, n);
83
+ }), r.add(o.recordId), e.failed += 1, e.lastError = s, p !== t) return !1;
84
+ await i.onEntryFailed?.(o, n);
67
85
  }
68
86
  }
69
- return l === t;
87
+ return p === t;
70
88
  }
71
- async function x(e) {
72
- let t = a.get(), n = null, i;
89
+ async function T(e) {
90
+ let t = o.get(), n = null, r;
73
91
  do {
74
- let a = await r.pullPage(t, n);
92
+ let a = await i.pullPage(t, n);
75
93
  for (let t of a.items) {
76
- if (l !== e) return !1;
77
- await r.applyRemote(t);
94
+ if (p !== e) return !1;
95
+ await i.applyRemote(t);
78
96
  }
79
- if (l !== e) return !1;
80
- n = a.nextCursor, i = a.serverTime;
97
+ if (p !== e) return !1;
98
+ n = a.nextCursor, r = a.serverTime;
81
99
  } while (n);
82
- return i && a.set(i), !0;
100
+ return r && o.set(r), !0;
83
101
  }
84
- async function S(e, t) {
102
+ async function E(e, t) {
85
103
  let n = Date.now(), r = {
86
104
  trigger: e,
87
105
  succeeded: 0,
@@ -91,11 +109,11 @@ function r(r) {
91
109
  skipped: !1,
92
110
  lastError: null
93
111
  };
94
- return s() ? (await b(r, t) && await x(t) || (r.interrupted = !0), r.durationMs = Date.now() - n, r) : (r.skipped = !0, r.durationMs = Date.now() - n, r);
112
+ return c() ? (await w(r, t) && await T(t) || (r.interrupted = !0), r.durationMs = Date.now() - n, r) : (r.skipped = !0, r.durationMs = Date.now() - n, r);
95
113
  }
96
- async function C(e, t) {
114
+ async function D(e, t) {
97
115
  let n = typeof navigator < "u" ? navigator.locks : void 0;
98
- return !f || !n ? S(e, t) : await n.request(`${p}:flush`, { ifAvailable: !0 }, async (n) => n ? S(e, t) : null) ?? {
116
+ return !g || !n ? E(e, t) : await n.request(`${_}:flush`, { ifAvailable: !0 }, async (n) => n ? E(e, t) : null) ?? {
99
117
  trigger: e,
100
118
  succeeded: 0,
101
119
  failed: 0,
@@ -105,65 +123,65 @@ function r(r) {
105
123
  lastError: null
106
124
  };
107
125
  }
108
- v();
109
- let w = 0;
110
- function T() {
126
+ S();
127
+ let O = 0;
128
+ function k() {
111
129
  let e = Date.now();
112
- return w = e > w ? e : w + 1, w;
130
+ return O = e > O ? e : O + 1, O;
113
131
  }
114
132
  return {
115
133
  async enqueue(t, n, r) {
116
- let a = {
117
- id: e(o),
134
+ let i = {
135
+ id: e(s),
118
136
  op: t,
119
137
  recordId: n,
120
- enqueuedAt: T(),
138
+ enqueuedAt: k(),
121
139
  attempts: 0,
122
140
  payload: r
123
141
  };
124
- return await i.put(a), await v(), a.id;
142
+ return l && (i.owner = u(l)), await a.put(i), await S(), i.id;
125
143
  },
126
144
  flush(e = "manual") {
127
- if (!c) {
128
- _({ phase: "syncing" });
129
- let t = l, n = C(e, t).then(async (e) => (await v(), l === t && _({
145
+ if (!f) {
146
+ x({ phase: "syncing" });
147
+ let t = p, n = D(e, t).then(async (e) => (await S(), p === t && x({
130
148
  phase: e.skipped ? "offline" : e.failed > 0 ? "error" : "idle",
131
149
  lastSummary: e,
132
150
  lastError: e.lastError,
133
- lastSyncedAt: e.skipped ? d.lastSyncedAt : Date.now()
151
+ lastSyncedAt: e.skipped ? h.lastSyncedAt : Date.now()
134
152
  }), e)).finally(() => {
135
- c === n && (c = null);
153
+ f === n && (f = null);
136
154
  });
137
- c = n;
155
+ f = n;
138
156
  }
139
- return c;
157
+ return f;
140
158
  },
141
159
  pendingCount() {
142
- return i.count();
160
+ return a.count();
143
161
  },
144
162
  listPending() {
145
- return i.list(void 0, { orderBy: "enqueuedAt" });
163
+ return a.list(void 0, { orderBy: "enqueuedAt" });
146
164
  },
147
165
  async clearOutbox() {
148
- y(), await i.clear(), await v();
166
+ C(), await a.clear(), await S();
149
167
  },
150
168
  resetWatermark() {
151
- y(), a.clear();
169
+ C(), o.clear();
152
170
  },
153
171
  getState() {
154
- return d;
172
+ return h;
155
173
  },
156
174
  subscribe(e) {
157
- return u.add(e), () => {
158
- u.delete(e);
175
+ return m.add(e), () => {
176
+ m.delete(e);
159
177
  };
160
178
  },
161
179
  dispose() {
162
- h = !0, m?.close();
180
+ y = !0, v?.close();
163
181
  }
164
182
  };
165
183
  }
166
184
  //#endregion
167
- export { r as createOfflineSync };
185
+ export { i as createOfflineSync };
168
186
 
169
187
  //# sourceMappingURL=create-offline-sync.js.map