@rebasepro/server 0.24.0 → 0.24.1-canary.g19596ef

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 (240) hide show
  1. package/README.md +27 -2
  2. package/dist/GCSStorageController-DEga1ULx.js +402 -0
  3. package/dist/GCSStorageController-DEga1ULx.js.map +1 -0
  4. package/dist/S3StorageController-Btx-6bJ5.js +480 -0
  5. package/dist/S3StorageController-Btx-6bJ5.js.map +1 -0
  6. package/dist/api/errors.d.ts +32 -0
  7. package/dist/api/live-schema-routes.d.ts +27 -3
  8. package/dist/api/mount-path.d.ts +30 -0
  9. package/dist/api/mount.d.ts +6 -13
  10. package/dist/api/rest/api-generator.d.ts +53 -1
  11. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  12. package/dist/api/rest/field-access-query.d.ts +28 -8
  13. package/dist/api/rest/field-ops.d.ts +19 -7
  14. package/dist/api/rest/query-body.d.ts +10 -0
  15. package/dist/api/rest/write-validation.d.ts +7 -5
  16. package/dist/api/schema-editor-routes.d.ts +10 -1
  17. package/dist/api/sql-export-routes.d.ts +8 -0
  18. package/dist/api/types.d.ts +35 -0
  19. package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
  20. package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
  21. package/dist/auth/access.d.ts +62 -11
  22. package/dist/auth/admin-user-ops.d.ts +24 -2
  23. package/dist/auth/admin-users-route.d.ts +1 -0
  24. package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
  25. package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
  26. package/dist/auth/builtin-auth-adapter.d.ts +13 -0
  27. package/dist/auth/email-change-routes.d.ts +3 -0
  28. package/dist/auth/expired-token-sweep.d.ts +5 -1
  29. package/dist/auth/interfaces.d.ts +10 -0
  30. package/dist/auth/jwks-routes.d.ts +5 -1
  31. package/dist/auth/jwt-crypto.d.ts +7 -0
  32. package/dist/auth/jwt-keys.d.ts +4 -0
  33. package/dist/auth/jwt.d.ts +282 -166
  34. package/dist/auth/magic-link-routes.d.ts +3 -0
  35. package/dist/auth/mfa-crypto.d.ts +9 -3
  36. package/dist/auth/mfa-gate.d.ts +4 -2
  37. package/dist/auth/mfa-routes.d.ts +6 -0
  38. package/dist/auth/middleware.d.ts +9 -2
  39. package/dist/auth/otp-routes.d.ts +3 -0
  40. package/dist/auth/rate-limiter.d.ts +61 -34
  41. package/dist/auth/registration-policy.d.ts +17 -0
  42. package/dist/auth/require-auth.d.ts +1 -1
  43. package/dist/auth/routes.d.ts +16 -0
  44. package/dist/auth/session-routes.d.ts +6 -0
  45. package/dist/auth/token-revocation.d.ts +6 -0
  46. package/dist/auth/user-picking.d.ts +37 -0
  47. package/dist/auth/verify-credential.d.ts +25 -4
  48. package/dist/{auth-DMLngxn_.js → auth-CSBky1mt.js} +2927 -1882
  49. package/dist/auth-CSBky1mt.js.map +1 -0
  50. package/dist/auth_adapter-CIItCTc8.js +21 -0
  51. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  52. package/dist/backend/create-backend.d.ts +281 -0
  53. package/dist/backend/default-backend.d.ts +35 -0
  54. package/dist/backend/no-collections.d.ts +19 -0
  55. package/dist/backend/options.d.ts +912 -0
  56. package/dist/backend/scope.d.ts +23 -0
  57. package/dist/backend/services.d.ts +54 -0
  58. package/dist/backend-DTAOsLQc.js.map +1 -1
  59. package/dist/backup/backup-common.d.ts +14 -3
  60. package/dist/backup/backup-routes.d.ts +23 -2
  61. package/dist/backup/backup-storage.d.ts +3 -1
  62. package/dist/backup/index.d.ts +3 -1
  63. package/dist/{backup-CN0s50D2.js → backup-DBGRUVTS.js} +178 -37
  64. package/dist/backup-DBGRUVTS.js.map +1 -0
  65. package/dist/boot/boot.d.ts +2 -2
  66. package/dist/boot/driver.d.ts +3 -0
  67. package/dist/boot/env.d.ts +9 -2
  68. package/dist/boot/http-server.d.ts +22 -0
  69. package/dist/boot/options.d.ts +16 -1
  70. package/dist/boot/provision.d.ts +4 -2
  71. package/dist/boot/resource-adapters.d.ts +11 -14
  72. package/dist/boot/runtime-cors.d.ts +17 -0
  73. package/dist/boot/sources.d.ts +9 -0
  74. package/dist/boot/static-routing.d.ts +9 -0
  75. package/dist/{contract-routes-fz8i4pxs.js → contract-routes-DiFwiLg2.js} +5 -21
  76. package/dist/contract-routes-DiFwiLg2.js.map +1 -0
  77. package/dist/cron/cron-loader.d.ts +77 -18
  78. package/dist/cron/cron-routes.d.ts +1 -1
  79. package/dist/cron/cron-scheduler.d.ts +69 -3
  80. package/dist/cron/cron-store.d.ts +33 -5
  81. package/dist/cron/define-cron.d.ts +6 -2
  82. package/dist/cron/index.d.ts +3 -3
  83. package/dist/cron-loader-NHqRfKF0.js +202 -0
  84. package/dist/cron-loader-NHqRfKF0.js.map +1 -0
  85. package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-DsH8Jwn5.js} +29 -6
  86. package/dist/cron-routes-DsH8Jwn5.js.map +1 -0
  87. package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-D1DT0nw5.js} +300 -34
  88. package/dist/cron-scheduler-D1DT0nw5.js.map +1 -0
  89. package/dist/cron-store-DEpVstUU.js +577 -0
  90. package/dist/cron-store-DEpVstUU.js.map +1 -0
  91. package/dist/{ddl-bootstrap-BaqMSa4Y.js → ddl-bootstrap-BNqi2NZe.js} +2 -2
  92. package/dist/{ddl-bootstrap-BaqMSa4Y.js.map → ddl-bootstrap-BNqi2NZe.js.map} +1 -1
  93. package/dist/email/smtp-email-service.d.ts +23 -0
  94. package/dist/email/types.d.ts +18 -0
  95. package/dist/env.d.ts +0 -37
  96. package/dist/{errors-D6_y86c5.js → errors-Cqp86KnA.js} +166 -7
  97. package/dist/errors-Cqp86KnA.js.map +1 -0
  98. package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
  99. package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
  100. package/dist/{function-loader-D7o5Epjj.js → function-loader-DbCSoNJj.js} +69 -39
  101. package/dist/function-loader-DbCSoNJj.js.map +1 -0
  102. package/dist/{function-routes-CaNG4waN.js → function-routes-BSTClQNa.js} +6 -5
  103. package/dist/function-routes-BSTClQNa.js.map +1 -0
  104. package/dist/functions/context.d.ts +9 -0
  105. package/dist/functions/define-function.d.ts +10 -6
  106. package/dist/functions/function-loader.d.ts +75 -18
  107. package/dist/functions/function-routes.d.ts +2 -1
  108. package/dist/functions/index.d.ts +5 -4
  109. package/dist/functions/index.js +59 -15
  110. package/dist/functions/index.js.map +1 -1
  111. package/dist/functions/internal.d.ts +2 -2
  112. package/dist/functions/proxy.d.ts +6 -2
  113. package/dist/functions/request-timeout.d.ts +7 -5
  114. package/dist/history/history-routes.d.ts +9 -0
  115. package/dist/history/retention-sweep.d.ts +81 -0
  116. package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-RZ1Ow4YY.js} +4 -4
  117. package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-RZ1Ow4YY.js.map} +1 -1
  118. package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
  119. package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
  120. package/dist/index.d.ts +24 -11
  121. package/dist/index.es.js +24758 -20153
  122. package/dist/index.es.js.map +1 -1
  123. package/dist/init/docs.d.ts +35 -10
  124. package/dist/init/process-safety.d.ts +7 -2
  125. package/dist/init/shutdown.d.ts +18 -3
  126. package/dist/init.d.ts +49 -865
  127. package/dist/internal-tables-L2t39RdT.js +136 -0
  128. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  129. package/dist/jobs/job-queue.d.ts +8 -0
  130. package/dist/jobs/job-store.d.ts +24 -0
  131. package/dist/jobs/types.d.ts +25 -4
  132. package/dist/{jobs-DqYNfquG.js → jobs-8G-_agld.js} +235 -25
  133. package/dist/jobs-8G-_agld.js.map +1 -0
  134. package/dist/{jwt-R6bSPMjk.js → jwt-CBaFc34Y.js} +530 -455
  135. package/dist/jwt-CBaFc34Y.js.map +1 -0
  136. package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
  137. package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
  138. package/dist/load-failure-L5hvfDoG.js +80 -0
  139. package/dist/load-failure-L5hvfDoG.js.map +1 -0
  140. package/dist/{logger-D-S-hO5e.js → logger-Bt32sHFw.js} +17 -4
  141. package/dist/logger-Bt32sHFw.js.map +1 -0
  142. package/dist/{logs-routes-DAdv37GI.js → logs-routes-DQJhrf-b.js} +3 -3
  143. package/dist/{logs-routes-DAdv37GI.js.map → logs-routes-DQJhrf-b.js.map} +1 -1
  144. package/dist/mcp/mcp-routes.d.ts +6 -0
  145. package/dist/mcp/mcp-tools.d.ts +8 -0
  146. package/dist/mcp/oauth-routes.d.ts +7 -0
  147. package/dist/metrics/index.d.ts +18 -0
  148. package/dist/metrics/pool-wait.d.ts +50 -0
  149. package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
  150. package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
  151. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
  152. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
  153. package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-CeGW2dLC.js} +366 -82
  154. package/dist/openapi-generator-CeGW2dLC.js.map +1 -0
  155. package/dist/policy-PdzX-Opd.js +121 -0
  156. package/dist/policy-PdzX-Opd.js.map +1 -0
  157. package/dist/project_manifest-R6y_KHms.js +59 -0
  158. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  159. package/dist/{proxy-qRlqeUmO.js → proxy-BEKbsil6.js} +5 -4
  160. package/dist/proxy-BEKbsil6.js.map +1 -0
  161. package/dist/{query-parser-BgiKJKvc.js → query-parser-ECxP0nDw.js} +511 -16
  162. package/dist/query-parser-ECxP0nDw.js.map +1 -0
  163. package/dist/{request-timeout-DgH7j8qO.js → request-timeout-BiRelY-U.js} +17 -10
  164. package/dist/request-timeout-BiRelY-U.js.map +1 -0
  165. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  166. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  167. package/dist/schema-edit/apply-schema-change.d.ts +10 -0
  168. package/dist/schema-edit/local-source.d.ts +29 -0
  169. package/dist/schema-edit/remote-source.d.ts +13 -3
  170. package/dist/schema-edit/source-editor.d.ts +28 -0
  171. package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-DRXBAbZy.js} +55 -11
  172. package/dist/schema-editor-routes-DRXBAbZy.js.map +1 -0
  173. package/dist/services/outbound-post.d.ts +50 -0
  174. package/dist/services/outbound-url-guard.d.ts +34 -5
  175. package/dist/services/routed-realtime-service.d.ts +5 -0
  176. package/dist/services/webhook-service.d.ts +66 -14
  177. package/dist/singleton.d.ts +9 -5
  178. package/dist/soft-delete-params-CBryZXMq.js +668 -0
  179. package/dist/soft-delete-params-CBryZXMq.js.map +1 -0
  180. package/dist/sql-export-routes-D8ZaRvoR.js +240 -0
  181. package/dist/sql-export-routes-D8ZaRvoR.js.map +1 -0
  182. package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
  183. package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
  184. package/dist/storage/GCSStorageController.d.ts +46 -2
  185. package/dist/storage/LocalStorageController.d.ts +67 -2
  186. package/dist/storage/S3StorageController.d.ts +37 -3
  187. package/dist/storage/image-transform.d.ts +15 -0
  188. package/dist/storage/maintenance-routes.d.ts +23 -0
  189. package/dist/storage/maintenance.d.ts +126 -0
  190. package/dist/storage/rendition-cache.d.ts +9 -11
  191. package/dist/storage/request-principals.d.ts +30 -0
  192. package/dist/storage/routes.d.ts +20 -0
  193. package/dist/storage/source-variables.d.ts +10 -0
  194. package/dist/storage/streams.d.ts +42 -0
  195. package/dist/storage/tus-handler.d.ts +46 -2
  196. package/dist/storage/types.d.ts +82 -7
  197. package/dist/storage/upload-validation.d.ts +13 -0
  198. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  199. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  200. package/dist/strings.es-BeZWAodn.js +256 -0
  201. package/dist/strings.es-BeZWAodn.js.map +1 -0
  202. package/dist/upload-validation-CVobI5xO.js +172 -0
  203. package/dist/upload-validation-CVobI5xO.js.map +1 -0
  204. package/dist/utils/byte-size.d.ts +10 -0
  205. package/dist/utils/compression.d.ts +6 -0
  206. package/dist/utils/load-failure.d.ts +14 -0
  207. package/dist/utils/logger.d.ts +9 -0
  208. package/package.json +5 -5
  209. package/dist/GCSStorageController-BSiP1c-f.js +0 -256
  210. package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
  211. package/dist/S3StorageController-CAwFRgjV.js +0 -276
  212. package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
  213. package/dist/auth-DMLngxn_.js.map +0 -1
  214. package/dist/backup-CN0s50D2.js.map +0 -1
  215. package/dist/contract-routes-fz8i4pxs.js.map +0 -1
  216. package/dist/cron-loader-CwaANlOG.js +0 -118
  217. package/dist/cron-loader-CwaANlOG.js.map +0 -1
  218. package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
  219. package/dist/cron-scheduler-CYQgco86.js.map +0 -1
  220. package/dist/cron-store-D2Q9-Aco.js +0 -300
  221. package/dist/cron-store-D2Q9-Aco.js.map +0 -1
  222. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  223. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  224. package/dist/errors-D6_y86c5.js.map +0 -1
  225. package/dist/function-loader-D7o5Epjj.js.map +0 -1
  226. package/dist/function-routes-CaNG4waN.js.map +0 -1
  227. package/dist/jobs-DqYNfquG.js.map +0 -1
  228. package/dist/jwt-R6bSPMjk.js.map +0 -1
  229. package/dist/logger-D-S-hO5e.js.map +0 -1
  230. package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
  231. package/dist/proxy-qRlqeUmO.js.map +0 -1
  232. package/dist/query-parser-BgiKJKvc.js.map +0 -1
  233. package/dist/request-timeout-DgH7j8qO.js.map +0 -1
  234. package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
  235. package/dist/soft-delete-params-BWPilMPF.js +0 -59
  236. package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
  237. package/dist/src-I3aG1PcY.js +0 -6870
  238. package/dist/src-I3aG1PcY.js.map +0 -1
  239. package/dist/types-BfKcm9do.js +0 -39
  240. package/dist/types-BfKcm9do.js.map +0 -1
