@tier0/sdk-v2 0.9.9 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/LICENSE +21 -21
  3. package/README.md +165 -27
  4. package/dist/cjs/client.d.ts +79 -3
  5. package/dist/cjs/client.js +4 -6
  6. package/dist/cjs/client.js.map +1 -1
  7. package/dist/cjs/cloud-server.d.ts +33 -26
  8. package/dist/cjs/cloud-server.js +102 -100
  9. package/dist/cjs/cloud-server.js.map +1 -1
  10. package/dist/cjs/cloud-transport.d.ts +15 -20
  11. package/dist/cjs/cloud-transport.js +87 -68
  12. package/dist/cjs/cloud-transport.js.map +1 -1
  13. package/dist/cjs/connection-diagnostics.d.ts +2 -0
  14. package/dist/cjs/connection-diagnostics.js +20 -0
  15. package/dist/cjs/connection-diagnostics.js.map +1 -0
  16. package/dist/cjs/data/bootstrap.js +3 -4
  17. package/dist/cjs/data/bootstrap.js.map +1 -1
  18. package/dist/cjs/data/call.d.ts +1 -3
  19. package/dist/cjs/data/call.js +8 -12
  20. package/dist/cjs/data/call.js.map +1 -1
  21. package/dist/cjs/data/client.d.ts +8 -7
  22. package/dist/cjs/data/client.js +24 -11
  23. package/dist/cjs/data/client.js.map +1 -1
  24. package/dist/cjs/data/collection.d.ts +3 -5
  25. package/dist/cjs/data/collection.js +3 -4
  26. package/dist/cjs/data/collection.js.map +1 -1
  27. package/dist/cjs/data/context.d.ts +5 -11
  28. package/dist/cjs/data/context.js +21 -36
  29. package/dist/cjs/data/context.js.map +1 -1
  30. package/dist/cjs/data/error.d.ts +1 -1
  31. package/dist/cjs/data/error.js +3 -3
  32. package/dist/cjs/data/error.js.map +1 -1
  33. package/dist/cjs/data/index.d.ts +5 -3
  34. package/dist/cjs/data/index.js +5 -5
  35. package/dist/cjs/data/index.js.map +1 -1
  36. package/dist/cjs/data/relay.d.ts +51 -0
  37. package/dist/cjs/data/relay.js +371 -0
  38. package/dist/cjs/data/relay.js.map +1 -0
  39. package/dist/cjs/data/subscribe.d.ts +22 -38
  40. package/dist/cjs/data/subscribe.js +22 -28
  41. package/dist/cjs/data/subscribe.js.map +1 -1
  42. package/dist/cjs/event-stream.d.ts +28 -0
  43. package/dist/cjs/event-stream.js +222 -0
  44. package/dist/cjs/event-stream.js.map +1 -0
  45. package/dist/cjs/mq/client.d.ts +32 -1
  46. package/dist/cjs/mq/client.js +404 -69
  47. package/dist/cjs/mq/client.js.map +1 -1
  48. package/dist/cjs/mq/index.d.ts +1 -0
  49. package/dist/cjs/mq/index.js +3 -1
  50. package/dist/cjs/mq/index.js.map +1 -1
  51. package/dist/cjs/mq/topic.d.ts +29 -0
  52. package/dist/cjs/mq/topic.js +48 -0
  53. package/dist/cjs/mq/topic.js.map +1 -0
  54. package/dist/cjs/mq/types.d.ts +21 -1
  55. package/dist/cjs/mq/types.js.map +1 -1
  56. package/dist/cjs/openapi/api.generated.d.ts +20 -0
  57. package/dist/cjs/openapi/api.generated.js +11 -0
  58. package/dist/cjs/openapi/api.generated.js.map +1 -1
  59. package/dist/cjs/openapi/client.d.ts +46 -0
  60. package/dist/cjs/openapi/client.js +161 -12
  61. package/dist/cjs/openapi/client.js.map +1 -1
  62. package/dist/cjs/openapi/codecs.generated.d.ts +74 -0
  63. package/dist/cjs/openapi/codecs.generated.js +1 -1
  64. package/dist/cjs/openapi/codecs.generated.js.map +1 -1
  65. package/dist/cjs/openapi/types.generated.d.ts +205 -1
  66. package/dist/cjs/openapi/types.generated.js.map +1 -1
  67. package/dist/cjs/page-runtime.d.ts +48 -0
  68. package/dist/cjs/page-runtime.js +74 -0
  69. package/dist/cjs/page-runtime.js.map +1 -0
  70. package/dist/cjs/platform-proxy.d.ts +17 -0
  71. package/dist/cjs/platform-proxy.js +52 -0
  72. package/dist/cjs/platform-proxy.js.map +1 -0
  73. package/dist/cjs/runtime-context.d.ts +2 -1
  74. package/dist/cjs/runtime-context.js +6 -2
  75. package/dist/cjs/runtime-context.js.map +1 -1
  76. package/dist/cjs/runtime-env.js.map +1 -1
  77. package/dist/cjs/source.d.ts +19 -1
  78. package/dist/cjs/source.js +9 -3
  79. package/dist/cjs/source.js.map +1 -1
  80. package/dist/cjs/stream.js +3 -2
  81. package/dist/cjs/stream.js.map +1 -1
  82. package/dist/cjs/streams/contract.generated.d.ts +7 -1
  83. package/dist/cjs/streams/contract.generated.js.map +1 -1
  84. package/dist/cjs/streams/messages.js +1 -10
  85. package/dist/cjs/streams/messages.js.map +1 -1
  86. package/dist/cjs/streams/requests.d.ts +3 -0
  87. package/dist/cjs/streams/requests.js +5 -2
  88. package/dist/cjs/streams/requests.js.map +1 -1
  89. package/dist/cjs/streams/transport.d.ts +1 -13
  90. package/dist/cjs/streams/transport.js +2 -18
  91. package/dist/cjs/streams/transport.js.map +1 -1
  92. package/dist/cjs/uns-v2/client.d.ts +1 -6
  93. package/dist/cjs/uns-v2/client.js +33 -8
  94. package/dist/cjs/uns-v2/client.js.map +1 -1
  95. package/dist/cjs/uns-v2/data-client.d.ts +5 -2
  96. package/dist/cjs/uns-v2/data-client.js +7 -4
  97. package/dist/cjs/uns-v2/data-client.js.map +1 -1
  98. package/dist/cjs/uns-v2/generated.d.ts +6783 -4655
  99. package/dist/cjs/uns-v2/generated.js +221 -65
  100. package/dist/cjs/uns-v2/generated.js.map +1 -1
  101. package/dist/cjs/uns-v2/sse.d.ts +7 -1
  102. package/dist/cjs/uns-v2/sse.js +12 -2
  103. package/dist/cjs/uns-v2/sse.js.map +1 -1
  104. package/dist/esm/client.d.ts +79 -3
  105. package/dist/esm/client.js +5 -7
  106. package/dist/esm/client.js.map +1 -1
  107. package/dist/esm/cloud-server.d.ts +33 -26
  108. package/dist/esm/cloud-server.js +99 -97
  109. package/dist/esm/cloud-server.js.map +1 -1
  110. package/dist/esm/cloud-transport.d.ts +15 -20
  111. package/dist/esm/cloud-transport.js +86 -67
  112. package/dist/esm/cloud-transport.js.map +1 -1
  113. package/dist/esm/connection-diagnostics.d.ts +2 -0
  114. package/dist/esm/connection-diagnostics.js +17 -0
  115. package/dist/esm/connection-diagnostics.js.map +1 -0
  116. package/dist/esm/data/bootstrap.js +3 -4
  117. package/dist/esm/data/bootstrap.js.map +1 -1
  118. package/dist/esm/data/call.d.ts +1 -3
  119. package/dist/esm/data/call.js +9 -13
  120. package/dist/esm/data/call.js.map +1 -1
  121. package/dist/esm/data/client.d.ts +8 -7
  122. package/dist/esm/data/client.js +22 -9
  123. package/dist/esm/data/client.js.map +1 -1
  124. package/dist/esm/data/collection.d.ts +3 -5
  125. package/dist/esm/data/collection.js +3 -4
  126. package/dist/esm/data/collection.js.map +1 -1
  127. package/dist/esm/data/context.d.ts +5 -11
  128. package/dist/esm/data/context.js +19 -33
  129. package/dist/esm/data/context.js.map +1 -1
  130. package/dist/esm/data/error.d.ts +1 -1
  131. package/dist/esm/data/error.js +3 -3
  132. package/dist/esm/data/error.js.map +1 -1
  133. package/dist/esm/data/index.d.ts +5 -3
  134. package/dist/esm/data/index.js +3 -2
  135. package/dist/esm/data/index.js.map +1 -1
  136. package/dist/esm/data/relay.d.ts +51 -0
  137. package/dist/esm/data/relay.js +368 -0
  138. package/dist/esm/data/relay.js.map +1 -0
  139. package/dist/esm/data/subscribe.d.ts +22 -38
  140. package/dist/esm/data/subscribe.js +22 -27
  141. package/dist/esm/data/subscribe.js.map +1 -1
  142. package/dist/esm/event-stream.d.ts +28 -0
  143. package/dist/esm/event-stream.js +217 -0
  144. package/dist/esm/event-stream.js.map +1 -0
  145. package/dist/esm/mq/client.d.ts +32 -1
  146. package/dist/esm/mq/client.js +404 -69
  147. package/dist/esm/mq/client.js.map +1 -1
  148. package/dist/esm/mq/index.d.ts +1 -0
  149. package/dist/esm/mq/index.js +1 -0
  150. package/dist/esm/mq/index.js.map +1 -1
  151. package/dist/esm/mq/topic.d.ts +29 -0
  152. package/dist/esm/mq/topic.js +44 -0
  153. package/dist/esm/mq/topic.js.map +1 -0
  154. package/dist/esm/mq/types.d.ts +21 -1
  155. package/dist/esm/mq/types.js.map +1 -1
  156. package/dist/esm/openapi/api.generated.d.ts +20 -0
  157. package/dist/esm/openapi/api.generated.js +11 -0
  158. package/dist/esm/openapi/api.generated.js.map +1 -1
  159. package/dist/esm/openapi/client.d.ts +46 -0
  160. package/dist/esm/openapi/client.js +160 -12
  161. package/dist/esm/openapi/client.js.map +1 -1
  162. package/dist/esm/openapi/codecs.generated.d.ts +74 -0
  163. package/dist/esm/openapi/codecs.generated.js +1 -1
  164. package/dist/esm/openapi/codecs.generated.js.map +1 -1
  165. package/dist/esm/openapi/types.generated.d.ts +205 -1
  166. package/dist/esm/openapi/types.generated.js.map +1 -1
  167. package/dist/esm/page-runtime.d.ts +48 -0
  168. package/dist/esm/page-runtime.js +65 -0
  169. package/dist/esm/page-runtime.js.map +1 -0
  170. package/dist/esm/platform-proxy.d.ts +17 -0
  171. package/dist/esm/platform-proxy.js +47 -0
  172. package/dist/esm/platform-proxy.js.map +1 -0
  173. package/dist/esm/runtime-context.d.ts +2 -1
  174. package/dist/esm/runtime-context.js +6 -2
  175. package/dist/esm/runtime-context.js.map +1 -1
  176. package/dist/esm/runtime-env.js.map +1 -1
  177. package/dist/esm/source.d.ts +19 -1
  178. package/dist/esm/source.js +10 -4
  179. package/dist/esm/source.js.map +1 -1
  180. package/dist/esm/stream.js +4 -3
  181. package/dist/esm/stream.js.map +1 -1
  182. package/dist/esm/streams/contract.generated.d.ts +7 -1
  183. package/dist/esm/streams/contract.generated.js.map +1 -1
  184. package/dist/esm/streams/messages.js +1 -10
  185. package/dist/esm/streams/messages.js.map +1 -1
  186. package/dist/esm/streams/requests.d.ts +3 -0
  187. package/dist/esm/streams/requests.js +5 -2
  188. package/dist/esm/streams/requests.js.map +1 -1
  189. package/dist/esm/streams/transport.d.ts +1 -13
  190. package/dist/esm/streams/transport.js +1 -17
  191. package/dist/esm/streams/transport.js.map +1 -1
  192. package/dist/esm/uns-v2/client.d.ts +1 -6
  193. package/dist/esm/uns-v2/client.js +33 -8
  194. package/dist/esm/uns-v2/client.js.map +1 -1
  195. package/dist/esm/uns-v2/data-client.d.ts +5 -2
  196. package/dist/esm/uns-v2/data-client.js +8 -5
  197. package/dist/esm/uns-v2/data-client.js.map +1 -1
  198. package/dist/esm/uns-v2/generated.d.ts +6783 -4655
  199. package/dist/esm/uns-v2/generated.js +221 -65
  200. package/dist/esm/uns-v2/generated.js.map +1 -1
  201. package/dist/esm/uns-v2/sse.d.ts +7 -1
  202. package/dist/esm/uns-v2/sse.js +12 -2
  203. package/dist/esm/uns-v2/sse.js.map +1 -1
  204. package/package.json +25 -12
  205. package/skills/tier0-sdk-v2/SKILL.md +13 -5
  206. package/skills/tier0-sdk-v2/references/integration.md +14 -24
  207. package/skills/tier0-sdk-v2/references/local-connection.md +61 -0
  208. package/skills/tier0-sdk-v2/tier0-sdk-v2-data/SKILL.md +11 -5
  209. package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/consistency.md +8 -12
  210. package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/errors.md +24 -5
  211. package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/modeling.md +3 -1
  212. package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/read-write.md +46 -13
  213. package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/schema-evolution.md +2 -0
  214. package/skills/tier0-sdk-v2/tier0-sdk-v2-members/SKILL.md +2 -2
  215. package/skills/tier0-sdk-v2/tier0-sdk-v2-members/references/app-members.md +5 -1
  216. package/skills/tier0-sdk-v2/tier0-sdk-v2-mq/SKILL.md +25 -0
  217. package/skills/tier0-sdk-v2/tier0-sdk-v2-mq/references/quickstart.md +1 -1
  218. package/skills/tier0-sdk-v2/tier0-sdk-v2-notifications/SKILL.md +3 -3
  219. package/skills/tier0-sdk-v2/tier0-sdk-v2-notifications/references/send.md +6 -6
  220. package/skills/tier0-sdk-v2/tier0-sdk-v2-source/SKILL.md +4 -4
  221. package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/changes.md +2 -2
  222. package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/create.md +26 -36
  223. package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/delete.md +9 -23
  224. package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/protocols.md +12 -29
  225. package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/read.md +4 -5
  226. package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/update.md +5 -5
  227. package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/SKILL.md +3 -3
  228. package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/create.md +23 -9
  229. package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/deploy.md +14 -12
  230. package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/http-server.md +4 -4
  231. package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/read.md +4 -3
  232. package/skills/tier0-sdk-v2/tier0-sdk-v2-streams/SKILL.md +12 -12
  233. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/SKILL.md +3 -2
  234. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/create.md +2 -2
  235. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/delete.md +47 -20
  236. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/namespace.md +3 -3
  237. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/read.md +11 -6
  238. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/restore.md +12 -13
  239. package/dist/cjs/app-identity-contract.generated.d.ts +0 -22
  240. package/dist/cjs/app-identity-contract.generated.js +0 -51
  241. package/dist/cjs/app-identity-contract.generated.js.map +0 -1
  242. package/dist/cjs/runtime.d.ts +0 -13
  243. package/dist/cjs/runtime.js +0 -51
  244. package/dist/cjs/runtime.js.map +0 -1
  245. package/dist/esm/app-identity-contract.generated.d.ts +0 -22
  246. package/dist/esm/app-identity-contract.generated.js +0 -48
  247. package/dist/esm/app-identity-contract.generated.js.map +0 -1
  248. package/dist/esm/runtime.d.ts +0 -13
  249. package/dist/esm/runtime.js +0 -46
  250. package/dist/esm/runtime.js.map +0 -1
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SUBSCRIBE_IDLE_TIMEOUT_MS = void 0;
4
- exports.redactRestricted = redactRestricted;
5
4
  exports.createSubscribe = createSubscribe;