@@ -0,0 +1,1915 @@
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ import { c as RebaseClientError, l as isUnsupported, s as RebaseApiError, u as unsupportedMethod } from "./strings.es-BeZWAodn.js";
6
+ import { D as Vector, E as GeoPoint, T as EntityRelation, _ as normalizeOrderBy, a as serializeFilter, c as collectAllPages, h as serializeInclude, l as paginateFind, o as serializeLogicalCondition, p as mergeIncludeSpecs, t as RESERVED_QUERY_KEYS, u as resolveFindWindow, v as serializeOrderBy, w as EntityReference } from "./filter-dialect-O1HK6Nvr.js";
7
+ import { et as sortKeyToString } from "./src-D5Y3N-S4.js";
8
+ import { t as IMPERSONATE_HEADER } from "./auth_adapter-CIItCTc8.js";
9
+ import { t as SCHEMA_VERSION_HEADER } from "./project_manifest-R6y_KHms.js";
10
+ //#region ../utils/dist/unref.es.js
11
+ /**
12
+ * Release a Node handle's hold on the event loop, where the runtime has one.
13
+ *
14
+ * `unref` is Node's, not the web platform's. Node ref-counts the handles that
15
+ * can keep a process alive — timers, `BroadcastChannel`, sockets — and gives
16
+ * each an `unref()` that says "you are still live, but do not be the reason
17
+ * this process refuses to exit". The browser's equivalents have no such method:
18
+ * `setTimeout` there returns a plain number, and nothing is holding a page open
19
+ * in the first place.
20
+ *
21
+ * So the call is genuinely conditional at runtime, in every package that runs
22
+ * in both places. Spelled out at the call site it came to
23
+ * `(handle as unknown as { unref?: () => void }).unref?.()` — a double cast,
24
+ * because where the DOM declaration of `setTimeout` wins the handle is a
25
+ * `number`, and `number` does not overlap an object type, so a single `as` is
26
+ * refused. The `unknown` in the middle was there to make the refusal go away,
27
+ * and it also discarded the check on the member's name and signature. There
28
+ * were seven copies, which is seven chances to get one of those wrong.
29
+ *
30
+ * Takes `unknown` on purpose: the argument's type is exactly what is not known
31
+ * here, and narrowing it to a timer would be a claim this cannot make about a
32
+ * `BroadcastChannel`. A no-op on a browser handle, on `null`/`undefined`, and
33
+ * on a Node timer that has already fired.
34
+ */
35
+ function unref(handle) {
36
+ if (handle === null || typeof handle !== "object") return;
37
+ const fn = handle.unref;
38
+ if (typeof fn === "function") fn.call(handle);
39
+ }
40
+ //#endregion
41
+ //#region ../common/src/data/row-etag.ts
42
+ /**
43
+ * A row's version — the `ETag` the read that produced it reported — kept on
44
+ * the row without being part of it.
45
+ *
46
+ * @module
47
+ */
48
+ /**
49
+ * Where a fetched row's version is kept.
50
+ *
51
+ * Non-enumerable, so it does not appear in `Object.keys`, in `JSON.stringify`,
52
+ * in a spread into an update body, or in the generated `Row` type — a row's
53
+ * version is a fact about the *response*, not a column, and putting it in the
54
+ * row proper would mean the server having to strip it back off every write.
55
+ *
56
+ * A string key rather than a symbol so it survives the structured clone an
57
+ * offline store or a worker boundary does… which it does not, and that is the
58
+ * honest position: an etag is per-read, and a row that has been through a cache
59
+ * has not been read from the server. `etagOf` answers `undefined` there, and a
60
+ * conditional write without a tag is an unconditional one.
61
+ */
62
+ var ETAG_KEY = "__etag";
63
+ /**
64
+ * The version of a row, when it came straight from a read that reported one.
65
+ *
66
+ * ```ts
67
+ * const post = await client.data.posts.get(1);
68
+ * await client.data.posts.update(1, { title: "New" }, { ifMatch: etagOf(post) });
69
+ * ```
70
+ *
71
+ * Set on a row from `findById()`/`get()`, from `update()`, and on every row
72
+ * `listenById()` delivers (the SDK asks the server to tag each frame).
73
+ * `undefined` for a row from `find()` (a list response carries one ETag at
74
+ * most, and it would not be this row's), from the offline cache, or from a
75
+ * server that does not send them. Passing `undefined` as `ifMatch` sends no
76
+ * precondition, so the call above degrades to an ordinary update rather than
77
+ * failing — which is the right default, and the reason to read this at the call
78
+ * site where the choice is visible.
79
+ */
80
+ function etagOf(row) {
81
+ if (!row || typeof row !== "object") return void 0;
82
+ const value = row[ETAG_KEY];
83
+ return typeof value === "string" ? value : void 0;
84
+ }
85
+ /**
86
+ * Attach a version to a row without making it part of the row: a
87
+ * non-enumerable `__etag`, so it stays out of `Object.keys`, a spread and
88
+ * `JSON.stringify`, and a row copied through any of them has no tag — which
89
+ * is right, since the copy was not read from the server.
90
+ */
91
+ function withETag(row, etag) {
92
+ if (!etag || !row || typeof row !== "object") return row;
93
+ Object.defineProperty(row, ETAG_KEY, {
94
+ value: etag,
95
+ enumerable: false,
96
+ configurable: true,
97
+ writable: true
98
+ });
99
+ return row;
100
+ }
101
+ //#endregion
102
+ //#region ../client/dist/offline-store-Cm5sAX81.js
103
+ function rebaseReviver(_key, value) {
104
+ if (value && typeof value === "object" && "__type" in value) {
105
+ const record = value;
106
+ switch (record.__type) {
107
+ case "date":
108
+ case "Date": {
109
+ if (typeof record.value !== "string") return value;
110
+ const date = new Date(record.value);
111
+ return isNaN(date.getTime()) ? null : date;
112
+ }
113
+ case "reference":
114
+ case "EntityReference": return new EntityReference({
115
+ id: String(record.id),
116
+ path: record.path,
117
+ driver: record.driver,
118
+ databaseId: record.databaseId
119
+ });
120
+ case "relation":
121
+ case "EntityRelation": return new EntityRelation(record.id, record.path, record.data);
122
+ case "GeoPoint": return new GeoPoint(record.latitude, record.longitude);
123
+ case "Vector": return new Vector(record.value);
124
+ default: return value;
125
+ }
126
+ }
127
+ return value;
128
+ }
129
+ /**
130
+ * Could `text` hold an object with a `__type` key — the only values
131
+ * {@link rebaseReviver} changes?
132
+ *
133
+ * A key is spelled either literally or with `\u` escapes, so a body with
134
+ * neither cannot contain one. The check is two substring scans, which run at
135
+ * memory speed; a false positive only costs the walk below.
136
+ */
137
+ function mayHoldTypedValues(text) {
138
+ return text.includes("\"__type\"") || text.includes("\\u");
139
+ }
140
+ /**
141
+ * Set a property the way `JSON.parse`'s reviver does: as an own data property.
142
+ * A plain assignment to a key named `__proto__` would call the setter and swap
143
+ * the object's prototype instead.
144
+ */
145
+ function setRevived(target, key, value) {
146
+ if (key === "__proto__") Object.defineProperty(target, key, {
147
+ value,
148
+ writable: true,
149
+ enumerable: true,
150
+ configurable: true
151
+ });
152
+ else target[key] = value;
153
+ }
154
+ /**
155
+ * {@link rebaseReviver}, applied bottom-up to an already parsed value, in
156
+ * place: every child is revived before its parent, so a relation's `data` holds
157
+ * its own revived values by the time the relation is built from it — the order
158
+ * `JSON.parse` calls a reviver in. Primitives are never visited; the reviver
159
+ * hands every one of them back unchanged.
160
+ */
161
+ function reviveTree(value) {
162
+ if (value === null || typeof value !== "object") return value;
163
+ if (Array.isArray(value)) {
164
+ for (let i = 0; i < value.length; i++) {
165
+ const item = value[i];
166
+ if (item !== null && typeof item === "object") {
167
+ const revived = reviveTree(item);
168
+ if (revived !== item) value[i] = revived;
169
+ }
170
+ }
171
+ return value;
172
+ }
173
+ const record = value;
174
+ for (const key of Object.keys(record)) {
175
+ const item = record[key];
176
+ if (item !== null && typeof item === "object") {
177
+ const revived = reviveTree(item);
178
+ if (revived !== item) setRevived(record, key, revived);
179
+ }
180
+ }
181
+ return rebaseReviver("", record);
182
+ }
183
+ /**
184
+ * `JSON.parse(text, rebaseReviver)`, without the reviver's cost where it has
185
+ * nothing to do.
186
+ *
187
+ * Handing `JSON.parse` a reviver takes V8 off its fast path and calls back for
188
+ * every key and every value, primitives included: about 7× a plain parse on a
189
+ * 1,000-row page, 10 ms of main thread per 600 KB. The reviver only ever
190
+ * changes objects carrying `__type`, and a REST page of flat rows has none. So
191
+ * this parses plainly and walks the result only when the text can hold one —
192
+ * and the walk visits objects, never primitives.
193
+ *
194
+ * Same result, instance for instance: the same values revived, bottom-up, as
195
+ * own data properties, and nothing deleted (the reviver never answers
196
+ * `undefined`).
197
+ *
198
+ * @throws SyntaxError exactly where `JSON.parse` does.
199
+ */
200
+ function parseRebaseJson(text) {
201
+ const parsed = JSON.parse(text);
202
+ if (!mayHoldTypedValues(text)) return parsed;
203
+ return reviveTree(parsed);
204
+ }
205
+ /**
206
+ * True when there is no browser to have signed a user in — a Node script, a
207
+ * cron job, an edge worker.
208
+ *
209
+ * Anonymous is an ordinary, correct state in a browser: before sign-in, on a
210
+ * marketing page, for public reads. Warning there would be noise that teaches
211
+ * people to ignore warnings, so the guard is off entirely. This uses the same
212
+ * `typeof window` test as {@link resolveBaseUrl}, and additionally treats a
213
+ * defined `document` as a browser so an SSR shim or test harness that installs
214
+ * only one of the two is still excluded.
215
+ */
216
+ function isServerLikeEnvironment() {
217
+ return typeof window === "undefined" && typeof document === "undefined";
218
+ }
219
+ /**
220
+ * Emitted once per client. Kept as a constant so the wording is testable and
221
+ * greppable — this is the string a user will paste into a search.
222
+ */
223
+ var ANONYMOUS_SERVER_CLIENT_WARNING = "[rebase] This client was created outside a browser with no credential — no `token`, no auth token getter, and no cookie auth flow — so every request runs as an anonymous caller. Row-level security will return only publicly readable rows, which is usually nothing and occasionally the wrong thing. Inside a cron or function handler, use the `rebase` you were handed instead of building a new one: its data plane is already admin-scoped. In a standalone script or job, pass the service key as `token`. If you really do want anonymous access, pass `anonymous: true` to silence this.";
224
+ /**
225
+ * Refuse a filter whose *value* is missing.
226
+ *
227
+ * `where: { status: ["==", undefined] }` used to serialize to the literal
228
+ * string, so `status=eq.undefined` went out on the wire and the server dutifully
229
+ * looked for rows whose status is the four-letter word "undefined". The caller
230
+ * saw an empty page, not an error — the classic shape of a variable that was
231
+ * never set.
232
+ *
233
+ * Dropping the condition instead would be worse than sending it: the query
234
+ * would come back *unfiltered*, which for an ownership or tenant filter means
235
+ * returning rows the caller never asked to see. So this is a hard error, and
236
+ * both correct spellings are named in the message: omit the key to skip the
237
+ * filter, or use `["is-null", null]` to match SQL NULL (which still
238
+ * serializes — `null` is a value, `undefined` is the absence of one).
239
+ */
240
+ function assertNoUndefinedFilterValues(where) {
241
+ const reject = (field, op) => {
242
+ throw new RebaseClientError(`Filter on "${field}" has an undefined value (["${String(op)}", undefined]). Omit "${field}" from \`where\` to skip the filter, or use ["is-null", null] to match SQL NULL.`, {
243
+ code: "INVALID_FILTER",
244
+ details: {
245
+ field,
246
+ operator: String(op)
247
+ }
248
+ });
249
+ };
250
+ for (const [field, condition] of Object.entries(where)) {
251
+ if (condition === void 0) continue;
252
+ if (!Array.isArray(condition)) continue;
253
+ const tuples = Array.isArray(condition[0]) ? condition : [condition];
254
+ for (const tuple of tuples) {
255
+ if (!Array.isArray(tuple) || tuple.length !== 2) continue;
256
+ const [op, value] = tuple;
257
+ if (value === void 0) reject(field, op);
258
+ if (Array.isArray(value) && value.some((v) => v === void 0)) reject(field, op);
259
+ }
260
+ }
261
+ }
262
+ /**
263
+ * A find's parameters as `[name, value]` pairs, in the order the URL carries
264
+ * them, values not yet URL-encoded.
265
+ *
266
+ * One list for both spellings of a listing: {@link buildQueryString} encodes it
267
+ * into the `GET`'s URL, and {@link buildQueryBody} gathers it into the body of
268
+ * `POST …/query`, which the server turns back into exactly these parameters.
269
+ * Two serializers would be two chances to send a query one way and not the
270
+ * other.
271
+ */
272
+ function findParamPairs(params, deleted) {
273
+ const pairs = [];
274
+ if (deleted) pairs.push(["deleted", deleted]);
275
+ if (!params) return pairs;
276
+ resolveFindWindow(params);
277
+ if (params.limit != null) pairs.push(["limit", String(params.limit)]);
278
+ if (params.offset != null) pairs.push(["offset", String(params.offset)]);
279
+ if (params.page != null) pairs.push(["page", String(params.page)]);
280
+ if (params.after) pairs.push(["after", params.after]);
281
+ if (params.count === "none") pairs.push(["count", "none"]);
282
+ if (params.orderBy) {
283
+ const wire = serializeOrderBy(params.orderBy);
284
+ if (wire) pairs.push(["orderBy", wire]);
285
+ }
286
+ if (params.searchString) {
287
+ pairs.push(["searchString", params.searchString]);
288
+ if (params.searchExplain) pairs.push(["searchExplain", "true"]);
289
+ }
290
+ if (params.vectorSearch) {
291
+ const vs = params.vectorSearch;
292
+ pairs.push(["vector_search", vs.property]);
293
+ pairs.push(["vector", JSON.stringify(vs.vector)]);
294
+ if (vs.distance) pairs.push(["vector_distance", vs.distance]);
295
+ if (vs.threshold !== void 0) pairs.push(["vector_threshold", String(vs.threshold)]);
296
+ }
297
+ const include = serializeInclude(params.include);
298
+ if (include) pairs.push(["include", include]);
299
+ if (params.fields && params.fields.length > 0) pairs.push(["fields", params.fields.join(",")]);
300
+ if (params.distinct) pairs.push(["distinct", "true"]);
301
+ if (params.logical) {
302
+ const root = params.logical;
303
+ const serialized = (root.conditions ?? []).map(serializeLogicalCondition).join(",");
304
+ pairs.push([root.type, `(${serialized})`]);
305
+ }
306
+ if (params.where) appendWhere(pairs, params.where);
307
+ return pairs;
308
+ }
309
+ var encodePairs = (pairs) => pairs.length > 0 ? "?" + pairs.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join("&") : "";
310
+ function buildQueryString(params, deleted) {
311
+ return encodePairs(findParamPairs(params, deleted));
312
+ }
313
+ /**
314
+ * The body of `POST /data/<collection>/query`: the parameters
315
+ * {@link buildQueryString} puts in the URL, spelled as the URL spells them —
316
+ * each value a string, and a parameter the URL repeats (a filter with two
317
+ * conditions on one field) a list of them. The server reads it back into the
318
+ * same parameters and runs the same listing.
319
+ */
320
+ function buildQueryBody(params, deleted) {
321
+ const body = {};
322
+ for (const [name, value] of findParamPairs(params, deleted)) {
323
+ const held = body[name];
324
+ body[name] = held === void 0 ? value : [...Array.isArray(held) ? held : [held], value];
325
+ }
326
+ return body;
327
+ }
328
+ /**
329
+ * Append a `where` to a query string, one `?<field>=<op>.<value>` parameter per
330
+ * condition.
331
+ *
332
+ * Except on a column whose name the server reads as a parameter of its own —
333
+ * `page`, `select`, `hard`, `where` ({@link RESERVED_QUERY_KEYS}). A filter on
334
+ * such a column sent as its own parameter is read as that parameter, and the
335
+ * filter is never applied. Those conditions go inside `?where=` instead, in the
336
+ * same dot-string spelling, which the server decodes through the same codec and
337
+ * merges in. A collection with no such column sends exactly what it did before.
338
+ */
339
+ function appendWhere(pairs, where) {
340
+ assertNoUndefinedFilterValues(where);
341
+ const serialized = serializeFilter(where);
342
+ const reserved = {};
343
+ for (const [field, value] of Object.entries(serialized)) {
344
+ if (RESERVED_QUERY_KEYS.has(field)) {
345
+ reserved[field] = value;
346
+ continue;
347
+ }
348
+ for (const v of Array.isArray(value) ? value : [value]) pairs.push([field, v]);
349
+ }
350
+ if (Object.keys(reserved).length > 0) pairs.push(["where", JSON.stringify(reserved)]);
351
+ }
352
+ /**
353
+ * The query string for `GET /<collection>/aggregate`.
354
+ *
355
+ * `?select=sum(total),count()` is SQL's spelling, because whoever writes an
356
+ * aggregate is thinking in SQL and any other spelling has to be learned first —
357
+ * and because it is what the route already parses. The filters are serialised
358
+ * by exactly the same code a `find()` uses, so "revenue by status, this month"
359
+ * narrows the same rows whichever call is asking.
360
+ *
361
+ * `include` and a cursor are deliberately not here: an aggregate has no
362
+ * relations to load and no row to continue after. `limit`, `offset` and
363
+ * `orderBy` are, and page and sort the *groups* — `orderBy` by a group column or
364
+ * an aggregate's alias, in the spelling a `find()` sends.
365
+ */
366
+ function buildAggregateQueryString(params) {
367
+ const pairs = [];
368
+ const select = params.select.map((entry) => `${entry.fn}(${entry.field ?? ""})`).join(",");
369
+ pairs.push(["select", select]);
370
+ if (params.groupBy && params.groupBy.length > 0) pairs.push(["groupBy", params.groupBy.join(",")]);
371
+ if (params.limit != null) pairs.push(["limit", String(params.limit)]);
372
+ if (params.offset != null) pairs.push(["offset", String(params.offset)]);
373
+ if (params.orderBy) {
374
+ const wire = serializeOrderBy(params.orderBy);
375
+ if (wire) pairs.push(["orderBy", wire]);
376
+ }
377
+ if (params.searchString) pairs.push(["searchString", params.searchString]);
378
+ if (params.logical) {
379
+ const root = params.logical;
380
+ const serialized = (root.conditions ?? []).map(serializeLogicalCondition).join(",");
381
+ pairs.push([root.type, `(${serialized})`]);
382
+ }
383
+ if (params.where) appendWhere(pairs, params.where);
384
+ return encodePairs(pairs);
385
+ }
386
+ /**
387
+ * The 401 codes that mean "the credential on this request is no good", which a
388
+ * refreshed token can fix.
389
+ *
390
+ * The rest of the 401s are answers about something the caller *sent*: a wrong
391
+ * current password (`INVALID_CREDENTIALS`), a mistyped MFA code (`INVALID_CODE`,
392
+ * `CHALLENGE_EXHAUSTED`), a sign-in that needs a second factor
393
+ * (`MFA_REQUIRED`). Refreshing and resending one of those rotated the session
394
+ * for nothing, sent the same wrong answer twice — and an MFA challenge claims
395
+ * an attempt before it judges the code, so every typo cost two of its five.
396
+ */
397
+ var TOKEN_REFUSAL_CODES = /* @__PURE__ */ new Set([
398
+ "UNAUTHORIZED",
399
+ "INVALID_TOKEN",
400
+ "TOKEN_EXPIRED",
401
+ "SESSION_REVOKED"
402
+ ]);
403
+ /**
404
+ * Whether a 401's body says the token was the problem.
405
+ *
406
+ * A 401 with no code at all counts: it is what an older server, a proxy or
407
+ * anything not speaking the envelope sends, and there the token is the
408
+ * likeliest cause — refreshing is what this client always did with it.
409
+ */
410
+ function isTokenRefusal(body) {
411
+ const error = body?.error;
412
+ const code = error && typeof error === "object" ? error.code : void 0;
413
+ return typeof code !== "string" || code === "" || TOKEN_REFUSAL_CODES.has(code);
414
+ }
415
+ /**
416
+ * The base every request and every caller-built URL resolves against.
417
+ *
418
+ * `baseUrl` is optional because the common production shape is a Rebase
419
+ * backend serving its own SPA, where the API is simply the page's origin.
420
+ * Leaving it unset is therefore the *correct* configuration there — and the
421
+ * one that keeps working when a second hostname (a custom domain) points at
422
+ * the same app.
423
+ *
424
+ * When unset in a browser this resolves to the page origin rather than "".
425
+ * Requests behave identically either way, but the empty string is a trap for
426
+ * anything that builds a URL from `client.baseUrl`: `new URL("" + path)`
427
+ * throws, so apps "fixed" it by baking an absolute host into their bundle —
428
+ * which is exactly what breaks the day a custom domain is added, and which no
429
+ * amount of CORS configuration repairs, because a SameSite=Lax auth cookie is
430
+ * not sent cross-site either.
431
+ */
432
+ /**
433
+ * The code an error carries when the response did not name one: a proxy's 413,
434
+ * a load balancer's 502, a crash page's 500 — anything without the server's
435
+ * envelope. Without it `e.code` was `undefined` and the documented
436
+ * `switch (e.code)` fell through to `default` on exactly the failures that are
437
+ * not Rebase's own. Only codes the backend itself answers with, so a switch
438
+ * written against the error reference catches both.
439
+ *
440
+ * @internal
441
+ */
442
+ function defaultCodeForStatus(status) {
443
+ switch (status) {
444
+ case 400: return "BAD_REQUEST";
445
+ case 401: return "UNAUTHORIZED";
446
+ case 403: return "FORBIDDEN";
447
+ case 404: return "NOT_FOUND";
448
+ case 409: return "CONFLICT";
449
+ case 412: return "PRECONDITION_FAILED";
450
+ case 413: return "PAYLOAD_TOO_LARGE";
451
+ case 415: return "UNSUPPORTED_MEDIA_TYPE";
452
+ case 429: return "RATE_LIMITED";
453
+ case 502:
454
+ case 503:
455
+ case 504: return "SERVICE_UNAVAILABLE";
456
+ default: return status >= 500 && status < 600 ? "INTERNAL_ERROR" : void 0;
457
+ }
458
+ }
459
+ function resolveBaseUrl(configured) {
460
+ if (configured) return configured.replace(/\/$/, "");
461
+ if (typeof window !== "undefined" && window.location?.origin) return window.location.origin;
462
+ return "";
463
+ }
464
+ function createTransport(config, environment) {
465
+ const rawFetch = config.fetch || globalThis.fetch;
466
+ /**
467
+ * `fetch`, but its rejection is a `RebaseApiError` like everything else
468
+ * this client throws.
469
+ *
470
+ * A transport failure — DNS, a refused connection, CORS, an aborted
471
+ * request — rejects with whatever the runtime's `fetch` felt like: a
472
+ * `TypeError` reading "Failed to fetch" in a browser, a `TypeError` with a
473
+ * `cause` in undici, a `DOMException` on abort. So the one class the SDK
474
+ * documents as "a `catch` block only ever needs to check for this" did not
475
+ * cover the most common failure of all, and `e.status` / `e.code` were
476
+ * undefined on the error every app hits first: the server being down.
477
+ *
478
+ * `status: 0` rather than a made-up 5xx. There was no response, and 0 is
479
+ * how `XMLHttpRequest` has always spelled that; a fabricated 503 would be
480
+ * indistinguishable from one the server actually sent. The original error
481
+ * is on `cause`, so nothing is hidden.
482
+ *
483
+ * Wrapped once, at construction, because the request path reads `fetchFn`
484
+ * twice — the first attempt and the post-refresh retry — and a wrapper
485
+ * applied at one call site is a wrapper missing from the other.
486
+ */
487
+ const fetchFn = async (input, init) => {
488
+ try {
489
+ return await rawFetch(input, init);
490
+ } catch (e) {
491
+ if (e instanceof RebaseApiError) throw e;
492
+ const url = typeof input === "string" ? input : String(input.url ?? input);
493
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(url) && typeof window === "undefined") throw new RebaseClientError(`This client has no \`baseUrl\`, and outside a browser the relative URL ${url} cannot be fetched. Pass \`baseUrl\` (e.g. "http://localhost:3001") to createRebaseClient().`, {
494
+ code: "BASE_URL_REQUIRED",
495
+ cause: e
496
+ });
497
+ throw new RebaseApiError(`Could not reach the server at ${url}: ${e instanceof Error ? e.message : String(e)}`, {
498
+ status: 0,
499
+ code: "NETWORK_ERROR",
500
+ cause: e
501
+ });
502
+ }
503
+ };
504
+ const apiPath = config.apiPath || "/api";
505
+ for (const field of ["baseUrl", "storageUrlOrigin"]) {
506
+ const value = config[field];
507
+ if (!value || !apiPath) continue;
508
+ const trimmed = value.replace(/\/+$/, "");
509
+ if (!trimmed.endsWith(apiPath)) continue;
510
+ console.warn(`[Rebase] ${field} ${JSON.stringify(value)} already ends with the API path ${JSON.stringify(apiPath)}, which is appended to it — requests will go to ${trimmed}${apiPath}/… and 404. Pass the origin only (${JSON.stringify(trimmed.slice(0, trimmed.length - apiPath.length) || "/")}), or set \`apiPath\` if the server really does mount the API one level deeper.`);
511
+ }
512
+ /**
513
+ * The token `setToken` last set — the auth module sets it on every sign-in,
514
+ * refresh and sign-out — or, until anything has, `config.token`.
515
+ *
516
+ * `config.token` is read each time a token is needed rather than copied
517
+ * here once, so a config that declares `token` as a getter hands over what
518
+ * its source holds at that moment: the Rebase console lends its own,
519
+ * background-refreshed session to a tenant client that way. The realtime
520
+ * socket resolves its token through this transport, so the two read it
521
+ * alike.
522
+ */
523
+ let setTokenValue;
524
+ const heldToken = () => setTokenValue ? setTokenValue.token : config.token;
525
+ let tokenGetter;
526
+ let tokenRefresher;
527
+ let onUnauthorizedHandler = config.onUnauthorized;
528
+ /** Once per client, never per request — log spam is its own bug. */
529
+ let anonymousWarningIssued = false;
530
+ /**
531
+ * Warn a server-side caller that it built a client that can only ever be
532
+ * anonymous. Deliberately checked at the *first request* rather than at
533
+ * construction: `setToken()` / `setAuthTokenGetter()` and a server-side
534
+ * `auth.signIn…()` (which calls `transport.setToken`) all land after the
535
+ * constructor, and warning at construction would fire on every one of them.
536
+ */
537
+ function warnIfAnonymousServerClient(activeToken) {
538
+ if (anonymousWarningIssued) return;
539
+ if (activeToken) return;
540
+ if (tokenGetter) return;
541
+ if (config.anonymous) return;
542
+ if (environment?.credentialOutOfBand) return;
543
+ if (!isServerLikeEnvironment()) return;
544
+ anonymousWarningIssued = true;
545
+ console.warn(ANONYMOUS_SERVER_CLIENT_WARNING);
546
+ }
547
+ const defaultHeaders = {
548
+ ...config.headers ?? {},
549
+ ...config.schemaVersion ? { [SCHEMA_VERSION_HEADER]: config.schemaVersion } : {},
550
+ ...config.impersonate !== void 0 ? { [IMPERSONATE_HEADER]: config.impersonate } : {}
551
+ };
552
+ function getHeaders(activeToken, init) {
553
+ const headers = {
554
+ "Content-Type": "application/json",
555
+ ...defaultHeaders,
556
+ ...activeToken ? { Authorization: `Bearer ${activeToken}` } : {},
557
+ ...init?.headers || {}
558
+ };
559
+ if (init?.body instanceof FormData) delete headers["Content-Type"];
560
+ return headers;
561
+ }
562
+ /**
563
+ * The refusal for a success status carrying a body this client cannot read.
564
+ *
565
+ * The first 120 characters go in the message because they identify the
566
+ * sender at a glance: `<!doctype html>` says "you are talking to a web
567
+ * server, not to this API" faster than any wording here could.
568
+ *
569
+ * One function for both the first attempt and the post-refresh retry — the
570
+ * retry is a second copy of this whole response-reading path, and copies
571
+ * are how one of them ends up fixed and the other not.
572
+ */
573
+ function unreadableResponse(status, text, contentType) {
574
+ const type = contentType?.split(";")[0].trim().toLowerCase() || "";
575
+ return new RebaseApiError(`The server answered ${status} with a body that is not JSON, so there is nothing to return. ${type !== "" && type !== "text/html" && !type.includes("json") ? `The body is ${type}. If this is a custom function that answers text or a file, ask for it with \`functions.invoke(name, payload, { responseType: "text" | "blob" | "response" })\`.` : "This usually means the request reached something other than the Rebase API — a single-page-app fallback serving index.html, or a proxy error page — so check the API URL configuration (e.g. VITE_API_URL)."} The body began: ${JSON.stringify(text.slice(0, 120))}`, {
576
+ status,
577
+ code: "INVALID_JSON_RESPONSE",
578
+ ...type ? { details: { contentType: type } } : {}
579
+ });
580
+ }
581
+ /**
582
+ * The server always emits the canonical
583
+ * `{ error: { message, code, details?, requestId? } }` envelope (formatted
584
+ * by the central `errorHandler`), so a field is read strictly from
585
+ * `body.error.*` and never from the top level.
586
+ */
587
+ function getErrorField(obj, field) {
588
+ const err = obj?.error;
589
+ if (err && typeof err === "object" && err !== null) return err[field];
590
+ }
591
+ /**
592
+ * `Retry-After` as whole seconds, or `undefined`.
593
+ *
594
+ * The header is either a number of seconds or an HTTP date; both spellings
595
+ * are legal and servers use both, so both are read. A date in the past —
596
+ * which happens on a clock skew — is clamped to 0 rather than negative.
597
+ */
598
+ function parseRetryAfter(header) {
599
+ if (!header) return void 0;
600
+ const seconds = Number(header.trim());
601
+ if (Number.isFinite(seconds)) return Math.max(0, Math.floor(seconds));
602
+ const at = Date.parse(header);
603
+ if (Number.isNaN(at)) return void 0;
604
+ return Math.max(0, Math.round((at - Date.now()) / 1e3));
605
+ }
606
+ /**
607
+ * Everything a failed response says about itself, in one place.
608
+ *
609
+ * The request path reads a response twice — the first attempt and the
610
+ * post-refresh retry — and the two used to build their error separately.
611
+ * Which is how they came to disagree: both dropped `requestId` and
612
+ * `Retry-After`, and a fix applied to one would have missed the other.
613
+ */
614
+ function errorFrom(res, body, fallbackMessage) {
615
+ const requestId = getErrorField(body, "requestId") ?? res.headers?.get("X-Request-ID") ?? void 0;
616
+ return new RebaseApiError(String(getErrorField(body, "message") || fallbackMessage || `Request failed with status ${res.status}`), {
617
+ status: res.status,
618
+ code: getErrorField(body, "code") || defaultCodeForStatus(res.status),
619
+ details: getErrorField(body, "details"),
620
+ ...typeof requestId === "string" && requestId ? { requestId } : {},
621
+ ...(() => {
622
+ const retryAfter = parseRetryAfter(res.headers?.get("Retry-After") ?? null);
623
+ return retryAfter === void 0 ? {} : { retryAfterSeconds: retryAfter };
624
+ })()
625
+ });
626
+ }
627
+ /**
628
+ * The token a request should carry: the getter's when one is installed,
629
+ * otherwise the one held — or, when the token refresher says the held one
630
+ * is spent, the one it hands back once its refresh lands.
631
+ */
632
+ async function tokenForRequest() {
633
+ if (tokenGetter) {
634
+ try {
635
+ const fetched = await tokenGetter();
636
+ if (fetched !== null && fetched !== void 0) return fetched;
637
+ } catch (e) {}
638
+ return heldToken();
639
+ }
640
+ const pending = tokenRefresher?.();
641
+ if (pending) try {
642
+ const fresh = await pending;
643
+ if (fresh) return fresh;
644
+ } catch (e) {}
645
+ return heldToken();
646
+ }
647
+ /** The body as JSON, `{}` for none, `undefined` when it is there and is not JSON. */
648
+ function parseBody(text) {
649
+ if (!text) return {};
650
+ try {
651
+ return parseRebaseJson(text);
652
+ } catch (e) {
653
+ return;
654
+ }
655
+ }
656
+ /**
657
+ * Send a request and hand back the response that answers it: the token
658
+ * attached, a token refusal answered by one refresh and resend, and an
659
+ * error status thrown as a {@link RebaseApiError}. The body of a success is
660
+ * left unread, for the caller to read in whatever form it expects.
661
+ *
662
+ * One path for the first attempt and the post-refresh retry: the two used
663
+ * to be separate copies of this whole sequence, and copies are how one of
664
+ * them ends up fixed and the other not.
665
+ */
666
+ async function requestRaw(path, init, meta) {
667
+ const url = resolveBaseUrl(config.baseUrl) + apiPath + path;
668
+ const activeToken = await tokenForRequest();
669
+ warnIfAnonymousServerClient(activeToken);
670
+ let res = await fetchFn(url, {
671
+ ...init,
672
+ headers: getHeaders(activeToken, init)
673
+ });
674
+ let readText;
675
+ if (res.status === 401 && onUnauthorizedHandler) {
676
+ readText = await res.text().catch(() => "");
677
+ if (isTokenRefusal(parseBody(readText) ?? {})) {
678
+ if (await onUnauthorizedHandler()) {
679
+ let retryToken = heldToken();
680
+ if (tokenGetter) try {
681
+ const fetched = await tokenGetter();
682
+ if (fetched !== null && fetched !== void 0) retryToken = fetched;
683
+ } catch (e) {}
684
+ res = await fetchFn(url, {
685
+ ...init,
686
+ headers: getHeaders(retryToken, init)
687
+ });
688
+ readText = void 0;
689
+ }
690
+ }
691
+ }
692
+ if (meta) {
693
+ meta.status = res.status;
694
+ meta.etag = res.headers?.get?.("ETag") ?? void 0;
695
+ meta.headers = res.headers;
696
+ }
697
+ if (!res.ok) {
698
+ const text = readText ?? await res.text().catch(() => "");
699
+ let fallbackMessage = res.statusText;
700
+ if (res.status === 404 && !fallbackMessage) fallbackMessage = `Endpoint not found (${init?.method || "GET"} ${path}). This usually means the collection is not registered on the backend, or the frontend API URL configuration (e.g. VITE_API_URL) is missing or pointing to the wrong host.`;
701
+ throw errorFrom(res, parseBody(text) ?? {}, fallbackMessage);
702
+ }
703
+ return res;
704
+ }
705
+ async function request(path, init, meta) {
706
+ const res = await requestRaw(path, init, meta);
707
+ if (res.status === 204) return void 0;
708
+ const text = await res.text().catch(() => "");
709
+ /**
710
+ * `undefined` when the body was there and could not be read as JSON.
711
+ *
712
+ * On a *success* status that is the whole answer, and `{}` was being
713
+ * returned as though the server had sent it: `find()` answered `{}`
714
+ * instead of an array, `getOne()` an empty object, with nothing thrown.
715
+ *
716
+ * The case that produces it is not exotic. Point `VITE_API_URL` at the
717
+ * frontend's own host and `/api/data/posts` lands on the SPA fallback,
718
+ * which answers `200` with `index.html` — so the misconfiguration the
719
+ * 404 message spends four lines explaining reaches the caller, in its
720
+ * most common form, as an empty success.
721
+ */
722
+ const body = parseBody(text);
723
+ if (body === void 0) throw unreadableResponse(res.status, text, res.headers?.get?.("Content-Type") ?? null);
724
+ return body;
725
+ }
726
+ return {
727
+ request,
728
+ requestRaw,
729
+ setToken(newToken) {
730
+ setTokenValue = { token: newToken || void 0 };
731
+ },
732
+ setAuthTokenGetter(getter) {
733
+ tokenGetter = getter;
734
+ },
735
+ setOnUnauthorized(handler) {
736
+ onUnauthorizedHandler = handler;
737
+ },
738
+ setTokenRefresher(refresher) {
739
+ tokenRefresher = refresher ?? void 0;
740
+ },
741
+ get baseUrl() {
742
+ return resolveBaseUrl(config.baseUrl);
743
+ },
744
+ get apiPath() {
745
+ return apiPath;
746
+ },
747
+ get storageUrlOrigin() {
748
+ return config.storageUrlOrigin?.replace(/\/$/, "") || void 0;
749
+ },
750
+ get fetchFn() {
751
+ return fetchFn;
752
+ },
753
+ getHeaders: (init) => getHeaders(heldToken(), init),
754
+ resolveToken: async () => await tokenForRequest() || null
755
+ };
756
+ }
757
+ /**
758
+ * SDK Query Builder — returns flat rows (`FindResult<M>`) instead of
759
+ * Entity-wrapped results (`FindResponse<M>`).
760
+ *
761
+ * @example
762
+ * const { data } = await rebase.data.posts
763
+ * .where("status", "==", "published")
764
+ * .orderBy("created_at", "desc")
765
+ * .limit(10)
766
+ * .find();
767
+ *
768
+ * console.log(data[0].title); // flat access
769
+ */
770
+ var SDKQueryBuilder = class {
771
+ collection;
772
+ params = { where: {} };
773
+ constructor(collection) {
774
+ this.collection = collection;
775
+ }
776
+ where(columnOrCondition, operator, value) {
777
+ if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
778
+ const next = columnOrCondition;
779
+ this.params.logical = this.params.logical ? {
780
+ type: "and",
781
+ conditions: [this.params.logical, next]
782
+ } : next;
783
+ return this;
784
+ }
785
+ if (!this.params.where) this.params.where = {};
786
+ const column = columnOrCondition;
787
+ const condition = [operator, value];
788
+ const existing = this.params.where[column];
789
+ if (existing === void 0) this.params.where[column] = condition;
790
+ else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
791
+ else {
792
+ let firstCondition;
793
+ if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
794
+ else firstCondition = ["==", existing];
795
+ this.params.where[column] = [firstCondition, condition];
796
+ }
797
+ return this;
798
+ }
799
+ /**
800
+ * Order the results by a specific column.
801
+ *
802
+ * Call it again to add a tie-breaker rather than replace the sort: keys
803
+ * apply in the order they were added, so
804
+ * `.orderBy("roles").orderBy("created_at", "desc")` sorts by role and
805
+ * shows the newest first within each one.
806
+ */
807
+ orderBy(column, direction = "asc", nulls) {
808
+ const existing = normalizeOrderBy(this.params.orderBy) ?? [];
809
+ const key = sortKeyToString(column);
810
+ this.params.orderBy = [...existing, nulls ? [
811
+ key,
812
+ direction,
813
+ nulls
814
+ ] : [key, direction]];
815
+ return this;
816
+ }
817
+ /**
818
+ * Limit the number of results returned.
819
+ */
820
+ limit(count) {
821
+ this.params.limit = count;
822
+ return this;
823
+ }
824
+ /**
825
+ * Skip the first N results.
826
+ */
827
+ offset(count) {
828
+ this.params.offset = count;
829
+ return this;
830
+ }
831
+ /**
832
+ * Set a free-text search string if supported by the backend.
833
+ *
834
+ * By default this is a substring match across the collection's top-level
835
+ * string properties. A Postgres collection that declares a `search` block
836
+ * gets ranked full-text matching over the fields it named instead, and each
837
+ * row comes back with a `_score` you can sort on:
838
+ *
839
+ * ```ts
840
+ * client.data.talents.search("auditor iso 14001").orderBy("_score", "desc").find()
841
+ * ```
842
+ *
843
+ * Pass `{ explain: true }` to have each row report which of the declared
844
+ * fields matched, with a highlighted snippet, on `_matches`:
845
+ *
846
+ * ```ts
847
+ * const { data } = await client.data.talents.search("iso 14001", { explain: true }).find();
848
+ * data[0]._matches
849
+ * // [{ field: "questionnaire.certifications", snippet: "<mark>ISO</mark> <mark>14001</mark> Lead Auditor" }]
850
+ * ```
851
+ */
852
+ search(searchString, options) {
853
+ this.params.searchString = searchString;
854
+ if (options?.explain !== void 0) this.params.searchExplain = options.explain;
855
+ return this;
856
+ }
857
+ /**
858
+ * Order rows by nearest-neighbour distance to `vector`.
859
+ *
860
+ * The server has supported this from the REST layer since vectors landed;
861
+ * this is the SDK reaching it. Results come back closest-first with a
862
+ * `_distance` on each row, and any `where` / `orderBy` on the same query is
863
+ * a filter applied before the ordering — distance decides the order.
864
+ *
865
+ * You supply the query vector. Rebase stores and searches embeddings; it
866
+ * does not produce them, so this is where whatever model you already use
867
+ * for the stored vectors gets called.
868
+ *
869
+ * @param property - Name of the `vector` property to compare against.
870
+ * @param vector - The query embedding. Its length must match the property's
871
+ * declared `dimensions`, or the server answers 400.
872
+ * @example
873
+ * client.data.docs.vectorSearch("embedding", queryVector, { threshold: 0.35 }).limit(10).find()
874
+ */
875
+ vectorSearch(property, vector, options) {
876
+ this.params.vectorSearch = {
877
+ property,
878
+ vector,
879
+ ...options?.distance !== void 0 && { distance: options.distance },
880
+ ...options?.threshold !== void 0 && { threshold: options.threshold }
881
+ };
882
+ return this;
883
+ }
884
+ /**
885
+ * Load related rows into the response, in place of their foreign keys.
886
+ *
887
+ * Three spellings, all the same request:
888
+ *
889
+ * ```ts
890
+ * client.data.posts.include("tags", "author") // names
891
+ * client.data.posts.include("comments.author") // a dotted path
892
+ * client.data.posts.include({ // parametrised
893
+ * comments: {
894
+ * limit: 5,
895
+ * where: { published: ["==", true] },
896
+ * orderBy: ["created_at", "desc"],
897
+ * include: { author: true }
898
+ * }
899
+ * })
900
+ * ```
901
+ *
902
+ * Up to three hops deep, and `"*"` still loads every relation one hop
903
+ * deep. A name that is not a relation of the collection is a 400
904
+ * `UNKNOWN_RELATION` — it used to be ignored, which answers 200 with the
905
+ * field missing and reads exactly like a row that has no related row.
906
+ *
907
+ * Repeated calls **merge**: `.include("author").include("tags")` asks for
908
+ * both. Assigning here — which is what it used to do — meant the second
909
+ * call silently discarded the first.
910
+ */
911
+ include(...relations) {
912
+ this.params.include = mergeIncludeSpecs(this.params.include, relations);
913
+ return this;
914
+ }
915
+ /**
916
+ * Return only these columns.
917
+ *
918
+ * A projection at the database, not a trim of the response: a query that
919
+ * needs two fields of a wide row reads two columns. The primary key always
920
+ * comes back regardless — a row that cannot be addressed cannot be
921
+ * updated, deleted or paged past — and `excludeFromApi` columns stay
922
+ * hidden whether or not they are named.
923
+ */
924
+ fields(...columns) {
925
+ this.params.fields = [...this.params.fields ?? [], ...columns];
926
+ return this;
927
+ }
928
+ /**
929
+ * Collapse rows that are identical over the columns being returned.
930
+ *
931
+ * Only meaningful alongside {@link fields}: the primary key is always in
932
+ * the projection, so without narrowing it every row is already distinct.
933
+ *
934
+ * ```ts
935
+ * client.data.posts.fields("status").distinct().find() // the statuses in use
936
+ * ```
937
+ */
938
+ distinct(enabled = true) {
939
+ this.params.distinct = enabled;
940
+ return this;
941
+ }
942
+ /**
943
+ * Continue after a previous page's `meta.nextCursor` — keyset paging.
944
+ *
945
+ * Unlike `offset`, a row inserted or deleted before the cursor cannot
946
+ * shift the window, so a walk neither repeats nor skips rows. Keep
947
+ * `orderBy` identical across pages: a cursor only continues the listing it
948
+ * came from, and one used against a different sort is refused rather than
949
+ * seeked in an order nobody asked for.
950
+ */
951
+ after(cursor) {
952
+ this.params.after = cursor;
953
+ return this;
954
+ }
955
+ /**
956
+ * Execute the find query and return the results as flat rows.
957
+ */
958
+ async find() {
959
+ return this.collection.find(this.params);
960
+ }
961
+ /**
962
+ * Aggregate the rows this query matches instead of returning them.
963
+ *
964
+ * The builder's `where`/`logical`/`search` narrow which rows are
965
+ * aggregated; its `orderBy`, `include` and window do not apply — an
966
+ * aggregate has no rows to sort, no relations to load and no page to
967
+ * continue.
968
+ *
969
+ * ```ts
970
+ * await client.data.orders
971
+ * .where("created_at", ">=", startOfMonth)
972
+ * .aggregate({ select: [{ fn: "sum", field: "total" }], groupBy: ["status"] });
973
+ * // [{ status: "paid", sum_total: 41822.5 }, …]
974
+ * ```
975
+ */
976
+ async aggregate(params) {
977
+ return this.collection.aggregate({
978
+ ...params,
979
+ where: this.params.where,
980
+ logical: this.params.logical,
981
+ searchString: this.params.searchString
982
+ });
983
+ }
984
+ /**
985
+ * Page through everything this query matches, one row at a time.
986
+ *
987
+ * `.limit()` on the builder becomes the page size, so the ceiling on a
988
+ * single `find()` is not a ceiling on what the query can read.
989
+ */
990
+ iterate(options) {
991
+ return this.collection.iterate({
992
+ ...this.params,
993
+ ...this.params.limit !== void 0 && { pageSize: this.params.limit },
994
+ ...options
995
+ });
996
+ }
997
+ /** Collect everything this query matches into one array. */
998
+ findAll(options) {
999
+ return this.collection.findAll({
1000
+ ...this.params,
1001
+ ...this.params.limit !== void 0 && { pageSize: this.params.limit },
1002
+ ...options
1003
+ });
1004
+ }
1005
+ /**
1006
+ * Count the records matching this query.
1007
+ */
1008
+ async count() {
1009
+ return this.collection.count(this.params);
1010
+ }
1011
+ /**
1012
+ * Listen to realtime updates matching this query.
1013
+ *
1014
+ * The guard that used to stand here — `if (!this.collection.listen) throw`
1015
+ * — has moved onto the client itself, which is where the same failure had
1016
+ * to be diagnosed anyway: `client.data.posts.listen(…)`, one call short of
1017
+ * this builder, answered `undefined is not a function`. The message is now
1018
+ * the same either way.
1019
+ */
1020
+ listen(onUpdate, onError) {
1021
+ return this.collection.listen(this.params, onUpdate, onError);
1022
+ }
1023
+ };
1024
+ /**
1025
+ * What `listen`/`listenById` say on a client that has no socket.
1026
+ *
1027
+ * One sentence, in one place: it used to live in `SDKQueryBuilder.listen` only,
1028
+ * so the same failure reached callers of `client.data.posts.listen` as
1029
+ * `undefined is not a function`.
1030
+ */
1031
+ var NO_SOCKET = "Listen is only available when RebaseClient is configured with a websocketUrl, and not when it was created with realtime: false.";
1032
+ /**
1033
+ * Why a live query cannot be opened for these parameters, or `undefined` when
1034
+ * it can.
1035
+ *
1036
+ * A `listen()` is the same query as the `find()` beside it, so what `find()`
1037
+ * refuses it refuses too. The socket would not: a frame is JSON, where an
1038
+ * `undefined` filter value becomes `null`, so `["!=", uid]` with `uid` unset
1039
+ * subscribed to every row whose column is NOT NULL — an ownership filter
1040
+ * widened instead of refused. And `after` names a place in one run of the
1041
+ * query, which a subscription re-runs on every write; the server keeps no
1042
+ * cursor for one, so it was dropped, and page one streamed to a caller who had
1043
+ * asked for page two.
1044
+ */
1045
+ function listenRefusal(params) {
1046
+ if (params?.where) try {
1047
+ assertNoUndefinedFilterValues(params.where);
1048
+ } catch (error) {
1049
+ if (error instanceof RebaseClientError) return error;
1050
+ throw error;
1051
+ }
1052
+ if (params?.after) return new RebaseClientError("listen() cannot continue from a cursor: a subscription re-runs its query on every write, and `after` names a place in one run of it. Use `offset` or `page` for a live window, or `find({ after })` to read the page once.", { code: "CURSOR_NOT_LIVE" });
1053
+ }
1054
+ /**
1055
+ * Counts currently in flight, **per client**, keyed by the exact request they
1056
+ * issue. Entries live only for the duration of the request — see `count()` for
1057
+ * why they exist at all.
1058
+ *
1059
+ * The map used to be module-level, shared by every client in the process. A
1060
+ * request key is a path and a query string: it says nothing about *who* is
1061
+ * asking. So two clients holding different credentials — an admin panel beside
1062
+ * a signed-in user's client, a per-request server client in a Node process, a
1063
+ * test asserting one client's calls — counting the same collection would share
1064
+ * one request, and whichever arrived second was answered with the first one's
1065
+ * total. Row-level security makes those genuinely different numbers.
1066
+ *
1067
+ * Keyed on the transport instead, which is the object a credential lives on.
1068
+ * A `WeakMap`, so a closed client's entry goes with it.
1069
+ */
1070
+ var inflightCounts = /* @__PURE__ */ new WeakMap();
1071
+ /** The in-flight map for one transport, created on first use. */
1072
+ function inflightCountsFor(transport) {
1073
+ let map = inflightCounts.get(transport);
1074
+ if (!map) {
1075
+ map = /* @__PURE__ */ new Map();
1076
+ inflightCounts.set(transport, map);
1077
+ }
1078
+ return map;
1079
+ }
1080
+ /**
1081
+ * Transports whose server reads `?count=none` as a filter on a column named
1082
+ * `count`.
1083
+ *
1084
+ * Every server released before the parameter existed does, and answers a 400
1085
+ * `UNKNOWN_FILTER_FIELD` naming `count`. Skipping the count only ever saves
1086
+ * the server work, so a read refused for it is sent again without it, and
1087
+ * later reads through the same transport (one per client, so one server) stop
1088
+ * sending it. Membership is never cleared: a server does not lose the
1089
+ * parameter, and one that gains it in an upgrade gets it again on the next
1090
+ * page load.
1091
+ */
1092
+ var serversWithoutCountParam = /* @__PURE__ */ new WeakSet();
1093
+ /**
1094
+ * The longest URL a `find()` sends as a `GET`. Past it the parameters go in
1095
+ * the body of `POST /data/<collection>/query` instead.
1096
+ *
1097
+ * Not the server's own ceiling but the one in front of it: nginx refuses a
1098
+ * request line over 8 KB by default (414), and a proxy or CDN is in front of
1099
+ * most deployments. 6 KB leaves the rest of the request line, and the headers
1100
+ * some proxies count against the same buffer, room.
1101
+ */
1102
+ var MAX_FIND_URL_LENGTH = 6 * 1024;
1103
+ /**
1104
+ * Transports whose server has no `POST …/query` — every server before 0.24.
1105
+ *
1106
+ * Such a server answers the POST 404 (or a proxy 405), and the read is sent
1107
+ * again as the `GET` it has always been, which may well be refused for its
1108
+ * length: that refusal is the old server's answer, and the caller sees it.
1109
+ * Membership is never cleared, as for {@link serversWithoutCountParam}.
1110
+ */
1111
+ var serversWithoutQueryRoute = /* @__PURE__ */ new WeakSet();
1112
+ /** {@link serversWithoutQueryRoute}, for `POST …/count`. */
1113
+ var serversWithoutCountRoute = /* @__PURE__ */ new WeakSet();
1114
+ /**
1115
+ * Whether a read goes as its `POST` twin rather than `GET`: always for a
1116
+ * vector search, whose embedding is a JSON array in the URL (768 dimensions
1117
+ * are 17 KB of it, past Node's 16 KB and nginx's 8 KB), and for anything else
1118
+ * whose URL would not fit through a proxy.
1119
+ */
1120
+ function sendsQueryBody(transport, path, queryString, params, serversWithoutRoute) {
1121
+ if (serversWithoutRoute.has(transport)) return false;
1122
+ if (params?.vectorSearch) return true;
1123
+ return `${transport.baseUrl ?? ""}${transport.apiPath ?? ""}${path}${queryString}`.length > MAX_FIND_URL_LENGTH;
1124
+ }
1125
+ /**
1126
+ * A read whose parameters go in the URL of `GET <path>`, or — where
1127
+ * {@link sendsQueryBody} says so — in the JSON body of its `POST` twin:
1128
+ * `POST …/query` for a listing, `POST …/count` for a count.
1129
+ *
1130
+ * Against a server without the twin (404, or a proxy's 405) the `GET` is sent
1131
+ * instead, and the transport remembers — unless the `GET` 404s as well, which
1132
+ * is the collection's answer, not the route's.
1133
+ */
1134
+ /**
1135
+ * A success whose body is not the shape the method reads: no body at all (a
1136
+ * `204`, or a `200` with nothing in it), or JSON without the page's `data`
1137
+ * list. Usually a proxy or a route that is not the Rebase API answering. It
1138
+ * used to resolve as an empty page, or the row `{}`, or throw a bare
1139
+ * `TypeError` — none of which `switch (e.code)` could tell from a real answer.
1140
+ */
1141
+ function unexpectedResponse(slug, method, status, expected) {
1142
+ return new RebaseApiError(`${method} on "${slug}" got ${status === 204 ? "a 204 with no body" : `a ${status} without ${expected}`} from the server. This usually means something other than the Rebase API answered — a proxy, or a route outside the API — so check the API URL configuration.`, {
1143
+ status,
1144
+ code: "UNEXPECTED_RESPONSE"
1145
+ });
1146
+ }
1147
+ /**
1148
+ * A bulk call given something other than an array. A `RebaseClientError` with
1149
+ * the code the server uses for the same mistake in a request body, so one
1150
+ * `case "INVALID_BULK_BODY"` covers both.
1151
+ */
1152
+ function invalidBulkBody(message) {
1153
+ return new RebaseClientError(message, { code: "INVALID_BULK_BODY" });
1154
+ }
1155
+ async function readWithParams(transport, path, postPath, params, serversWithoutRoute, deleted) {
1156
+ const queryString = buildQueryString(params, deleted);
1157
+ const get = () => transport.request(path + queryString, { method: "GET" });
1158
+ if (!sendsQueryBody(transport, path, queryString, params, serversWithoutRoute)) return get();
1159
+ try {
1160
+ return await transport.request(postPath, {
1161
+ method: "POST",
1162
+ body: JSON.stringify(buildQueryBody(params, deleted))
1163
+ });
1164
+ } catch (error) {
1165
+ if (!(error instanceof RebaseApiError) || error.status !== 404 && error.status !== 405) throw error;
1166
+ try {
1167
+ const answer = await get();
1168
+ serversWithoutRoute.add(transport);
1169
+ return answer;
1170
+ } catch (getError) {
1171
+ if (!(getError instanceof RebaseApiError && getError.status === 404)) serversWithoutRoute.add(transport);
1172
+ throw getError;
1173
+ }
1174
+ }
1175
+ }
1176
+ function isCountReadAsFilter(error) {
1177
+ if (!(error instanceof RebaseApiError) || error.status !== 400 || error.code !== "UNKNOWN_FILTER_FIELD") return false;
1178
+ const details = error.details;
1179
+ return typeof details === "object" && details !== null && "field" in details && details.field === "count";
1180
+ }
1181
+ /**
1182
+ * The headers a write's options ask for.
1183
+ *
1184
+ * One function rather than a spread at each call site: the three headers are
1185
+ * easy to add to one method and forget on the next, which is exactly how single
1186
+ * `PATCH` and `DELETE` came to send an `Idempotency-Key` the server never read
1187
+ * and `delete()` came to have no options at all.
1188
+ */
1189
+ function writeHeaders(options) {
1190
+ const headers = {};
1191
+ if (options?.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
1192
+ if (options?.ifMatch) headers["If-Match"] = options.ifMatch;
1193
+ if (options?.returning === false) headers["Prefer"] = "return=minimal";
1194
+ return Object.keys(headers).length > 0 ? { headers } : void 0;
1195
+ }
1196
+ /** What `softDelete: true` names the field a delete stamps. */
1197
+ var DEFAULT_SOFT_DELETE_FIELD = "deletedAt";
1198
+ /**
1199
+ * The part of {@link ObserveByIdOptions} that is a read option: `include`, and
1200
+ * nothing else, so `realtime` never travels as a query parameter.
1201
+ */
1202
+ function byIdRead(options) {
1203
+ return options?.include !== void 0 ? { include: options.include } : void 0;
1204
+ }
1205
+ /**
1206
+ * The bulk route names the row a failure came from — "Row 0 of 1 (id 7)
1207
+ * failed: …" — which is what a caller needs from a 10,000-row import and noise
1208
+ * from `upsert()`, which the caller wrote as one row and which only travels as
1209
+ * a one-row batch. Drops that prefix and nothing else: the error keeps its
1210
+ * class, status, code, details and cause. `createMany` does not call this.
1211
+ */
1212
+ var ONE_ROW_BULK_PREFIX = /^Row 0 of 1 \(.*?\) failed: /;
1213
+ function withoutBulkRowPrefix(error) {
1214
+ if (error instanceof Error && ONE_ROW_BULK_PREFIX.test(error.message)) error.message = error.message.replace(ONE_ROW_BULK_PREFIX, "");
1215
+ return error;
1216
+ }
1217
+ function createCollectionClient(transport, slug, ws) {
1218
+ const basePath = `/data/${slug}`;
1219
+ /**
1220
+ * A listing — the live rows, or with `deleted` the trash's. One reader for
1221
+ * both, so the trash takes every parameter, and every fallback for an
1222
+ * older server, that `find()` does.
1223
+ */
1224
+ async function findRows(params, deleted) {
1225
+ const method = deleted ? "trash.find()" : "find()";
1226
+ const read = async (p) => {
1227
+ const page = await readWithParams(transport, basePath, `${basePath}/query`, p, serversWithoutQueryRoute, deleted);
1228
+ if (!page || !Array.isArray(page.data)) throw unexpectedResponse(slug, method, page ? 200 : 204, "a page with a `data` list");
1229
+ return page;
1230
+ };
1231
+ const counted = {
1232
+ ...params,
1233
+ count: void 0
1234
+ };
1235
+ let raw;
1236
+ if (params?.count !== "none") raw = await read(params);
1237
+ else if (serversWithoutCountParam.has(transport)) raw = await read(counted);
1238
+ else try {
1239
+ raw = await read(params);
1240
+ } catch (error) {
1241
+ if (!isCountReadAsFilter(error)) throw error;
1242
+ serversWithoutCountParam.add(transport);
1243
+ raw = await read(counted);
1244
+ }
1245
+ return {
1246
+ data: raw.data,
1247
+ meta: raw.meta
1248
+ };
1249
+ }
1250
+ /** A count — of the live rows, or with `deleted` of the trash's. */
1251
+ async function countRows(params, deleted) {
1252
+ const countParams = {
1253
+ ...params,
1254
+ limit: void 0,
1255
+ offset: void 0,
1256
+ include: void 0,
1257
+ count: void 0
1258
+ };
1259
+ const qs = buildQueryString(countParams, deleted);
1260
+ const key = basePath + "/count" + qs;
1261
+ const inflight = inflightCountsFor(transport);
1262
+ const existing = inflight.get(key);
1263
+ if (existing) return existing;
1264
+ const request = readWithParams(transport, `${basePath}/count`, `${basePath}/count`, countParams, serversWithoutCountRoute, deleted).then((raw) => raw.count ?? 0);
1265
+ inflight.set(key, request);
1266
+ try {
1267
+ return await request;
1268
+ } finally {
1269
+ inflight.delete(key);
1270
+ }
1271
+ }
1272
+ const restoreField = (options) => options?.field ?? DEFAULT_SOFT_DELETE_FIELD;
1273
+ const client = {
1274
+ find(params) {
1275
+ return findRows(params);
1276
+ },
1277
+ trash: {
1278
+ find: (params) => findRows(params, "only"),
1279
+ count: (params) => countRows(params, "only"),
1280
+ restore: (id, options) => client.update(id, { [restoreField(options)]: null }, options),
1281
+ restoreMany: (ids, options) => {
1282
+ if (!Array.isArray(ids)) return Promise.reject(invalidBulkBody("trash.restoreMany expects an array of ids."));
1283
+ const data = { [restoreField(options)]: null };
1284
+ return client.updateMany(ids.map((id) => ({
1285
+ id,
1286
+ data
1287
+ })), options);
1288
+ },
1289
+ async purge(id, options) {
1290
+ await transport.request(`${basePath}/${encodeURIComponent(String(id))}?hard=true`, {
1291
+ method: "DELETE",
1292
+ ...writeHeaders(options)
1293
+ });
1294
+ },
1295
+ async purgeMany(ids, options) {
1296
+ if (!Array.isArray(ids)) throw invalidBulkBody("trash.purgeMany expects an array of ids.");
1297
+ if (ids.length === 0) return;
1298
+ await transport.request(`${basePath}/bulk/delete?hard=true`, {
1299
+ method: "POST",
1300
+ body: JSON.stringify({ ids }),
1301
+ ...writeHeaders(options)
1302
+ });
1303
+ }
1304
+ },
1305
+ iterate(params) {
1306
+ return paginateFind((p) => client.find(p), params, slug);
1307
+ },
1308
+ findAll(params) {
1309
+ return collectAllPages((p) => client.find(p), params, slug);
1310
+ },
1311
+ async findById(id, options) {
1312
+ try {
1313
+ const meta = {};
1314
+ const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}${buildQueryString({ include: options?.include })}`, { method: "GET" }, meta);
1315
+ if (!raw) return void 0;
1316
+ if (Object.keys(raw).length === 0) throw unexpectedResponse(slug, "findById()", meta.status ?? 200, "a row");
1317
+ return withETag(raw, meta.etag);
1318
+ } catch (err) {
1319
+ if (err instanceof RebaseApiError && err.status === 404) return;
1320
+ throw err;
1321
+ }
1322
+ },
1323
+ async get(id) {
1324
+ const row = await client.findById(id);
1325
+ if (row === void 0) throw new RebaseApiError(`No record with id ${JSON.stringify(String(id))} in "${slug}".`, {
1326
+ status: 404,
1327
+ code: "NOT_FOUND"
1328
+ });
1329
+ return row;
1330
+ },
1331
+ async create(data, id, options) {
1332
+ const body = { ...data };
1333
+ if (id !== void 0) body.id = id;
1334
+ return await transport.request(basePath, {
1335
+ method: "POST",
1336
+ body: JSON.stringify(body),
1337
+ ...writeHeaders(options)
1338
+ });
1339
+ },
1340
+ /**
1341
+ * Insert, or replace the row already occupying the conflict target.
1342
+ *
1343
+ * One statement server-side (`INSERT ... ON CONFLICT DO UPDATE`), so
1344
+ * unlike a `findById` followed by `create`-or-`update` it cannot lose
1345
+ * the race between the two.
1346
+ *
1347
+ * Sent as a one-row bulk write with `upsert: true`, which is what the
1348
+ * in-process accessor and the offline queue already send. It used to be
1349
+ * `POST /data/<slug>` with `?on_conflict=` only when a target was
1350
+ * named — and the route reads that POST without the parameter as the
1351
+ * plain insert a `create` is, so the documented default ("on the
1352
+ * primary key") never happened online: re-upserting an existing row
1353
+ * answered 409, while the same call succeeded offline and in a
1354
+ * function.
1355
+ */
1356
+ async upsert(data, options) {
1357
+ const raw = await transport.request(`${basePath}/bulk`, {
1358
+ method: "POST",
1359
+ body: JSON.stringify({
1360
+ rows: [data],
1361
+ upsert: true,
1362
+ ...options?.onConflict?.length ? { onConflict: options.onConflict } : {}
1363
+ }),
1364
+ ...writeHeaders(options)
1365
+ }).catch((error) => {
1366
+ throw withoutBulkRowPrefix(error);
1367
+ });
1368
+ return options?.returning === false ? void 0 : raw?.data?.[0];
1369
+ },
1370
+ async createMany(data, options) {
1371
+ if (!Array.isArray(data)) throw invalidBulkBody("createMany expects an array of records.");
1372
+ if (data.length === 0) return [];
1373
+ const raw = await transport.request(`${basePath}/bulk`, {
1374
+ method: "POST",
1375
+ body: JSON.stringify({
1376
+ rows: data,
1377
+ ...options?.upsert ? { upsert: true } : {},
1378
+ ...options?.onConflict?.length ? { onConflict: options.onConflict } : {}
1379
+ }),
1380
+ ...writeHeaders(options)
1381
+ });
1382
+ if (options?.returning === false) return [];
1383
+ return raw.data || [];
1384
+ },
1385
+ /**
1386
+ * `PATCH`, the verb for a merge — `update(id, data: Partial<M>)` sends
1387
+ * only the keys the caller named and the server merges the rest.
1388
+ *
1389
+ * `PATCH` is the verb the OpenAPI spec names, so a spec-validating
1390
+ * gateway in front of the API sees the operation this sends. The
1391
+ * server also answers `PUT` on the same route, as a deprecated alias
1392
+ * marked `Deprecation: true` and left out of the spec; this client
1393
+ * never sends it.
1394
+ */
1395
+ async update(id, data, options) {
1396
+ const meta = {};
1397
+ return withETag(await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
1398
+ method: "PATCH",
1399
+ body: JSON.stringify(data),
1400
+ ...writeHeaders(options)
1401
+ }, meta), meta.etag);
1402
+ },
1403
+ async updateMany(updates, options) {
1404
+ if (!Array.isArray(updates)) throw invalidBulkBody("updateMany expects an array of { id, data } entries.");
1405
+ if (updates.length === 0) return [];
1406
+ const raw = await transport.request(`${basePath}/bulk`, {
1407
+ method: "PATCH",
1408
+ body: JSON.stringify({ updates }),
1409
+ ...writeHeaders(options)
1410
+ });
1411
+ if (options?.returning === false) return [];
1412
+ return raw.data || [];
1413
+ },
1414
+ async delete(id, options) {
1415
+ await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
1416
+ method: "DELETE",
1417
+ ...writeHeaders(options)
1418
+ });
1419
+ },
1420
+ /**
1421
+ * `POST .../bulk/delete`, not `DELETE .../bulk`.
1422
+ *
1423
+ * The honest verb would take the ids in a DELETE body, and that is the
1424
+ * one request shape the HTTP ecosystem handles unreliably: bodies on
1425
+ * DELETE are permitted but widely dropped by proxies and CDNs, and
1426
+ * several OpenAPI generators ignore `requestBody` on a DELETE
1427
+ * operation, so a generated client would send the request without its
1428
+ * ids. A backend deployed behind arbitrary ingress cannot take that
1429
+ * bet. Same reason `:batchDelete` exists in Google's API guidelines.
1430
+ */
1431
+ async deleteMany(ids, options) {
1432
+ if (!Array.isArray(ids)) throw invalidBulkBody("deleteMany expects an array of ids.");
1433
+ if (ids.length === 0) return;
1434
+ await transport.request(`${basePath}/bulk/delete`, {
1435
+ method: "POST",
1436
+ body: JSON.stringify({ ids }),
1437
+ ...writeHeaders(options)
1438
+ });
1439
+ },
1440
+ count(params) {
1441
+ return countRows(params);
1442
+ },
1443
+ observe(params, onResult, onError, options) {
1444
+ let closed = false;
1445
+ let liveDelivered = false;
1446
+ let signature;
1447
+ const deliver = (result, fromLive) => {
1448
+ if (closed) return;
1449
+ if (fromLive) liveDelivered = true;
1450
+ else if (liveDelivered) return;
1451
+ const next = `${result.meta?.total ?? ""}|${JSON.stringify(result.data)}`;
1452
+ if (signature !== void 0 && next === signature) return;
1453
+ signature = next;
1454
+ onResult({
1455
+ ...result,
1456
+ fromCache: false,
1457
+ hasPendingWrites: false,
1458
+ partial: false
1459
+ });
1460
+ };
1461
+ const wantsLive = options?.realtime !== false && !isUnsupported(client.listen);
1462
+ const refusal = wantsLive ? listenRefusal(params) : void 0;
1463
+ if (refusal) {
1464
+ queueMicrotask(() => {
1465
+ if (!closed) onError?.(refusal);
1466
+ });
1467
+ return () => {
1468
+ closed = true;
1469
+ };
1470
+ }
1471
+ const live = wantsLive ? client.listen(params, (result) => deliver(result, true), onError) : void 0;
1472
+ if (!liveDelivered) client.find(params).then((result) => deliver(result, false)).catch((error) => {
1473
+ if (!closed) onError?.(error);
1474
+ });
1475
+ return () => {
1476
+ closed = true;
1477
+ live?.();
1478
+ };
1479
+ },
1480
+ observeById(id, onResult, onError, options) {
1481
+ let closed = false;
1482
+ let liveDelivered = false;
1483
+ let signature;
1484
+ const deliver = (row, fromLive) => {
1485
+ if (closed) return;
1486
+ if (fromLive) liveDelivered = true;
1487
+ else if (liveDelivered) return;
1488
+ const next = row === void 0 ? "\0missing" : JSON.stringify(row);
1489
+ if (signature !== void 0 && next === signature) return;
1490
+ signature = next;
1491
+ onResult(row, {
1492
+ fromCache: false,
1493
+ hasPendingWrites: false
1494
+ });
1495
+ };
1496
+ const live = options?.realtime !== false && !isUnsupported(client.listenById) ? client.listenById(id, (row) => deliver(row, true), onError, byIdRead(options)) : void 0;
1497
+ if (!liveDelivered) client.findById(id, byIdRead(options)).then((row) => deliver(row, false)).catch((error) => {
1498
+ if (!closed) onError?.(error);
1499
+ });
1500
+ return () => {
1501
+ closed = true;
1502
+ live?.();
1503
+ };
1504
+ },
1505
+ where(columnOrCondition, operator, value) {
1506
+ const builder = new SDKQueryBuilder(client);
1507
+ if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
1508
+ return builder.where(columnOrCondition, operator, value);
1509
+ },
1510
+ orderBy(column, direction, nulls) {
1511
+ return new SDKQueryBuilder(client).orderBy(column, direction, nulls);
1512
+ },
1513
+ limit(count) {
1514
+ return new SDKQueryBuilder(client).limit(count);
1515
+ },
1516
+ offset(count) {
1517
+ return new SDKQueryBuilder(client).offset(count);
1518
+ },
1519
+ search(searchString, options) {
1520
+ return new SDKQueryBuilder(client).search(searchString, options);
1521
+ },
1522
+ vectorSearch(property, vector, options) {
1523
+ return new SDKQueryBuilder(client).vectorSearch(property, vector, options);
1524
+ },
1525
+ include(...relations) {
1526
+ return new SDKQueryBuilder(client).include(...relations);
1527
+ },
1528
+ fields(...columns) {
1529
+ return new SDKQueryBuilder(client).fields(...columns);
1530
+ },
1531
+ distinct(enabled) {
1532
+ return new SDKQueryBuilder(client).distinct(enabled);
1533
+ },
1534
+ after(cursor) {
1535
+ return new SDKQueryBuilder(client).after(cursor);
1536
+ },
1537
+ /**
1538
+ * `count`/`sum`/`avg`/`min`/`max` over the matching rows, optionally
1539
+ * grouped — `GET /<collection>/aggregate`.
1540
+ *
1541
+ * The REST route has served this since aggregates landed and the SDK
1542
+ * had no method for it, so the only way to reach it from a typed client
1543
+ * was to hand-build the URL. `findAll()` and a reduce is the thing this
1544
+ * exists to replace: wrong under a `limit`, unaffordable without one.
1545
+ */
1546
+ async aggregate(params) {
1547
+ const qs = buildAggregateQueryString(params);
1548
+ const raw = await transport.request(`${basePath}/aggregate${qs}`, { method: "GET" });
1549
+ const rows = raw.data || [];
1550
+ if (raw.meta) Object.defineProperty(rows, "meta", {
1551
+ value: raw.meta,
1552
+ enumerable: false
1553
+ });
1554
+ return rows;
1555
+ },
1556
+ listen: unsupportedMethod(NO_SOCKET),
1557
+ listenById: unsupportedMethod(NO_SOCKET),
1558
+ listenCount: unsupportedMethod(NO_SOCKET)
1559
+ };
1560
+ if (ws) {
1561
+ client.listen = (params, onUpdate, onError) => {
1562
+ let active = true;
1563
+ const refusal = listenRefusal(params);
1564
+ if (refusal) {
1565
+ queueMicrotask(() => {
1566
+ if (active) onError?.(refusal);
1567
+ });
1568
+ return () => {
1569
+ active = false;
1570
+ };
1571
+ }
1572
+ let lastUpdateId = 0;
1573
+ let lastKnownTotal;
1574
+ const window = resolveFindWindow(params);
1575
+ const unsub = ws.listenCollection({
1576
+ path: slug,
1577
+ filter: params?.where,
1578
+ logical: params?.logical,
1579
+ limit: params?.limit,
1580
+ offset: window.driverOffset,
1581
+ page: params?.page,
1582
+ include: params?.include,
1583
+ fields: params?.fields,
1584
+ distinct: params?.distinct,
1585
+ orderBy: normalizeOrderBy(params?.orderBy),
1586
+ searchString: params?.searchString,
1587
+ searchExplain: params?.searchExplain,
1588
+ vectorSearch: params?.vectorSearch
1589
+ }, (incomingRows, frameMeta) => {
1590
+ const currentUpdateId = ++lastUpdateId;
1591
+ const requestedLimit = window.limit;
1592
+ const offset = window.offset;
1593
+ const rows = incomingRows;
1594
+ const emit = (total, hasMore, nextCursor) => {
1595
+ if (!active || currentUpdateId !== lastUpdateId) return;
1596
+ onUpdate({
1597
+ data: rows,
1598
+ meta: {
1599
+ total,
1600
+ limit: frameMeta?.limit ?? requestedLimit,
1601
+ offset: frameMeta?.offset ?? offset,
1602
+ hasMore,
1603
+ ...nextCursor && { nextCursor }
1604
+ }
1605
+ });
1606
+ };
1607
+ if (frameMeta && frameMeta.total !== void 0) {
1608
+ lastKnownTotal = frameMeta.total;
1609
+ emit(frameMeta.total, frameMeta.hasMore, frameMeta.nextCursor);
1610
+ return;
1611
+ }
1612
+ if (lastKnownTotal !== void 0) {
1613
+ emit(lastKnownTotal, offset + rows.length < lastKnownTotal, frameMeta?.nextCursor);
1614
+ return;
1615
+ }
1616
+ client.count(params).then((total) => {
1617
+ lastKnownTotal = total;
1618
+ emit(total, offset + rows.length < total, frameMeta?.nextCursor);
1619
+ }).catch(() => {
1620
+ emit(offset + rows.length, rows.length >= requestedLimit, frameMeta?.nextCursor);
1621
+ });
1622
+ }, onError);
1623
+ return () => {
1624
+ active = false;
1625
+ unsub();
1626
+ };
1627
+ };
1628
+ /**
1629
+ * The count as a subscription of its own: `countOnly` asks the server
1630
+ * to read no rows. One that predates it reads the single row
1631
+ * `limit: 1` asks for and counts as usual, and the frame's `meta`
1632
+ * answers either way. One that sends no `meta` at all is asked with
1633
+ * `count()` on each frame instead.
1634
+ */
1635
+ client.listenCount = (params, onCount, onError) => {
1636
+ let active = true;
1637
+ let last;
1638
+ const emit = (total) => {
1639
+ if (!active || total === last) return;
1640
+ last = total;
1641
+ onCount(total);
1642
+ };
1643
+ const unsub = ws.listenCollection({
1644
+ path: slug,
1645
+ filter: params?.where,
1646
+ logical: params?.logical,
1647
+ searchString: params?.searchString,
1648
+ limit: 1,
1649
+ countOnly: true
1650
+ }, (_rows, meta) => {
1651
+ if (typeof meta?.total === "number") emit(meta.total);
1652
+ else if (!meta) client.count(params).then(emit).catch((error) => {
1653
+ if (active) onError?.(error instanceof Error ? error : new Error(String(error)));
1654
+ });
1655
+ }, onError);
1656
+ return () => {
1657
+ active = false;
1658
+ unsub();
1659
+ };
1660
+ };
1661
+ client.listenById = (id, onUpdate, onError, options) => {
1662
+ return ws.listenOne({
1663
+ path: slug,
1664
+ id: String(id),
1665
+ ...options?.include !== void 0 ? { include: options.include } : {}
1666
+ }, (row) => {
1667
+ if (row) onUpdate(row);
1668
+ else onUpdate(void 0);
1669
+ }, onError);
1670
+ };
1671
+ }
1672
+ return client;
1673
+ }
1674
+ /**
1675
+ * Monotonic within a tab, unique across tabs, and sortable as a plain string:
1676
+ * `<ms base36, padded>-<counter>-<random>`. The padding is what keeps
1677
+ * lexicographic order equal to chronological order, and the random suffix is
1678
+ * what stops two tabs from writing the same queue key in the same millisecond
1679
+ * — which would silently drop one of the two writes.
1680
+ */
1681
+ var mutationCounter = 0;
1682
+ function createMutationId(now = Date.now()) {
1683
+ return `${now.toString(36).padStart(10, "0")}-${(mutationCounter = (mutationCounter + 1) % 1679616).toString(36).padStart(4, "0")}-${Math.random().toString(36).slice(2, 10).padStart(8, "0")}`;
1684
+ }
1685
+ /**
1686
+ * In-memory store: the default outside the browser and the workhorse of the
1687
+ * test suite. Values are deep-copied on the way in and out so a caller
1688
+ * mutating a returned row cannot silently edit the "persisted" copy — the
1689
+ * IndexedDB implementation gets the same guarantee for free from structured
1690
+ * cloning, and the two must not differ in aliasing behaviour.
1691
+ */
1692
+ var MemoryOfflineStore = class {
1693
+ cache = /* @__PURE__ */ new Map();
1694
+ queue = /* @__PURE__ */ new Map();
1695
+ async getCache(key) {
1696
+ const entry = this.cache.get(key);
1697
+ return entry ? structuredClone(entry) : void 0;
1698
+ }
1699
+ async setCache(key, entry) {
1700
+ this.cache.set(key, structuredClone(entry));
1701
+ }
1702
+ async setCacheMany(entries) {
1703
+ for (const { key, entry } of entries) this.cache.set(key, structuredClone(entry));
1704
+ }
1705
+ async deleteCache(keys) {
1706
+ for (const key of keys) this.cache.delete(key);
1707
+ }
1708
+ async listCache(prefix) {
1709
+ const out = [];
1710
+ for (const [key, entry] of this.cache) if (key.startsWith(prefix)) out.push({
1711
+ key,
1712
+ cachedAt: entry.cachedAt
1713
+ });
1714
+ return out;
1715
+ }
1716
+ async listCacheEntries(prefix) {
1717
+ const out = [];
1718
+ for (const [key, entry] of this.cache) if (key.startsWith(prefix)) out.push({
1719
+ key,
1720
+ ...structuredClone(entry)
1721
+ });
1722
+ out.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
1723
+ return out;
1724
+ }
1725
+ async enqueue(key, mutation) {
1726
+ this.queue.set(key, structuredClone(mutation));
1727
+ }
1728
+ async dequeue(key) {
1729
+ this.queue.delete(key);
1730
+ }
1731
+ async listQueue(prefix) {
1732
+ return [...this.queue.entries()].filter(([key]) => key.startsWith(prefix)).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, mutation]) => structuredClone(mutation));
1733
+ }
1734
+ async clear(prefix) {
1735
+ for (const key of [...this.cache.keys()]) if (key.startsWith(prefix)) this.cache.delete(key);
1736
+ for (const key of [...this.queue.keys()]) if (key.startsWith(prefix)) this.queue.delete(key);
1737
+ }
1738
+ };
1739
+ var IDB_NAME = "rebase-offline";
1740
+ /**
1741
+ * v2 introduced the normalized row cache and string mutation ids. A v1
1742
+ * database holds whole-response blobs under keys this version cannot read and
1743
+ * queue entries ordered by a numeric `seq` this version no longer writes, so
1744
+ * the upgrade drops both stores rather than trying to translate them. Offline
1745
+ * support had not shipped in a release when v2 landed, so nothing in the wild
1746
+ * loses a queued write to this.
1747
+ */
1748
+ var IDB_VERSION = 2;
1749
+ var CACHE_STORE = "cache";
1750
+ var QUEUE_STORE = "queue";
1751
+ /** The exclusive upper bound of an IDBKeyRange covering every key under `prefix`. */
1752
+ function prefixRange(prefix) {
1753
+ return IDBKeyRange.bound(prefix, prefix + "￿", false, false);
1754
+ }
1755
+ function requestToPromise(request) {
1756
+ return new Promise((resolve, reject) => {
1757
+ request.onsuccess = () => resolve(request.result);
1758
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB request failed"));
1759
+ });
1760
+ }
1761
+ /** Resolve when the whole transaction commits, not just when the last request returns. */
1762
+ function transactionDone(tx) {
1763
+ return new Promise((resolve, reject) => {
1764
+ tx.oncomplete = () => resolve();
1765
+ tx.onabort = tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("IndexedDB transaction failed"));
1766
+ });
1767
+ }
1768
+ /**
1769
+ * IndexedDB-backed store — the browser default, so cached rows and queued
1770
+ * writes survive a reload or a browser restart. Everything lives in one
1771
+ * database with two object stores; keys are the manager's full prefixed
1772
+ * strings, so multiple users (scopes) share the database without ever
1773
+ * sharing entries.
1774
+ */
1775
+ var IndexedDBOfflineStore = class {
1776
+ dbPromise;
1777
+ /**
1778
+ * Where everything goes once the database turns out to belong to a newer
1779
+ * version of the SDK — see {@link backend}.
1780
+ */
1781
+ fallback;
1782
+ /**
1783
+ * The database, or the in-memory stand-in when it cannot be opened at
1784
+ * this version.
1785
+ *
1786
+ * A tab still running an older build (a stale tab, a rolled-back deploy)
1787
+ * that opens a database a newer build upgraded gets a `VersionError` —
1788
+ * and every read and every write after it failed with an opaque
1789
+ * IndexedDB rejection, so the app went down instead of degrading. It now
1790
+ * works in memory for the life of the page, says so once, and leaves the
1791
+ * newer build's data alone.
1792
+ */
1793
+ async backend() {
1794
+ if (this.fallback) return this.fallback;
1795
+ try {
1796
+ return await this.open();
1797
+ } catch (error) {
1798
+ if (error?.name !== "VersionError") throw error;
1799
+ if (!this.fallback) {
1800
+ console.warn("[rebase offline] The offline database on this device was written by a newer version of the SDK. This page keeps its offline data in memory only, and leaves that database untouched; reload to pick up the newer version.");
1801
+ this.fallback = new MemoryOfflineStore();
1802
+ }
1803
+ return this.fallback;
1804
+ }
1805
+ }
1806
+ open() {
1807
+ if (!this.dbPromise) this.dbPromise = new Promise((resolve, reject) => {
1808
+ const request = indexedDB.open(IDB_NAME, IDB_VERSION);
1809
+ request.onupgradeneeded = (event) => {
1810
+ const db = request.result;
1811
+ if (event.oldVersion > 0 && event.oldVersion < 2) {
1812
+ if (db.objectStoreNames.contains(CACHE_STORE)) db.deleteObjectStore(CACHE_STORE);
1813
+ if (db.objectStoreNames.contains(QUEUE_STORE)) db.deleteObjectStore(QUEUE_STORE);
1814
+ }
1815
+ if (!db.objectStoreNames.contains(CACHE_STORE)) db.createObjectStore(CACHE_STORE);
1816
+ if (!db.objectStoreNames.contains(QUEUE_STORE)) db.createObjectStore(QUEUE_STORE);
1817
+ };
1818
+ request.onsuccess = () => {
1819
+ const db = request.result;
1820
+ db.onversionchange = () => {
1821
+ db.close();
1822
+ this.dbPromise = void 0;
1823
+ };
1824
+ resolve(db);
1825
+ };
1826
+ request.onerror = () => {
1827
+ this.dbPromise = void 0;
1828
+ reject(request.error ?? /* @__PURE__ */ new Error("Failed to open IndexedDB"));
1829
+ };
1830
+ request.onblocked = () => {
1831
+ this.dbPromise = void 0;
1832
+ reject(/* @__PURE__ */ new Error("IndexedDB upgrade blocked by another tab"));
1833
+ };
1834
+ });
1835
+ return this.dbPromise;
1836
+ }
1837
+ async store(db, name, mode) {
1838
+ return db.transaction(name, mode).objectStore(name);
1839
+ }
1840
+ async getCache(key) {
1841
+ const db = await this.backend();
1842
+ if (db instanceof MemoryOfflineStore) return db.getCache(key);
1843
+ return await requestToPromise((await this.store(db, CACHE_STORE, "readonly")).get(key));
1844
+ }
1845
+ async setCache(key, entry) {
1846
+ const db = await this.backend();
1847
+ if (db instanceof MemoryOfflineStore) return db.setCache(key, entry);
1848
+ await requestToPromise((await this.store(db, CACHE_STORE, "readwrite")).put(entry, key));
1849
+ }
1850
+ async setCacheMany(entries) {
1851
+ if (entries.length === 0) return;
1852
+ const db = await this.backend();
1853
+ if (db instanceof MemoryOfflineStore) return db.setCacheMany(entries);
1854
+ const store = await this.store(db, CACHE_STORE, "readwrite");
1855
+ for (const { key, entry } of entries) store.put(entry, key);
1856
+ await transactionDone(store.transaction);
1857
+ }
1858
+ async deleteCache(keys) {
1859
+ if (keys.length === 0) return;
1860
+ const db = await this.backend();
1861
+ if (db instanceof MemoryOfflineStore) return db.deleteCache(keys);
1862
+ const store = await this.store(db, CACHE_STORE, "readwrite");
1863
+ for (const key of keys) store.delete(key);
1864
+ await transactionDone(store.transaction);
1865
+ }
1866
+ async listCache(prefix) {
1867
+ const db = await this.backend();
1868
+ if (db instanceof MemoryOfflineStore) return db.listCache(prefix);
1869
+ const store = await this.store(db, CACHE_STORE, "readonly");
1870
+ const [keys, entries] = await Promise.all([requestToPromise(store.getAllKeys(prefixRange(prefix))), requestToPromise(store.getAll(prefixRange(prefix)))]);
1871
+ return keys.map((key, i) => ({
1872
+ key: String(key),
1873
+ cachedAt: entries[i]?.cachedAt ?? 0
1874
+ }));
1875
+ }
1876
+ async listCacheEntries(prefix) {
1877
+ const db = await this.backend();
1878
+ if (db instanceof MemoryOfflineStore) return db.listCacheEntries(prefix);
1879
+ const store = await this.store(db, CACHE_STORE, "readonly");
1880
+ const [keys, entries] = await Promise.all([requestToPromise(store.getAllKeys(prefixRange(prefix))), requestToPromise(store.getAll(prefixRange(prefix)))]);
1881
+ return keys.map((key, i) => {
1882
+ const entry = entries[i];
1883
+ return {
1884
+ key: String(key),
1885
+ value: entry?.value,
1886
+ cachedAt: entry?.cachedAt ?? 0
1887
+ };
1888
+ });
1889
+ }
1890
+ async enqueue(key, mutation) {
1891
+ const db = await this.backend();
1892
+ if (db instanceof MemoryOfflineStore) return db.enqueue(key, mutation);
1893
+ await requestToPromise((await this.store(db, QUEUE_STORE, "readwrite")).put(mutation, key));
1894
+ }
1895
+ async dequeue(key) {
1896
+ const db = await this.backend();
1897
+ if (db instanceof MemoryOfflineStore) return db.dequeue(key);
1898
+ await requestToPromise((await this.store(db, QUEUE_STORE, "readwrite")).delete(key));
1899
+ }
1900
+ async listQueue(prefix) {
1901
+ const db = await this.backend();
1902
+ if (db instanceof MemoryOfflineStore) return db.listQueue(prefix);
1903
+ return await requestToPromise((await this.store(db, QUEUE_STORE, "readonly")).getAll(prefixRange(prefix)));
1904
+ }
1905
+ async clear(prefix) {
1906
+ const db = await this.backend();
1907
+ if (db instanceof MemoryOfflineStore) return db.clear(prefix);
1908
+ await requestToPromise((await this.store(db, CACHE_STORE, "readwrite")).delete(prefixRange(prefix)));
1909
+ await requestToPromise((await this.store(db, QUEUE_STORE, "readwrite")).delete(prefixRange(prefix)));
1910
+ }
1911
+ };
1912
+ //#endregion
1913
+ export { byIdRead as a, createTransport as c, parseRebaseJson as d, rebaseReviver as f, unref as h, buildQueryString as i, defaultCodeForStatus as l, withETag as m, MemoryOfflineStore as n, createCollectionClient as o, etagOf as p, SDKQueryBuilder as r, createMutationId as s, IndexedDBOfflineStore as t, invalidBulkBody as u };
1914
+
1915
+ //# sourceMappingURL=offline-store-Cm5sAX81-CALiO4Gn.js.map