6
5
  /**
7
6
  * `data.subscribe`: committed record changes of declared collections over
@@ -28,9 +27,11 @@ exports.createSubscribe = createSubscribe;
28
27
  * `delete` always passes, a late one too, and forgets the record's
29
28
  * revision, since a record written again under the same ID may start
30
29
  * over at revision 1.
31
- * - Changes in collections with employee data rules (`restricted`) come as
32
- * collection-level notices unless `restricted: "record"` is asked for.
33
30
  * - Aborting `signal` or leaving the loop ends the iteration without error.
31
+ * - The stream runs as the App: it sees every change of the collections
32
+ * granted to the App. A server opens one per process and fans it out to
33
+ * browsers with `createChangeRelay`, filtering per browser by what the App
34
+ * lets that visitor see.
34
35
  *
35
36
  * Events carry identities only, never record data; read the record to show it.
36
37
  */
@@ -48,18 +49,6 @@ const RESET_PAUSE_MS = 1_000;
48
49
  const HEALTHY_CONNECTION_MS = 30_000;
49
50
  /** Records whose last revision is remembered; beyond this the oldest is forgotten (a stale change may then pass, which only costs a reread). */
50
51
  const SUBSCRIBE_SEEN_RECORDS_MAX = 10_000;
51
- /**
52
- * The event as an employee's browser may see it: a change in a collection
53
- * with employee data rules becomes a collection-level notice without record
54
- * ID or revision, so it does not reveal records the employee may not read.
55
- * Other events pass unchanged.
56
- */
57
- function redactRestricted(event) {
58
- if (event.type !== 'change' || !event.restricted || event.change === 'collection')
59
- return event;
60
- const { eventId, collection, collectionId, occurredAt, restricted } = event;
61
- return { type: 'change', eventId, collection, collectionId, occurredAt, restricted, change: 'collection' };
62
- }
63
52
  function reconnectDelay(failures, afterMs) {
64
53
  if (afterMs !== undefined)
65
54
  return Math.min(afterMs, RECONNECT_MAX_MS);
@@ -111,7 +100,7 @@ function pause(ms, signal) {
111
100
  });
112
101
  }
113
102
  function createSubscribe(runtime) {
114
- return async function* subscribe(options = {}) {
103
+ return async function* subscribe(options = {}, observer = {}) {
115
104
  const definitions = [...new Set((options.collections ?? runtime.declared).map(runtime.lookup))];
116
105
  if (definitions.length < 1 || definitions.length > client_js_1.UNS_V2_SUBSCRIBE_MAX_COLLECTIONS) {
117
106
  throw new TypeError(`data.subscribe watches 1–${client_js_1.UNS_V2_SUBSCRIBE_MAX_COLLECTIONS} collections; got ${definitions.length}.`);
@@ -122,10 +111,7 @@ function createSubscribe(runtime) {
122
111
  }
123
112
  if (options.lastEventId !== undefined)
124
113
  (0, client_js_1.assertEventId)(options.lastEventId);
125
- const redact = (options.restricted ?? 'collection') === 'collection';
126
114
  const idleMs = options.idleTimeoutMs ?? exports.SUBSCRIBE_IDLE_TIMEOUT_MS;
127
- // The subject of the request that subscribed; an App-level subscription carries none.
128
- const call = { dataSubject: runtime.subject() };
129
115
  const stop = new AbortController();
130
116
  const onAbort = () => stop.abort();
131
117
  options.signal?.addEventListener('abort', onAbort, { once: true });
@@ -135,7 +121,9 @@ function createSubscribe(runtime) {
135
121
  let failures = 0;
136
122
  const seen = new SeenRevisions(SUBSCRIBE_SEEN_RECORDS_MAX);
137
123
  /** Waits, longer each time, before the next attempt. */
138
- const backOff = async (afterMs) => {
124
+ const backOff = async (error) => {
125
+ observer.retrying?.(error);
126
+ const afterMs = error.retry.afterMs;
139
127
  failures += 1;
140
128
  await pause(reconnectDelay(failures, afterMs), stop.signal);
141
129
  };
@@ -149,7 +137,7 @@ function createSubscribe(runtime) {
149
137
  // A freshly imported App answers APP_NOT_READY for a while: keep waiting for it.
150
138
  if (!(error instanceof error_js_1.DataError) || !error.retry.safe || stop.signal.aborted)
151
139
  throw error;
152
- await backOff(error.retry.afterMs);
140
+ await backOff(error);
153
141
  continue;
154
142
  }
155
143
  const aliases = new Map();
@@ -168,17 +156,24 @@ function createSubscribe(runtime) {
168
156
  const openedAt = Date.now();
169
157
  let healthy = false;
170
158
  let reset = false;
159
+ let streaming = false;
171
160
  try {
172
161
  arm();
173
- const stream = runtime.openStream({ collectionIds: [...aliases.keys()], ...(recordIds ? { recordIds } : {}) }, { ...call, lastEventId, signal: connection.signal });
162
+ const stream = runtime.openStream({ collectionIds: [...aliases.keys()], ...(recordIds ? { recordIds } : {}) }, { lastEventId, signal: connection.signal });
174
163
  for await (const event of stream) {
175
164
  arm();
176
165
  if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS)
177
166
  healthy = true;
167
+ if (!streaming) {
168
+ streaming = true;
169
+ observer.connected?.();
170
+ }
178
171
  if (event.type === 'heartbeat') {
179
172
  // The connect heartbeat names the current position: resuming from it loses nothing.
180
- if (event.eventId !== undefined)
173
+ if (event.eventId !== undefined) {
181
174
  lastEventId = event.eventId;
175
+ observer.position?.(event.eventId);
176
+ }
182
177
  continue;
183
178
  }
184
179
  if (event.type === 'reset') {
@@ -192,20 +187,19 @@ function createSubscribe(runtime) {
192
187
  healthy = true;
193
188
  // A dropped change is still a resume position.
194
189
  lastEventId = event.eventId;
190
+ observer.position?.(event.eventId);
195
191
  if (!seen.admit(event.collectionId, event.recordId, event.revision, event.change))
196
192
  continue;
197
- const change = {
193
+ yield {
198
194
  type: 'change',
199
195
  eventId: event.eventId,
200
196
  collection: aliases.get(event.collectionId) ?? event.collectionId,
201
197
  collectionId: event.collectionId,
202
198
  occurredAt: event.occurredAt,
203
- restricted: event.restricted,
204
199
  change: event.change,
205
200
  recordId: event.recordId,
206
201
  revision: event.revision,
207
202
  };
208
- yield redact ? redactRestricted(change) : change;
209
203
  }
210
204
  }
211
205
  catch (error) {
@@ -221,7 +215,7 @@ function createSubscribe(runtime) {
221
215
  catch (cause) {
222
216
  if (!(cause instanceof error_js_1.DataError) || !cause.retry.safe)
223
217
  throw cause;
224
- await backOff(cause.retry.afterMs);
218
+ await backOff(cause);
225
219
  continue;
226
220
  }
227
221
  if (!changed)
@@ -234,7 +228,7 @@ function createSubscribe(runtime) {
234
228
  }
235
229
  if (!error.retry.safe)
236
230
  throw error;
237
- await backOff(error.retry.afterMs);
231
+ await backOff(error);
238
232
  continue;
239
233
  }
240
234
  finally {
@@ -1 +1 @@
1
- {"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../../src/data/subscribe.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `data.subscribe`: committed record changes of declared collections over\n * UNS `records:subscribe` (server-sent events), as one async iterable that\n * outlives single connections.\n *\n * - Reconnects with `Last-Event-ID` when the stream ends, breaks, or stays\n * silent for `idleTimeoutMs` (UNS sends a heartbeat every 15 s). A stream\n * that dies young without a change backs off, so a server dropping every\n * connection is not hammered.\n * - Failures marked `retry.safe` (429 over the subscription limit, 503,\n * `APP_NOT_READY` from ensure …) are retried after `retry.afterMs` or a\n * backoff of up to 30 s, without end; any other failure ends the\n * iteration with that DataError.\n * - `reset` is passed on: `CURSOR_EXPIRED` (the position was cleaned up after\n * 24 h, cannot be decoded after a key rotation, or the tenant database\n * moved) or `SCOPE_CHANGED` (access changed, e.g. a collection is no longer\n * readable). The consumer reloads what it shows; the next connection\n * starts from now, without a resume position. A collection recreated\n * under a new ID (see bootstrap self-healing) yields `COLLECTION_RECREATED`.\n * - UNS delivers changes in write-transaction order, not commit order, so\n * revisions of one record may arrive out of order. A non-delete change at\n * or below the revision already passed on for its record is dropped; a\n * `delete` always passes, a late one too, and forgets the record's\n * revision, since a record written again under the same ID may start\n * over at revision 1.\n * - Changes in collections with employee data rules (`restricted`) come as\n * collection-level notices unless `restricted: \"record\"` is asked for.\n * - Aborting `signal` or leaving the loop ends the iteration without error.\n *\n * Events carry identities only, never record data; read the record to show it.\n */\nimport { isStaleCollectionError, type EnsureState } from './bootstrap.js';\nimport type { CallOptions, DataOpenStream } from './call.js';\nimport { assertEventId, UNS_V2_SUBSCRIBE_MAX_COLLECTIONS, UNS_V2_SUBSCRIBE_MAX_RECORD_IDS } from '../uns-v2/client.js';\nimport { collectionLabel, type CollectionTarget } from './define.js';\nimport { DataError } from './error.js';\n\n/** Three missed 15 s heartbeats. */\nexport const SUBSCRIBE_IDLE_TIMEOUT_MS = 45_000;\nconst RECONNECT_BASE_MS = 500;\nconst RECONNECT_MAX_MS = 30_000;\n/** After a reset, the next connection waits at least this long. */\nconst RESET_PAUSE_MS = 1_000;\n/** A connection alive this long counts as healthy even without changes. */\nconst HEALTHY_CONNECTION_MS = 30_000;\n/** Records whose last revision is remembered; beyond this the oldest is forgotten (a stale change may then pass, which only costs a reread). */\nconst SUBSCRIBE_SEEN_RECORDS_MAX = 10_000;\n\nexport interface SubscribeOptions {\n /** Declarations, aliases, or existing-ID references (at most 64); defaults to every declared collection. */\n collections?: readonly (CollectionTarget | string)[];\n /** Only changes of these record IDs (1–100, duplicates ignored), when exactly one collection is watched. */\n recordIds?: readonly string[];\n /**\n * How changes in collections with employee data rules arrive.\n * `collection` (default): as a collection-level notice without record ID\n * or revision, safe to forward to any employee. `record`: the full event,\n * for server-side consumers that do not forward it to browsers, or that\n * redact per recipient with `redactRestricted`.\n */\n restricted?: 'collection' | 'record';\n /** Resume after this event (e.g. a browser's `Last-Event-ID`). */\n lastEventId?: string;\n signal?: AbortSignal;\n /** Reconnect when nothing, not even a heartbeat, arrives for this long. */\n idleTimeoutMs?: number;\n}\n\ninterface ChangeBase {\n type: 'change';\n eventId: string;\n /** Alias of a declared collection, or the ID of an existing collection reference. */\n collection: string;\n collectionId: string;\n occurredAt: number;\n /**\n * The collection has employee data rules: whether an employee may see this\n * record depends on who they are. Such changes arrive as `CollectionChange`\n * unless the subscription asked for `restricted: \"record\"`.\n */\n restricted: boolean;\n}\n\n/**\n * One record committed (`record`) or deleted (`delete`) at `revision`. A\n * `record` change is never at or below a revision already passed on for the\n * same record since its last delete; a `delete` may be.\n */\nexport interface RecordChange extends ChangeBase {\n change: 'record' | 'delete';\n recordId: string;\n revision: number;\n}\n\n/** Something in the collection changed; which record is not disclosed. Reload through the data rules. */\nexport interface CollectionChange extends ChangeBase {\n change: 'collection';\n}\n\nexport type ChangeEvent = RecordChange | CollectionChange;\n/** `reason`: `CURSOR_EXPIRED`, `SCOPE_CHANGED` (from UNS) or `COLLECTION_RECREATED`. */\nexport type SubscriptionEvent = ChangeEvent | { type: 'reset'; reason: string; message?: string };\n\n/**\n * The event as an employee's browser may see it: a change in a collection\n * with employee data rules becomes a collection-level notice without record\n * ID or revision, so it does not reveal records the employee may not read.\n * Other events pass unchanged.\n */\nexport function redactRestricted<E extends SubscriptionEvent>(event: E): E | CollectionChange {\n if (event.type !== 'change' || !event.restricted || event.change === 'collection') return event;\n const { eventId, collection, collectionId, occurredAt, restricted } = event;\n return { type: 'change', eventId, collection, collectionId, occurredAt, restricted, change: 'collection' };\n}\n\nexport interface SubscribeRuntime {\n openStream: DataOpenStream;\n ensure(targets: readonly CollectionTarget[]): Promise<EnsureState>;\n rebuild(before: EnsureState, cause: DataError, targets: readonly CollectionTarget[]): Promise<boolean>;\n resolve(target: CollectionTarget): Promise<string>;\n lookup(target: CollectionTarget | string): CollectionTarget;\n declared: readonly CollectionTarget[];\n subject(): string | undefined;\n}\n\nfunction reconnectDelay(failures: number, afterMs: number | undefined): number {\n if (afterMs !== undefined) return Math.min(afterMs, RECONNECT_MAX_MS);\n const ceiling = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** Math.max(0, failures - 1));\n return ceiling / 2 + Math.random() * (ceiling / 2);\n}\n\n/**\n * The highest revision passed on per record, so changes delivered out of\n * order are dropped (UNS orders by write transaction, not by commit).\n */\nclass SeenRevisions {\n private readonly revisions = new Map<string, number>();\n\n constructor(private readonly max: number) {}\n\n /** Whether the change is new; remembers its revision, or forgets the record on delete. */\n admit(collectionId: string, recordId: string, revision: number, change: 'record' | 'delete'): boolean {\n const key = `${collectionId}\\u0000${recordId}`;\n const seen = this.revisions.get(key);\n // A delete always passes, a late one too: the record may be written again under the same ID, from revision 1.\n if (change === 'delete') {\n this.revisions.delete(key);\n return true;\n }\n if (seen !== undefined && revision <= seen) return false;\n this.revisions.delete(key);\n this.revisions.set(key, revision);\n if (this.revisions.size > this.max) this.revisions.delete(this.revisions.keys().next().value!);\n return true;\n }\n\n clear() {\n this.revisions.clear();\n }\n}\n\nfunction pause(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal.aborted) return resolve();\n const done = () => {\n clearTimeout(timer);\n signal.removeEventListener('abort', done);\n resolve();\n };\n const timer = setTimeout(done, ms);\n signal.addEventListener('abort', done, { once: true });\n });\n}\n\nexport function createSubscribe(runtime: SubscribeRuntime) {\n return async function* subscribe(options: SubscribeOptions = {}): AsyncGenerator<SubscriptionEvent, void, undefined> {\n const definitions = [...new Set((options.collections ?? runtime.declared).map(runtime.lookup))];\n if (definitions.length < 1 || definitions.length > UNS_V2_SUBSCRIBE_MAX_COLLECTIONS) {\n throw new TypeError(`data.subscribe watches 1–${UNS_V2_SUBSCRIBE_MAX_COLLECTIONS} collections; got ${definitions.length}.`);\n }\n const recordIds = options.recordIds === undefined ? undefined : [...new Set(options.recordIds)];\n if (recordIds && (recordIds.length < 1 || recordIds.length > UNS_V2_SUBSCRIBE_MAX_RECORD_IDS || definitions.length !== 1)) {\n throw new TypeError(`data.subscribe takes 1–${UNS_V2_SUBSCRIBE_MAX_RECORD_IDS} recordIds, of exactly one collection.`);\n }\n if (options.lastEventId !== undefined) assertEventId(options.lastEventId);\n const redact = (options.restricted ?? 'collection') === 'collection';\n const idleMs = options.idleTimeoutMs ?? SUBSCRIBE_IDLE_TIMEOUT_MS;\n // The subject of the request that subscribed; an App-level subscription carries none.\n const call: CallOptions = { dataSubject: runtime.subject() };\n const stop = new AbortController();\n const onAbort = () => stop.abort();\n options.signal?.addEventListener('abort', onAbort, { once: true });\n if (options.signal?.aborted) stop.abort();\n let lastEventId = options.lastEventId;\n let failures = 0;\n const seen = new SeenRevisions(SUBSCRIBE_SEEN_RECORDS_MAX);\n /** Waits, longer each time, before the next attempt. */\n const backOff = async (afterMs?: number) => {\n failures += 1;\n await pause(reconnectDelay(failures, afterMs), stop.signal);\n };\n try {\n while (!stop.signal.aborted) {\n let state: EnsureState;\n try {\n state = await runtime.ensure(definitions);\n } catch (error) {\n // A freshly imported App answers APP_NOT_READY for a while: keep waiting for it.\n if (!(error instanceof DataError) || !error.retry.safe || stop.signal.aborted) throw error;\n await backOff(error.retry.afterMs);\n continue;\n }\n const aliases = new Map<string, string>();\n for (const definition of definitions) {\n const id = await runtime.resolve(definition);\n aliases.set(id, collectionLabel(definition));\n }\n\n const connection = new AbortController();\n const closeConnection = () => connection.abort();\n stop.signal.addEventListener('abort', closeConnection, { once: true });\n let idle: ReturnType<typeof setTimeout> | undefined;\n const arm = () => {\n clearTimeout(idle);\n idle = setTimeout(closeConnection, idleMs);\n };\n const openedAt = Date.now();\n let healthy = false;\n let reset = false;\n try {\n arm();\n const stream = runtime.openStream(\n { collectionIds: [...aliases.keys()], ...(recordIds ? { recordIds } : {}) },\n { ...call, lastEventId, signal: connection.signal },\n );\n for await (const event of stream) {\n arm();\n if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS) healthy = true;\n if (event.type === 'heartbeat') {\n // The connect heartbeat names the current position: resuming from it loses nothing.\n if (event.eventId !== undefined) lastEventId = event.eventId;\n continue;\n }\n if (event.type === 'reset') {\n lastEventId = undefined;\n reset = true;\n // The consumer rereads everything; revisions seen before mean nothing for what follows.\n seen.clear();\n yield event.message === undefined ? { type: 'reset', reason: event.reason } : { type: 'reset', reason: event.reason, message: event.message };\n continue;\n }\n healthy = true;\n // A dropped change is still a resume position.\n lastEventId = event.eventId;\n if (!seen.admit(event.collectionId, event.recordId, event.revision, event.change)) continue;\n const change: RecordChange = {\n type: 'change',\n eventId: event.eventId,\n collection: aliases.get(event.collectionId) ?? event.collectionId,\n collectionId: event.collectionId,\n occurredAt: event.occurredAt,\n restricted: event.restricted,\n change: event.change,\n recordId: event.recordId,\n revision: event.revision,\n };\n yield redact ? redactRestricted(change) : change;\n }\n } catch (error) {\n if (stop.signal.aborted) return;\n if (!(error instanceof DataError)) throw error;\n if (isStaleCollectionError(error)) {\n let changed: boolean;\n try {\n changed = await runtime.rebuild(state, error, definitions);\n } catch (cause) {\n if (!(cause instanceof DataError) || !cause.retry.safe) throw cause;\n await backOff(cause.retry.afterMs);\n continue;\n }\n if (!changed) throw error;\n // Recreated collections have new IDs; their old cursor means nothing.\n lastEventId = undefined;\n seen.clear();\n yield { type: 'reset', reason: 'COLLECTION_RECREATED' };\n continue;\n }\n if (!error.retry.safe) throw error;\n await backOff(error.retry.afterMs);\n continue;\n } finally {\n clearTimeout(idle);\n connection.abort();\n stop.signal.removeEventListener('abort', closeConnection);\n }\n // The stream ended or went silent. Reconnect at once after a healthy connection, else back off.\n if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS) healthy = true;\n if (healthy) failures = 0;\n else failures += 1;\n // A reset always waits a moment: a server resetting in a loop is not reconnected to in a loop.\n const wait = Math.max(reset ? RESET_PAUSE_MS : 0, healthy ? 0 : reconnectDelay(failures, undefined));\n if (wait > 0) await pause(wait, stop.signal);\n }\n } finally {\n stop.abort();\n options.signal?.removeEventListener('abort', onAbort);\n }\n };\n}\n"]}
1
+ {"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../../src/data/subscribe.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `data.subscribe`: committed record changes of declared collections over\n * UNS `records:subscribe` (server-sent events), as one async iterable that\n * outlives single connections.\n *\n * - Reconnects with `Last-Event-ID` when the stream ends, breaks, or stays\n * silent for `idleTimeoutMs` (UNS sends a heartbeat every 15 s). A stream\n * that dies young without a change backs off, so a server dropping every\n * connection is not hammered.\n * - Failures marked `retry.safe` (429 over the subscription limit, 503,\n * `APP_NOT_READY` from ensure …) are retried after `retry.afterMs` or a\n * backoff of up to 30 s, without end; any other failure ends the\n * iteration with that DataError.\n * - `reset` is passed on: `CURSOR_EXPIRED` (the position was cleaned up after\n * 24 h, cannot be decoded after a key rotation, or the tenant database\n * moved) or `SCOPE_CHANGED` (access changed, e.g. a collection is no longer\n * readable). The consumer reloads what it shows; the next connection\n * starts from now, without a resume position. A collection recreated\n * under a new ID (see bootstrap self-healing) yields `COLLECTION_RECREATED`.\n * - UNS delivers changes in write-transaction order, not commit order, so\n * revisions of one record may arrive out of order. A non-delete change at\n * or below the revision already passed on for its record is dropped; a\n * `delete` always passes, a late one too, and forgets the record's\n * revision, since a record written again under the same ID may start\n * over at revision 1.\n * - Aborting `signal` or leaving the loop ends the iteration without error.\n * - The stream runs as the App: it sees every change of the collections\n * granted to the App. A server opens one per process and fans it out to\n * browsers with `createChangeRelay`, filtering per browser by what the App\n * lets that visitor see.\n *\n * Events carry identities only, never record data; read the record to show it.\n */\nimport { isStaleCollectionError, type EnsureState } from './bootstrap.js';\nimport type { DataOpenStream } from './call.js';\nimport { assertEventId, UNS_V2_SUBSCRIBE_MAX_COLLECTIONS, UNS_V2_SUBSCRIBE_MAX_RECORD_IDS } from '../uns-v2/client.js';\nimport { collectionLabel, type CollectionTarget } from './define.js';\nimport { DataError } from './error.js';\n\n/** Three missed 15 s heartbeats. */\nexport const SUBSCRIBE_IDLE_TIMEOUT_MS = 45_000;\nconst RECONNECT_BASE_MS = 500;\nconst RECONNECT_MAX_MS = 30_000;\n/** After a reset, the next connection waits at least this long. */\nconst RESET_PAUSE_MS = 1_000;\n/** A connection alive this long counts as healthy even without changes. */\nconst HEALTHY_CONNECTION_MS = 30_000;\n/** Records whose last revision is remembered; beyond this the oldest is forgotten (a stale change may then pass, which only costs a reread). */\nconst SUBSCRIBE_SEEN_RECORDS_MAX = 10_000;\n\nexport interface SubscribeOptions {\n /** Declarations, aliases, or existing-ID references (at most 64); defaults to every declared collection. */\n collections?: readonly (CollectionTarget | string)[];\n /** Only changes of these record IDs (1–100, duplicates ignored), when exactly one collection is watched. */\n recordIds?: readonly string[];\n /** Resume after this event (e.g. a browser's `Last-Event-ID`). */\n lastEventId?: string;\n signal?: AbortSignal;\n /** Reconnect when nothing, not even a heartbeat, arrives for this long. */\n idleTimeoutMs?: number;\n}\n\n/**\n * One record committed (`record`) or deleted (`delete`) at `revision`. A\n * `record` change is never at or below a revision already passed on for the\n * same record since its last delete; a `delete` may be.\n */\nexport interface RecordChange {\n type: 'change';\n eventId: string;\n /** Alias of a declared collection, or the ID of an existing collection reference. */\n collection: string;\n collectionId: string;\n occurredAt: number;\n change: 'record' | 'delete';\n recordId: string;\n revision: number;\n}\n\n/** `reason`: `CURSOR_EXPIRED`, `SCOPE_CHANGED` (from UNS) or `COLLECTION_RECREATED`. */\nexport type SubscriptionEvent = RecordChange | { type: 'reset'; reason: string; message?: string };\n\n/**\n * What `data.subscribe` tells a server-side consumer about the upstream\n * (`createChangeRelay` uses it; not part of the public API).\n */\nexport interface SubscribeObserver {\n /** A connection is streaming: its first frame (the connect heartbeat) arrived. */\n connected?(): void;\n /** The resume position moved: a heartbeat position or a change, dropped or not. */\n position?(eventId: string): void;\n /** An attempt failed and will be retried after a wait (e.g. 429 over the subscription limit, `APP_NOT_READY`). */\n retrying?(error: DataError): void;\n}\n\nexport interface SubscribeRuntime {\n openStream: DataOpenStream;\n ensure(targets: readonly CollectionTarget[]): Promise<EnsureState>;\n rebuild(before: EnsureState, cause: DataError, targets: readonly CollectionTarget[]): Promise<boolean>;\n resolve(target: CollectionTarget): Promise<string>;\n lookup(target: CollectionTarget | string): CollectionTarget;\n declared: readonly CollectionTarget[];\n}\n\nfunction reconnectDelay(failures: number, afterMs: number | undefined): number {\n if (afterMs !== undefined) return Math.min(afterMs, RECONNECT_MAX_MS);\n const ceiling = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** Math.max(0, failures - 1));\n return ceiling / 2 + Math.random() * (ceiling / 2);\n}\n\n/**\n * The highest revision passed on per record, so changes delivered out of\n * order are dropped (UNS orders by write transaction, not by commit).\n */\nclass SeenRevisions {\n private readonly revisions = new Map<string, number>();\n\n constructor(private readonly max: number) {}\n\n /** Whether the change is new; remembers its revision, or forgets the record on delete. */\n admit(collectionId: string, recordId: string, revision: number, change: 'record' | 'delete'): boolean {\n const key = `${collectionId}\\u0000${recordId}`;\n const seen = this.revisions.get(key);\n // A delete always passes, a late one too: the record may be written again under the same ID, from revision 1.\n if (change === 'delete') {\n this.revisions.delete(key);\n return true;\n }\n if (seen !== undefined && revision <= seen) return false;\n this.revisions.delete(key);\n this.revisions.set(key, revision);\n if (this.revisions.size > this.max) this.revisions.delete(this.revisions.keys().next().value!);\n return true;\n }\n\n clear() {\n this.revisions.clear();\n }\n}\n\nfunction pause(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal.aborted) return resolve();\n const done = () => {\n clearTimeout(timer);\n signal.removeEventListener('abort', done);\n resolve();\n };\n const timer = setTimeout(done, ms);\n signal.addEventListener('abort', done, { once: true });\n });\n}\n\nexport function createSubscribe(runtime: SubscribeRuntime) {\n return async function* subscribe(options: SubscribeOptions = {}, observer: SubscribeObserver = {}): AsyncGenerator<SubscriptionEvent, void, undefined> {\n const definitions = [...new Set((options.collections ?? runtime.declared).map(runtime.lookup))];\n if (definitions.length < 1 || definitions.length > UNS_V2_SUBSCRIBE_MAX_COLLECTIONS) {\n throw new TypeError(`data.subscribe watches 1–${UNS_V2_SUBSCRIBE_MAX_COLLECTIONS} collections; got ${definitions.length}.`);\n }\n const recordIds = options.recordIds === undefined ? undefined : [...new Set(options.recordIds)];\n if (recordIds && (recordIds.length < 1 || recordIds.length > UNS_V2_SUBSCRIBE_MAX_RECORD_IDS || definitions.length !== 1)) {\n throw new TypeError(`data.subscribe takes 1–${UNS_V2_SUBSCRIBE_MAX_RECORD_IDS} recordIds, of exactly one collection.`);\n }\n if (options.lastEventId !== undefined) assertEventId(options.lastEventId);\n const idleMs = options.idleTimeoutMs ?? SUBSCRIBE_IDLE_TIMEOUT_MS;\n const stop = new AbortController();\n const onAbort = () => stop.abort();\n options.signal?.addEventListener('abort', onAbort, { once: true });\n if (options.signal?.aborted) stop.abort();\n let lastEventId = options.lastEventId;\n let failures = 0;\n const seen = new SeenRevisions(SUBSCRIBE_SEEN_RECORDS_MAX);\n /** Waits, longer each time, before the next attempt. */\n const backOff = async (error: DataError) => {\n observer.retrying?.(error);\n const afterMs = error.retry.afterMs;\n failures += 1;\n await pause(reconnectDelay(failures, afterMs), stop.signal);\n };\n try {\n while (!stop.signal.aborted) {\n let state: EnsureState;\n try {\n state = await runtime.ensure(definitions);\n } catch (error) {\n // A freshly imported App answers APP_NOT_READY for a while: keep waiting for it.\n if (!(error instanceof DataError) || !error.retry.safe || stop.signal.aborted) throw error;\n await backOff(error);\n continue;\n }\n const aliases = new Map<string, string>();\n for (const definition of definitions) {\n const id = await runtime.resolve(definition);\n aliases.set(id, collectionLabel(definition));\n }\n\n const connection = new AbortController();\n const closeConnection = () => connection.abort();\n stop.signal.addEventListener('abort', closeConnection, { once: true });\n let idle: ReturnType<typeof setTimeout> | undefined;\n const arm = () => {\n clearTimeout(idle);\n idle = setTimeout(closeConnection, idleMs);\n };\n const openedAt = Date.now();\n let healthy = false;\n let reset = false;\n let streaming = false;\n try {\n arm();\n const stream = runtime.openStream(\n { collectionIds: [...aliases.keys()], ...(recordIds ? { recordIds } : {}) },\n { lastEventId, signal: connection.signal },\n );\n for await (const event of stream) {\n arm();\n if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS) healthy = true;\n if (!streaming) {\n streaming = true;\n observer.connected?.();\n }\n if (event.type === 'heartbeat') {\n // The connect heartbeat names the current position: resuming from it loses nothing.\n if (event.eventId !== undefined) {\n lastEventId = event.eventId;\n observer.position?.(event.eventId);\n }\n continue;\n }\n if (event.type === 'reset') {\n lastEventId = undefined;\n reset = true;\n // The consumer rereads everything; revisions seen before mean nothing for what follows.\n seen.clear();\n yield event.message === undefined ? { type: 'reset', reason: event.reason } : { type: 'reset', reason: event.reason, message: event.message };\n continue;\n }\n healthy = true;\n // A dropped change is still a resume position.\n lastEventId = event.eventId;\n observer.position?.(event.eventId);\n if (!seen.admit(event.collectionId, event.recordId, event.revision, event.change)) continue;\n yield {\n type: 'change',\n eventId: event.eventId,\n collection: aliases.get(event.collectionId) ?? event.collectionId,\n collectionId: event.collectionId,\n occurredAt: event.occurredAt,\n change: event.change,\n recordId: event.recordId,\n revision: event.revision,\n };\n }\n } catch (error) {\n if (stop.signal.aborted) return;\n if (!(error instanceof DataError)) throw error;\n if (isStaleCollectionError(error)) {\n let changed: boolean;\n try {\n changed = await runtime.rebuild(state, error, definitions);\n } catch (cause) {\n if (!(cause instanceof DataError) || !cause.retry.safe) throw cause;\n await backOff(cause);\n continue;\n }\n if (!changed) throw error;\n // Recreated collections have new IDs; their old cursor means nothing.\n lastEventId = undefined;\n seen.clear();\n yield { type: 'reset', reason: 'COLLECTION_RECREATED' };\n continue;\n }\n if (!error.retry.safe) throw error;\n await backOff(error);\n continue;\n } finally {\n clearTimeout(idle);\n connection.abort();\n stop.signal.removeEventListener('abort', closeConnection);\n }\n // The stream ended or went silent. Reconnect at once after a healthy connection, else back off.\n if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS) healthy = true;\n if (healthy) failures = 0;\n else failures += 1;\n // A reset always waits a moment: a server resetting in a loop is not reconnected to in a loop.\n const wait = Math.max(reset ? RESET_PAUSE_MS : 0, healthy ? 0 : reconnectDelay(failures, undefined));\n if (wait > 0) await pause(wait, stop.signal);\n }\n } finally {\n stop.abort();\n options.signal?.removeEventListener('abort', onAbort);\n }\n };\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import { type ServerSentEvent } from './uns-v2/sse.js';
2
+ export interface EventStreamOptions {
3
+ url: string;
4
+ headers?: Record<string, string>;
5
+ lastEventId?: string;
6
+ fetch?: typeof globalThis.fetch;
7
+ onEvent(event: ServerSentEvent): void;
8
+ /**
9
+ * The stream is back after a pause for the App's sleep or an authorization
10
+ * failure. Events may have been missed, resume position or not.
11
+ */
12
+ onResume?(): void;
13
+ /** The stream ended for good; it does not reconnect. Not called after `close()`. */
14
+ onClose?(error: EventStreamError): void;
15
+ /** Reconnect when nothing, not even a heartbeat, arrives for this long. Off by default. */
16
+ idleTimeoutMs?: number;
17
+ /** Close the connection while the page is hidden and resume from the last event id when it is visible. Default true. */
18
+ pauseWhenHidden?: boolean;
19
+ }
20
+ export interface EventStream {
21
+ close(): void;
22
+ }
23
+ export declare class EventStreamError extends Error {
24
+ readonly status: number;
25
+ readonly code: string | undefined;
26
+ constructor(status: number, code: string | undefined);
27
+ }
28
+ export declare function openEventStream(options: EventStreamOptions): EventStream;
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventStreamError = void 0;
4
+ exports.openEventStream = openEventStream;
5
+ /**
6
+ * A server-sent event stream over `fetch`, for browsers. `EventSource` gives up
7
+ * for good on any HTTP error, so a 503 from a sleeping App or a restarting node
8
+ * agent (design 21.1) would end it. This one reads the error and acts on it:
9
+ *
10
+ * - `sbx_sleeping`, `sbx_starting`: stop, and reconnect on the runtime's
11
+ * `resumed`. Without the runtime `sbx_sleeping` ends the stream and
12
+ * `sbx_starting` waits `retry_after_ms`.
13
+ * - an authorization failure: reported to the runtime, then as above. A
14
+ * successful reauthorization also fires `resumed`.
15
+ * - `node_restarting`, `upstream_error` and any other retryable failure, and a
16
+ * broken connection: reconnect by itself after `retry_after_ms` or a backoff,
17
+ * resuming from `Last-Event-ID`.
18
+ * - a non-retryable failure: ends the stream.
19
+ *
20
+ * While the runtime says `sleeping` nothing is sent: those requests would not
21
+ * wake the App anyway.
22
+ *
23
+ * While the page is hidden (`document.visibilityState`) the connection is
24
+ * closed, so a background tab does not hold a long connection to the App;
25
+ * when the page is visible again it reconnects at once from the last event
26
+ * id (`Last-Event-ID`). The server replays what was missed or answers with
27
+ * its own reset; `onResume` is not called for it. `pauseWhenHidden: false`
28
+ * keeps the connection open in the background.
29
+ */
30
+ const page_runtime_js_1 = require("./page-runtime.js");
31
+ const sse_js_1 = require("./uns-v2/sse.js");
32
+ const RECONNECT_BASE_MS = 1_000;
33
+ const RECONNECT_MAX_MS = 30_000;
34
+ /** A connection alive this long resets the backoff. */
35
+ const HEALTHY_CONNECTION_MS = 30_000;
36
+ /**
37
+ * A paused stream looks again this often while the runtime still says
38
+ * `running`: a routing race can answer `sbx_sleeping` for a running App, and
39
+ * then no `resumed` ever comes. Such a request cannot wake anything.
40
+ */
41
+ const PAUSE_RECHECK_MS = 30_000;
42
+ class EventStreamError extends Error {
43
+ status;
44
+ code;
45
+ constructor(status, code) {
46
+ super(`Event stream refused with HTTP ${status}${code ? ` (${code})` : ''}.`);
47
+ this.status = status;
48
+ this.code = code;
49
+ this.name = 'EventStreamError';
50
+ }
51
+ }
52
+ exports.EventStreamError = EventStreamError;
53
+ function classify(status, failure) {
54
+ const code = failure?.code;
55
+ const runtime = (0, page_runtime_js_1.getPageRuntime)() !== undefined;
56
+ if ((0, page_runtime_js_1.isAuthErrorCode)(code)) {
57
+ (0, page_runtime_js_1.reportAuthError)(code);
58
+ return runtime ? { next: 'pause' } : { next: 'end', error: new EventStreamError(status, code) };
59
+ }
60
+ if (code === 'sbx_sleeping' || code === 'sbx_starting' || code === 'sbx_start_failed') {
61
+ if (runtime)
62
+ return { next: 'pause' };
63
+ if (code !== 'sbx_starting')
64
+ return { next: 'end', error: new EventStreamError(status, code) };
65
+ }
66
+ if (failure?.retryable)
67
+ return { next: 'reconnect', afterMs: failure.retryAfterMs };
68
+ // Not ours to classify: an edge proxy's 5xx passes, a route's 4xx is final.
69
+ if (!failure && status >= 500)
70
+ return { next: 'reconnect' };
71
+ return { next: 'end', error: new EventStreamError(status, code) };
72
+ }
73
+ function openEventStream(options) {
74
+ const { url, onEvent, onResume, onClose, idleTimeoutMs } = options;
75
+ let lastEventId = options.lastEventId;
76
+ let closed = false;
77
+ let paused = (0, page_runtime_js_1.isAppSleeping)();
78
+ let resumedFromPause = false;
79
+ let failures = 0;
80
+ let resumes = 0;
81
+ let connection;
82
+ let wake;
83
+ const page = (options.pauseWhenHidden ?? true) && typeof document !== 'undefined' ? document : undefined;
84
+ let hidden = page?.visibilityState === 'hidden';
85
+ const onVisibility = () => {
86
+ hidden = page.visibilityState === 'hidden';
87
+ if (hidden)
88
+ connection?.abort();
89
+ wake?.();
90
+ };
91
+ page?.addEventListener('visibilitychange', onVisibility);
92
+ const sleep = (ms) => new Promise((resolve) => {
93
+ const timer = ms === undefined ? undefined : setTimeout(() => done(), ms);
94
+ const done = () => {
95
+ clearTimeout(timer);
96
+ wake = undefined;
97
+ resolve();
98
+ };
99
+ wake = done;
100
+ });
101
+ const offSleeping = (0, page_runtime_js_1.onPageRuntime)('sleeping', () => {
102
+ paused = true;
103
+ connection?.abort();
104
+ wake?.();
105
+ });
106
+ const offResumed = (0, page_runtime_js_1.onPageRuntime)('resumed', () => {
107
+ if (paused)
108
+ resumedFromPause = true;
109
+ paused = false;
110
+ resumes += 1;
111
+ wake?.();
112
+ });
113
+ function backoff() {
114
+ failures += 1;
115
+ const ceiling = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** (failures - 1));
116
+ return ceiling / 2 + Math.random() * (ceiling / 2);
117
+ }
118
+ async function consume(body, controller) {
119
+ const openedAt = Date.now();
120
+ let idle;
121
+ const arm = () => {
122
+ if (idleTimeoutMs === undefined)
123
+ return;
124
+ clearTimeout(idle);
125
+ idle = setTimeout(() => controller.abort(), idleTimeoutMs);
126
+ };
127
+ arm();
128
+ try {
129
+ for await (const event of (0, sse_js_1.parseServerSentEvents)(body)) {
130
+ arm();
131
+ if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS)
132
+ failures = 0;
133
+ if ('comment' in event)
134
+ continue;
135
+ if (event.id !== undefined)
136
+ lastEventId = event.id;
137
+ if ('event' in event)
138
+ onEvent(event);
139
+ }
140
+ }
141
+ finally {
142
+ clearTimeout(idle);
143
+ }
144
+ }
145
+ async function connect() {
146
+ const controller = new AbortController();
147
+ connection = controller;
148
+ const headers = { ...options.headers, Accept: 'text/event-stream' };
149
+ if (lastEventId !== undefined)
150
+ headers['Last-Event-ID'] = lastEventId;
151
+ try {
152
+ const response = await (options.fetch ?? globalThis.fetch)(url, { headers, cache: 'no-store', signal: controller.signal });
153
+ const type = response.headers.get('content-type') ?? '';
154
+ if (response.ok && /^text\/event-stream\b/i.test(type) && response.body) {
155
+ if (resumedFromPause) {
156
+ resumedFromPause = false;
157
+ onResume?.();
158
+ }
159
+ await consume(response.body, controller);
160
+ return { next: 'reconnect' };
161
+ }
162
+ const failure = await (0, page_runtime_js_1.readPlatformError)(response);
163
+ await response.body?.cancel().catch(() => undefined);
164
+ return classify(response.status, failure);
165
+ }
166
+ catch {
167
+ return { next: 'reconnect' };
168
+ }
169
+ finally {
170
+ connection = undefined;
171
+ }
172
+ }
173
+ async function run() {
174
+ while (!closed) {
175
+ if (hidden) {
176
+ await sleep();
177
+ continue;
178
+ }
179
+ if (paused) {
180
+ await sleep((0, page_runtime_js_1.getPageRuntime)()?.getState() === 'running' ? PAUSE_RECHECK_MS : undefined);
181
+ if ((0, page_runtime_js_1.getPageRuntime)()?.getState() === 'running' && !closed) {
182
+ resumedFromPause = true;
183
+ paused = false;
184
+ }
185
+ continue;
186
+ }
187
+ const seenResumes = resumes;
188
+ const outcome = await connect();
189
+ if (closed)
190
+ return;
191
+ if (outcome.next === 'end') {
192
+ close();
193
+ onClose?.(outcome.error);
194
+ return;
195
+ }
196
+ if (outcome.next === 'pause') {
197
+ // A `resumed` that arrived while the refusal was on its way already happened.
198
+ if (resumes === seenResumes)
199
+ paused = true;
200
+ else
201
+ resumedFromPause = true;
202
+ continue;
203
+ }
204
+ // Closed for a hidden page: wait for it to be visible, not for a backoff.
205
+ if (hidden)
206
+ continue;
207
+ if (!paused)
208
+ await sleep(outcome.afterMs ?? backoff());
209
+ }
210
+ }
211
+ function close() {
212
+ closed = true;
213
+ offSleeping();
214
+ offResumed();
215
+ page?.removeEventListener('visibilitychange', onVisibility);
216
+ connection?.abort();
217
+ wake?.();
218
+ }
219
+ void run();
220
+ return { close };
221
+ }
222
+ //# sourceMappingURL=event-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-stream.js","sourceRoot":"","sources":["../../src/event-stream.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A server-sent event stream over `fetch`, for browsers. `EventSource` gives up\n * for good on any HTTP error, so a 503 from a sleeping App or a restarting node\n * agent (design 21.1) would end it. This one reads the error and acts on it:\n *\n * - `sbx_sleeping`, `sbx_starting`: stop, and reconnect on the runtime's\n * `resumed`. Without the runtime `sbx_sleeping` ends the stream and\n * `sbx_starting` waits `retry_after_ms`.\n * - an authorization failure: reported to the runtime, then as above. A\n * successful reauthorization also fires `resumed`.\n * - `node_restarting`, `upstream_error` and any other retryable failure, and a\n * broken connection: reconnect by itself after `retry_after_ms` or a backoff,\n * resuming from `Last-Event-ID`.\n * - a non-retryable failure: ends the stream.\n *\n * While the runtime says `sleeping` nothing is sent: those requests would not\n * wake the App anyway.\n *\n * While the page is hidden (`document.visibilityState`) the connection is\n * closed, so a background tab does not hold a long connection to the App;\n * when the page is visible again it reconnects at once from the last event\n * id (`Last-Event-ID`). The server replays what was missed or answers with\n * its own reset; `onResume` is not called for it. `pauseWhenHidden: false`\n * keeps the connection open in the background.\n */\nimport { getPageRuntime, onPageRuntime, isAuthErrorCode, isAppSleeping, readPlatformError, reportAuthError, type PlatformError } from './page-runtime.js';\nimport { parseServerSentEvents, type ServerSentEvent } from './uns-v2/sse.js';\n\nconst RECONNECT_BASE_MS = 1_000;\nconst RECONNECT_MAX_MS = 30_000;\n/** A connection alive this long resets the backoff. */\nconst HEALTHY_CONNECTION_MS = 30_000;\n/**\n * A paused stream looks again this often while the runtime still says\n * `running`: a routing race can answer `sbx_sleeping` for a running App, and\n * then no `resumed` ever comes. Such a request cannot wake anything.\n */\nconst PAUSE_RECHECK_MS = 30_000;\n\nexport interface EventStreamOptions {\n url: string;\n headers?: Record<string, string>;\n lastEventId?: string;\n fetch?: typeof globalThis.fetch;\n onEvent(event: ServerSentEvent): void;\n /**\n * The stream is back after a pause for the App's sleep or an authorization\n * failure. Events may have been missed, resume position or not.\n */\n onResume?(): void;\n /** The stream ended for good; it does not reconnect. Not called after `close()`. */\n onClose?(error: EventStreamError): void;\n /** Reconnect when nothing, not even a heartbeat, arrives for this long. Off by default. */\n idleTimeoutMs?: number;\n /** Close the connection while the page is hidden and resume from the last event id when it is visible. Default true. */\n pauseWhenHidden?: boolean;\n}\n\nexport interface EventStream {\n close(): void;\n}\n\nexport class EventStreamError extends Error {\n constructor(readonly status: number, readonly code: string | undefined) {\n super(`Event stream refused with HTTP ${status}${code ? ` (${code})` : ''}.`);\n this.name = 'EventStreamError';\n }\n}\n\ntype Outcome =\n | { next: 'reconnect'; afterMs?: number }\n | { next: 'pause' }\n | { next: 'end'; error: EventStreamError };\n\nfunction classify(status: number, failure: PlatformError | undefined): Outcome {\n const code = failure?.code;\n const runtime = getPageRuntime() !== undefined;\n if (isAuthErrorCode(code)) {\n reportAuthError(code);\n return runtime ? { next: 'pause' } : { next: 'end', error: new EventStreamError(status, code) };\n }\n if (code === 'sbx_sleeping' || code === 'sbx_starting' || code === 'sbx_start_failed') {\n if (runtime) return { next: 'pause' };\n if (code !== 'sbx_starting') return { next: 'end', error: new EventStreamError(status, code) };\n }\n if (failure?.retryable) return { next: 'reconnect', afterMs: failure.retryAfterMs };\n // Not ours to classify: an edge proxy's 5xx passes, a route's 4xx is final.\n if (!failure && status >= 500) return { next: 'reconnect' };\n return { next: 'end', error: new EventStreamError(status, code) };\n}\n\nexport function openEventStream(options: EventStreamOptions): EventStream {\n const { url, onEvent, onResume, onClose, idleTimeoutMs } = options;\n let lastEventId = options.lastEventId;\n let closed = false;\n let paused = isAppSleeping();\n let resumedFromPause = false;\n let failures = 0;\n let resumes = 0;\n let connection: AbortController | undefined;\n let wake: (() => void) | undefined;\n const page = (options.pauseWhenHidden ?? true) && typeof document !== 'undefined' ? document : undefined;\n let hidden = page?.visibilityState === 'hidden';\n const onVisibility = () => {\n hidden = page!.visibilityState === 'hidden';\n if (hidden) connection?.abort();\n wake?.();\n };\n page?.addEventListener('visibilitychange', onVisibility);\n\n const sleep = (ms?: number) => new Promise<void>((resolve) => {\n const timer = ms === undefined ? undefined : setTimeout(() => done(), ms);\n const done = () => {\n clearTimeout(timer);\n wake = undefined;\n resolve();\n };\n wake = done;\n });\n\n const offSleeping = onPageRuntime('sleeping', () => {\n paused = true;\n connection?.abort();\n wake?.();\n });\n const offResumed = onPageRuntime('resumed', () => {\n if (paused) resumedFromPause = true;\n paused = false;\n resumes += 1;\n wake?.();\n });\n\n function backoff(): number {\n failures += 1;\n const ceiling = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** (failures - 1));\n return ceiling / 2 + Math.random() * (ceiling / 2);\n }\n\n async function consume(body: ReadableStream<Uint8Array>, controller: AbortController): Promise<void> {\n const openedAt = Date.now();\n let idle: ReturnType<typeof setTimeout> | undefined;\n const arm = () => {\n if (idleTimeoutMs === undefined) return;\n clearTimeout(idle);\n idle = setTimeout(() => controller.abort(), idleTimeoutMs);\n };\n arm();\n try {\n for await (const event of parseServerSentEvents(body)) {\n arm();\n if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS) failures = 0;\n if ('comment' in event) continue;\n if (event.id !== undefined) lastEventId = event.id;\n if ('event' in event) onEvent(event);\n }\n } finally {\n clearTimeout(idle);\n }\n }\n\n async function connect(): Promise<Outcome> {\n const controller = new AbortController();\n connection = controller;\n const headers: Record<string, string> = { ...options.headers, Accept: 'text/event-stream' };\n if (lastEventId !== undefined) headers['Last-Event-ID'] = lastEventId;\n try {\n const response = await (options.fetch ?? globalThis.fetch)(url, { headers, cache: 'no-store', signal: controller.signal });\n const type = response.headers.get('content-type') ?? '';\n if (response.ok && /^text\\/event-stream\\b/i.test(type) && response.body) {\n if (resumedFromPause) {\n resumedFromPause = false;\n onResume?.();\n }\n await consume(response.body, controller);\n return { next: 'reconnect' };\n }\n const failure = await readPlatformError(response);\n await response.body?.cancel().catch(() => undefined);\n return classify(response.status, failure);\n } catch {\n return { next: 'reconnect' };\n } finally {\n connection = undefined;\n }\n }\n\n async function run(): Promise<void> {\n while (!closed) {\n if (hidden) {\n await sleep();\n continue;\n }\n if (paused) {\n await sleep(getPageRuntime()?.getState() === 'running' ? PAUSE_RECHECK_MS : undefined);\n if (getPageRuntime()?.getState() === 'running' && !closed) {\n resumedFromPause = true;\n paused = false;\n }\n continue;\n }\n const seenResumes = resumes;\n const outcome = await connect();\n if (closed) return;\n if (outcome.next === 'end') {\n close();\n onClose?.(outcome.error);\n return;\n }\n if (outcome.next === 'pause') {\n // A `resumed` that arrived while the refusal was on its way already happened.\n if (resumes === seenResumes) paused = true;\n else resumedFromPause = true;\n continue;\n }\n // Closed for a hidden page: wait for it to be visible, not for a backoff.\n if (hidden) continue;\n if (!paused) await sleep(outcome.afterMs ?? backoff());\n }\n }\n\n function close(): void {\n closed = true;\n offSleeping();\n offResumed();\n page?.removeEventListener('visibilitychange', onVisibility);\n connection?.abort();\n wake?.();\n }\n\n void run();\n return { close };\n}\n"]}
@@ -13,11 +13,40 @@ export declare class Tier0MQClient {
13
13
  private explicitUsername;
14
14
  private listeners;
15
15
  private subscriptions;
16
+ private topicSubscriptions;
17
+ private pendingTopicPackets;
18
+ private sentTopicPackets;
16
19
  private connectingPromise;
20
+ private rejectPendingConnect;
17
21
  private _connected;
22
+ /** Bumped by disconnect(); callbacks of an older generation are ignored. */
23
+ private generation;
24
+ /** Random clientId segment; a persistent session is found again through it. */
25
+ private readonly clientSuffix;
26
+ private reconnectTimer;
27
+ private reconnectAttempts;
28
+ private connectWaiters;
18
29
  constructor(config?: MQTTConfig);
19
- private get mqttUrl();
30
+ private mqttUrlFor;
31
+ /**
32
+ * Where the credential comes from; undefined when a fixed password is used.
33
+ * `proxy`: platform proxy mode, the node proxy puts in the App's key and
34
+ * the SDK sends none.
35
+ */
36
+ private tokenSource;
37
+ /** Local preflight before a transport exists: address and credentials must be usable. */
38
+ private validateConnectionConfig;
39
+ /**
40
+ * What the next connection presents. Through the node proxy the same
41
+ * identity reconnects, and the proxy puts in the App's current key.
42
+ */
43
+ private connectionParams;
20
44
  private ensureConnected;
45
+ private clearReconnect;
46
+ private reconnectDelay;
47
+ private scheduleReconnect;
48
+ /** The same client: mqtt.js keeps its session and offline queue. */
49
+ private reconnect;
21
50
  private doConnect;
22
51
  connect(config?: MQTTConfig): Promise<void>;
23
52
  subscribe(topic: string, handler: TopicHandler): void;
@@ -31,6 +60,8 @@ export declare class Tier0MQClient {
31
60
  off<K extends keyof MQTTEventMap>(event: K, handler: MQTTEventMap[K]): void;
32
61
  private emit;
33
62
  private resubscribeAll;
63
+ private clearTopicState;
64
+ private subscribeTopic;
34
65
  private topicMatch;
35
66
  get isConnected(): boolean;
36
67
  get subscribedTopics(): string[];