@chelone/core 0.5.0 → 0.7.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 (253) hide show
  1. package/dist/assistant/assistant-session.d.ts +28 -0
  2. package/dist/assistant/assistant-session.js +208 -0
  3. package/dist/assistant/echo-transport.d.ts +7 -0
  4. package/dist/assistant/echo-transport.js +28 -0
  5. package/dist/assistant/http-transport.d.ts +10 -0
  6. package/dist/assistant/http-transport.js +98 -0
  7. package/dist/assistant/types.d.ts +54 -0
  8. package/dist/auth/access-control.d.ts +13 -5
  9. package/dist/auth/access-control.js +25 -21
  10. package/dist/auth/auth-options.d.ts +21 -17
  11. package/dist/auth/auth-options.js +80 -48
  12. package/dist/auth/permission-matrix.d.ts +12 -0
  13. package/dist/auth/permission-matrix.js +67 -0
  14. package/dist/auth/types.d.ts +14 -4
  15. package/dist/block-editor/block-document.d.ts +60 -0
  16. package/dist/block-editor/block-document.js +143 -0
  17. package/dist/block-editor/data-blocks.d.ts +63 -0
  18. package/dist/block-editor/data-blocks.js +117 -0
  19. package/dist/chart/chart-data.d.ts +25 -0
  20. package/dist/chart/chart-data.js +61 -0
  21. package/dist/command-menu/hotkey.js +58 -47
  22. package/dist/command-menu/recents.js +8 -11
  23. package/dist/command-menu/registry.d.ts +9 -5
  24. package/dist/command-menu/registry.js +36 -46
  25. package/dist/controller/calendar-dates.js +82 -113
  26. package/dist/controller/calendar-events.js +93 -88
  27. package/dist/controller/client-list-controller.d.ts +2 -2
  28. package/dist/controller/client-list-controller.js +251 -254
  29. package/dist/controller/data-table-columns.d.ts +2 -0
  30. package/dist/controller/data-table-columns.js +50 -56
  31. package/dist/controller/deep-equal.d.ts +1 -0
  32. package/dist/controller/deep-equal.js +14 -0
  33. package/dist/controller/filter-toggle.js +36 -44
  34. package/dist/controller/in-place-editor.d.ts +42 -0
  35. package/dist/controller/in-place-editor.js +42 -0
  36. package/dist/controller/infinite-list-controller.js +220 -190
  37. package/dist/controller/inspector.d.ts +49 -0
  38. package/dist/controller/inspector.js +64 -0
  39. package/dist/controller/job-monitor.d.ts +41 -0
  40. package/dist/controller/job-monitor.js +49 -0
  41. package/dist/controller/kanban-board.d.ts +2 -3
  42. package/dist/controller/kanban-board.js +43 -24
  43. package/dist/controller/list-controller.d.ts +5 -5
  44. package/dist/controller/list-controller.js +444 -400
  45. package/dist/controller/list-params.js +96 -87
  46. package/dist/controller/pivot-grid.d.ts +36 -0
  47. package/dist/controller/pivot-grid.js +99 -0
  48. package/dist/controller/prev-next.d.ts +2 -2
  49. package/dist/controller/prev-next.js +74 -54
  50. package/dist/controller/record-controllers.d.ts +14 -6
  51. package/dist/controller/record-controllers.js +288 -257
  52. package/dist/controller/record-diff.d.ts +21 -0
  53. package/dist/controller/record-diff.js +50 -0
  54. package/dist/controller/record-timeline.d.ts +35 -0
  55. package/dist/controller/record-timeline.js +48 -0
  56. package/dist/controller/record-tree.js +57 -47
  57. package/dist/controller/record-workflow.d.ts +38 -16
  58. package/dist/controller/record-workflow.js +85 -64
  59. package/dist/controller/reference-many-controller.js +198 -192
  60. package/dist/controller/reference-one-controller.d.ts +1 -1
  61. package/dist/controller/reference-one-controller.js +63 -56
  62. package/dist/controller/saved-queries.js +104 -91
  63. package/dist/controller/translate-fallback.js +10 -6
  64. package/dist/csv/export.d.ts +2 -1
  65. package/dist/csv/export.js +43 -40
  66. package/dist/csv/import-controller.js +185 -169
  67. package/dist/csv/match-headers.js +29 -22
  68. package/dist/csv/parse-with.js +60 -54
  69. package/dist/csv/run-import.js +69 -72
  70. package/dist/csv/validate-rows.d.ts +1 -1
  71. package/dist/csv/validate-rows.js +65 -52
  72. package/dist/data/cache-ops.d.ts +5 -4
  73. package/dist/data/cache-ops.js +208 -190
  74. package/dist/data/combine-data-providers.js +25 -23
  75. package/dist/data/data-provider-log.d.ts +45 -0
  76. package/dist/data/data-provider-log.js +93 -0
  77. package/dist/data/errors.js +14 -10
  78. package/dist/data/fake-provider.d.ts +6 -3
  79. package/dist/data/fake-provider.js +129 -114
  80. package/dist/data/fetch-utils.js +61 -44
  81. package/dist/data/filter-branch.d.ts +5 -0
  82. package/dist/data/filter-branch.js +10 -0
  83. package/dist/data/get-many-aggregate.js +84 -76
  84. package/dist/data/http-error.js +17 -12
  85. package/dist/data/keys.js +50 -36
  86. package/dist/data/mutation-options.d.ts +14 -9
  87. package/dist/data/mutation-options.js +93 -109
  88. package/dist/data/query-options.js +73 -68
  89. package/dist/data/remove-empty.d.ts +3 -0
  90. package/dist/data/remove-empty.js +11 -0
  91. package/dist/data/undoable.d.ts +2 -2
  92. package/dist/data/undoable.js +190 -174
  93. package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
  94. package/dist/data/with-lifecycle-callbacks.js +66 -52
  95. package/dist/environment/application-update-store.js +58 -55
  96. package/dist/environment/offline-store.js +21 -21
  97. package/dist/form/array-binding.d.ts +30 -11
  98. package/dist/form/array-binding.js +184 -166
  99. package/dist/form/choice-ids.d.ts +7 -0
  100. package/dist/form/choice-ids.js +11 -0
  101. package/dist/form/choices-controller.d.ts +3 -2
  102. package/dist/form/choices-controller.js +302 -275
  103. package/dist/form/date-format.d.ts +38 -0
  104. package/dist/form/date-format.js +89 -0
  105. package/dist/form/dirty-values.d.ts +2 -2
  106. package/dist/form/dirty-values.js +22 -11
  107. package/dist/form/duration.d.ts +22 -0
  108. package/dist/form/duration.js +49 -0
  109. package/dist/form/edit-create-form.d.ts +12 -5
  110. package/dist/form/edit-create-form.js +243 -188
  111. package/dist/form/event-value.d.ts +6 -0
  112. package/dist/form/event-value.js +17 -0
  113. package/dist/form/field-error.d.ts +14 -0
  114. package/dist/form/field-error.js +28 -0
  115. package/dist/form/field-name.d.ts +20 -0
  116. package/dist/form/field-name.js +6 -0
  117. package/dist/form/filter-form-values.js +63 -49
  118. package/dist/form/form-controller.d.ts +9 -1
  119. package/dist/form/form-controller.js +165 -160
  120. package/dist/form/form-groups.d.ts +4 -3
  121. package/dist/form/form-groups.js +79 -79
  122. package/dist/form/form-handle.d.ts +175 -0
  123. package/dist/form/form-handle.js +68 -0
  124. package/dist/form/input-binding.d.ts +56 -31
  125. package/dist/form/input-binding.js +169 -118
  126. package/dist/form/input-values.d.ts +130 -0
  127. package/dist/form/input-values.js +177 -0
  128. package/dist/form/json-text.js +51 -37
  129. package/dist/form/save-pipeline.d.ts +6 -8
  130. package/dist/form/save-pipeline.js +91 -82
  131. package/dist/form/set-form-values.d.ts +6 -0
  132. package/dist/form/set-form-values.js +10 -0
  133. package/dist/form/source.js +97 -65
  134. package/dist/form/text-of.d.ts +2 -0
  135. package/dist/form/text-of.js +7 -0
  136. package/dist/form/validators.d.ts +58 -19
  137. package/dist/form/validators.js +97 -71
  138. package/dist/geo/bbox.d.ts +5 -4
  139. package/dist/geo/bbox.js +69 -48
  140. package/dist/geo/bounds.d.ts +14 -3
  141. package/dist/geo/bounds.js +98 -68
  142. package/dist/geo/constraints.js +88 -93
  143. package/dist/geo/coordinates.js +31 -23
  144. package/dist/geo/editor.d.ts +13 -13
  145. package/dist/geo/editor.js +176 -153
  146. package/dist/geo/geocoding.d.ts +4 -4
  147. package/dist/geo/geocoding.js +38 -41
  148. package/dist/geo/geojson.d.ts +12 -74
  149. package/dist/geo/geojson.js +52 -40
  150. package/dist/geo/geometry-math.d.ts +4 -4
  151. package/dist/geo/geometry-math.js +100 -93
  152. package/dist/geo/index.d.ts +7 -6
  153. package/dist/geo/map-layers.d.ts +42 -7
  154. package/dist/geo/map-layers.js +88 -62
  155. package/dist/geo/map-style.js +31 -32
  156. package/dist/geo/messages.js +48 -45
  157. package/dist/geo/nominatim.js +88 -80
  158. package/dist/geo/osm-presets.d.ts +4 -3
  159. package/dist/geo/osm-presets.js +108 -86
  160. package/dist/geo/osm-tags.js +14 -12
  161. package/dist/geo/overpass.d.ts +14 -2
  162. package/dist/geo/overpass.js +83 -90
  163. package/dist/geo/shape-value.d.ts +9 -5
  164. package/dist/geo/shape-value.js +169 -146
  165. package/dist/geo/snap-to-roads.d.ts +3 -3
  166. package/dist/geo/snap-to-roads.js +55 -50
  167. package/dist/geo/throttle.js +36 -39
  168. package/dist/guards.d.ts +37 -0
  169. package/dist/guards.js +62 -0
  170. package/dist/i18n/default-messages.js +34 -29
  171. package/dist/i18n/english-messages.js +187 -189
  172. package/dist/i18n/i18n.d.ts +8 -2
  173. package/dist/i18n/i18n.js +47 -48
  174. package/dist/i18n/inflection.d.ts +34 -0
  175. package/dist/i18n/inflection.js +82 -0
  176. package/dist/i18n/messages.d.ts +10 -2
  177. package/dist/i18n/messages.js +17 -4
  178. package/dist/i18n/missing-keys.d.ts +19 -0
  179. package/dist/i18n/missing-keys.js +68 -0
  180. package/dist/i18n/translatable.d.ts +16 -1
  181. package/dist/i18n/translatable.js +40 -47
  182. package/dist/index.d.ts +96 -22
  183. package/dist/index.js +52 -257
  184. package/dist/json/deep-equal.d.ts +8 -0
  185. package/dist/json/deep-equal.js +24 -0
  186. package/dist/json/strip-json-comments.js +38 -42
  187. package/dist/noop.d.ts +2 -0
  188. package/dist/noop.js +4 -0
  189. package/dist/notification/error-message.d.ts +2 -0
  190. package/dist/notification/error-message.js +10 -0
  191. package/dist/notification/notification-store.d.ts +3 -2
  192. package/dist/notification/notification-store.js +41 -45
  193. package/dist/onboarding-tour/placement.d.ts +34 -0
  194. package/dist/onboarding-tour/placement.js +67 -0
  195. package/dist/onboarding-tour/tour-controller.d.ts +25 -0
  196. package/dist/onboarding-tour/tour-controller.js +50 -0
  197. package/dist/realtime/add-events-for-mutations.js +75 -81
  198. package/dist/realtime/announce-lock.d.ts +7 -0
  199. package/dist/realtime/announce-lock.js +18 -0
  200. package/dist/realtime/broadcast-channel-transport.d.ts +14 -0
  201. package/dist/realtime/broadcast-channel-transport.js +69 -0
  202. package/dist/realtime/copy-members.js +22 -14
  203. package/dist/realtime/in-memory-lock-provider.js +41 -60
  204. package/dist/realtime/live.js +95 -101
  205. package/dist/realtime/lock-options.js +16 -16
  206. package/dist/realtime/lock-session.js +91 -78
  207. package/dist/realtime/memory-transport.js +26 -67
  208. package/dist/realtime/presence.js +120 -122
  209. package/dist/realtime/realtime-data-provider.d.ts +10 -0
  210. package/dist/realtime/realtime-data-provider.js +45 -42
  211. package/dist/realtime/reconnect-backoff.d.ts +15 -0
  212. package/dist/realtime/reconnect-backoff.js +17 -0
  213. package/dist/realtime/sse-transport.d.ts +31 -0
  214. package/dist/realtime/sse-transport.js +235 -0
  215. package/dist/realtime/status-store.js +23 -28
  216. package/dist/realtime/topic-hub.d.ts +24 -0
  217. package/dist/realtime/topic-hub.js +66 -0
  218. package/dist/realtime/topics.js +10 -12
  219. package/dist/realtime/types.js +10 -12
  220. package/dist/realtime/unread-count.js +21 -21
  221. package/dist/realtime/websocket-transport.d.ts +32 -0
  222. package/dist/realtime/websocket-transport.js +306 -0
  223. package/dist/resource/describe-resource.d.ts +2 -0
  224. package/dist/resource/describe-resource.js +232 -219
  225. package/dist/resource/get-at-path.js +29 -14
  226. package/dist/resource/identifier.d.ts +18 -0
  227. package/dist/resource/identifier.js +27 -0
  228. package/dist/resource/record.d.ts +4 -0
  229. package/dist/resource/record.js +10 -0
  230. package/dist/resource/register.d.ts +7 -0
  231. package/dist/resource/registry-store.d.ts +45 -15
  232. package/dist/resource/registry-store.js +67 -60
  233. package/dist/resource/registry.d.ts +10 -0
  234. package/dist/resource/registry.js +8 -4
  235. package/dist/resource/source-path.d.ts +16 -1
  236. package/dist/rich-text/rich-text-extras.d.ts +132 -0
  237. package/dist/rich-text/rich-text-extras.js +179 -0
  238. package/dist/rich-text/rich-text-image.d.ts +124 -0
  239. package/dist/rich-text/rich-text-image.js +163 -0
  240. package/dist/rich-text/rich-text.d.ts +62 -0
  241. package/dist/rich-text/rich-text.js +142 -0
  242. package/dist/router/location.d.ts +22 -4
  243. package/dist/router/location.js +41 -18
  244. package/dist/router/match.js +47 -39
  245. package/dist/router/paths.js +40 -34
  246. package/dist/router/record-from-location.d.ts +1 -1
  247. package/dist/router/record-from-location.js +25 -17
  248. package/dist/store/adapters.js +51 -55
  249. package/dist/store/micro-store.js +57 -59
  250. package/dist/store/shallow-equal.d.ts +12 -0
  251. package/dist/store/shallow-equal.js +20 -0
  252. package/dist/store/store.js +54 -57
  253. package/package.json +13 -11
@@ -1,128 +1,126 @@
1
+ import { isRecord } from "../guards.js";
1
2
  import { createMicroStore } from "../store/micro-store.js";
2
- const toError = (error) => error instanceof Error ? error : new Error(String(error));
3
- const sameUser = (a, b) => a.id === b.id && a.name === b.name && a.avatar === b.avatar;
4
- const isRecord = (value) => typeof value === "object" && value !== null;
5
- const isPresenceUser = (value) => isRecord(value) && typeof value.id === "string" && typeof value.name === "string";
6
- function createPresenceSession({
7
- dataProvider,
8
- topic,
9
- user,
10
- sessionId = crypto.randomUUID(),
11
- heartbeatMs = 15e3,
12
- staleMs = 3e4,
13
- now = Date.now,
14
- onError
15
- }) {
16
- const store = createMicroStore({ users: [] });
17
- const sessions = /* @__PURE__ */ new Map();
18
- let running = false;
19
- let unsubscribe;
20
- let unsubscribeReconnect;
21
- let heartbeat;
22
- let sweep;
23
- const report = (cause) => {
24
- try {
25
- onError?.(toError(cause));
26
- } catch {
27
- }
28
- };
29
- const publish = (type, payload) => {
30
- try {
31
- dataProvider.publish(topic, { type, payload }).catch(report);
32
- } catch (cause) {
33
- report(cause);
34
- }
35
- };
36
- const announce = () => publish("presence", { sessionId, user });
37
- const refresh = () => {
38
- const live = /* @__PURE__ */ new Map();
39
- for (const entry of sessions.values()) live.set(entry.user.id, entry.user);
40
- const previous = store.getSnapshot().users;
41
- const users = previous.flatMap((listed) => live.get(listed.id) ?? []);
42
- const listedIds = new Set(users.map((listed) => listed.id));
43
- for (const [id, other] of live) if (!listedIds.has(id)) users.push(other);
44
- const unchanged = users.length === previous.length && users.every((other, index) => {
45
- const prior = previous[index];
46
- return prior !== void 0 && sameUser(other, prior);
47
- });
48
- if (!unchanged) store.setState({ users });
49
- };
50
- const handle = ({ type, payload }) => {
51
- try {
52
- if (!isRecord(payload) || typeof payload.sessionId !== "string") return;
53
- const from = payload.sessionId;
54
- if (from === sessionId) return;
55
- if (type === "presence_leave") {
56
- sessions.delete(from);
57
- refresh();
58
- } else if (type === "presence" && isPresenceUser(payload.user)) {
59
- const known = sessions.has(from);
60
- sessions.set(from, { user: payload.user, heardAt: now() });
61
- if (!known) announce();
62
- refresh();
63
- }
64
- } catch (cause) {
65
- report(cause);
66
- }
67
- };
68
- return {
69
- store,
70
- start() {
71
- if (running) return;
72
- running = true;
73
- unsubscribe = dataProvider.subscribe(topic, handle);
74
- unsubscribeReconnect = dataProvider.onReconnect?.(announce);
75
- announce();
76
- heartbeat = setInterval(announce, heartbeatMs);
77
- sweep = setInterval(
78
- () => {
79
- try {
80
- const cutoff = now() - staleMs;
81
- for (const [id, entry] of sessions)
82
- if (entry.heardAt < cutoff) sessions.delete(id);
83
- refresh();
84
- } catch (cause) {
85
- report(cause);
86
- }
87
- },
88
- Math.min(staleMs / 2, 5e3)
89
- );
90
- },
91
- stop() {
92
- if (!running) return;
93
- running = false;
94
- unsubscribe?.();
95
- unsubscribe = void 0;
96
- unsubscribeReconnect?.();
97
- unsubscribeReconnect = void 0;
98
- clearInterval(heartbeat);
99
- clearInterval(sweep);
100
- heartbeat = sweep = void 0;
101
- publish("presence_leave", { sessionId });
102
- sessions.clear();
103
- try {
104
- store.setState({ users: [] });
105
- } catch (cause) {
106
- report(cause);
107
- }
108
- }
109
- };
3
+ // src/realtime/presence.ts
4
+ var toError = (error) => error instanceof Error ? error : new Error(String(error));
5
+ var sameUser = (a, b) => a.id === b.id && a.name === b.name && a.avatar === b.avatar;
6
+ var isPresenceUser = (value) => isRecord(value) && typeof value.id === "string" && typeof value.name === "string";
7
+ /**
8
+ * Announces `user` on `topic` and keeps the other users who announce there.
9
+ * Self is told by `sessionId`, not by user id, so a second tab of the same
10
+ * person still counts. Staleness is measured with `now()` at receipt, never
11
+ * from a time the sender wrote.
12
+ */
13
+ function createPresenceSession({ dataProvider, topic, user, sessionId = crypto.randomUUID(), heartbeatMs = 15e3, staleMs = 3e4, now = Date.now, onError }) {
14
+ const store = createMicroStore({ users: [] });
15
+ const sessions = /* @__PURE__ */ new Map();
16
+ let running = false;
17
+ let unsubscribe;
18
+ let unsubscribeReconnect;
19
+ let heartbeat;
20
+ let sweep;
21
+ const report = (cause) => {
22
+ try {
23
+ onError?.(toError(cause));
24
+ } catch {}
25
+ };
26
+ const publish = (type, payload) => {
27
+ try {
28
+ dataProvider.publish(topic, {
29
+ type,
30
+ payload
31
+ }).catch(report);
32
+ } catch (cause) {
33
+ report(cause);
34
+ }
35
+ };
36
+ const announce = () => publish("presence", {
37
+ sessionId,
38
+ user
39
+ });
40
+ const refresh = () => {
41
+ const live = /* @__PURE__ */ new Map();
42
+ for (const entry of sessions.values()) live.set(entry.user.id, entry.user);
43
+ const previous = store.getSnapshot().users;
44
+ const users = previous.flatMap((listed) => live.get(listed.id) ?? []);
45
+ const listedIds = new Set(users.map((listed) => listed.id));
46
+ for (const [id, other] of live) if (!listedIds.has(id)) users.push(other);
47
+ if (!(users.length === previous.length && users.every((other, index) => {
48
+ const prior = previous[index];
49
+ return prior !== void 0 && sameUser(other, prior);
50
+ }))) store.setState({ users });
51
+ };
52
+ const handle = ({ type, payload }) => {
53
+ try {
54
+ if (!isRecord(payload) || typeof payload.sessionId !== "string") return;
55
+ const from = payload.sessionId;
56
+ if (from === sessionId) return;
57
+ if (type === "presence_leave") {
58
+ sessions.delete(from);
59
+ refresh();
60
+ } else if (type === "presence" && isPresenceUser(payload.user)) {
61
+ const known = sessions.has(from);
62
+ sessions.set(from, {
63
+ user: payload.user,
64
+ heardAt: now()
65
+ });
66
+ if (!known) announce();
67
+ refresh();
68
+ }
69
+ } catch (cause) {
70
+ report(cause);
71
+ }
72
+ };
73
+ return {
74
+ store,
75
+ start() {
76
+ if (running) return;
77
+ running = true;
78
+ unsubscribe = dataProvider.subscribe(topic, handle);
79
+ unsubscribeReconnect = dataProvider.onReconnect?.(announce);
80
+ announce();
81
+ heartbeat = setInterval(announce, heartbeatMs);
82
+ sweep = setInterval(() => {
83
+ try {
84
+ const cutoff = now() - staleMs;
85
+ for (const [id, entry] of sessions) if (entry.heardAt < cutoff) sessions.delete(id);
86
+ refresh();
87
+ } catch (cause) {
88
+ report(cause);
89
+ }
90
+ }, Math.min(staleMs / 2, 5e3));
91
+ },
92
+ stop() {
93
+ if (!running) return;
94
+ running = false;
95
+ unsubscribe?.();
96
+ unsubscribe = void 0;
97
+ unsubscribeReconnect?.();
98
+ unsubscribeReconnect = void 0;
99
+ clearInterval(heartbeat);
100
+ clearInterval(sweep);
101
+ heartbeat = sweep = void 0;
102
+ publish("presence_leave", { sessionId });
103
+ sessions.clear();
104
+ try {
105
+ store.setState({ users: [] });
106
+ } catch (cause) {
107
+ report(cause);
108
+ }
109
+ }
110
+ };
110
111
  }
111
112
  function toPresenceUser(identity) {
112
- const id = String(identity.id);
113
- return {
114
- id,
115
- name: identity.fullName || id,
116
- ...typeof identity.avatar === "string" ? { avatar: identity.avatar } : {}
117
- };
113
+ const id = String(identity.id);
114
+ return {
115
+ id,
116
+ name: identity.fullName || id,
117
+ ...typeof identity.avatar === "string" ? { avatar: identity.avatar } : {}
118
+ };
118
119
  }
120
+ /** The first letters of the first two words, or `?` for a blank name. */
119
121
  function getPresenceInitials(name) {
120
- const words = name.trim().split(/\s+/).filter(Boolean).slice(0, 2);
121
- if (words.length === 0) return "?";
122
- return words.map((word) => Array.from(word)[0]).join("").toUpperCase();
122
+ const words = name.trim().split(/\s+/).filter(Boolean).slice(0, 2);
123
+ if (words.length === 0) return "?";
124
+ return words.map((word) => Array.from(word)[0]).join("").toUpperCase();
123
125
  }
124
- export {
125
- createPresenceSession,
126
- getPresenceInitials,
127
- toPresenceUser
128
- };
126
+ export { createPresenceSession, getPresenceInitials, toPresenceUser };
@@ -11,4 +11,14 @@ import type { RealtimeDataProvider, RealtimeDataProviderOptions, RealtimeTranspo
11
11
  export declare function realtimeDataProvider<TResources extends ResourceMap = ResourceMap>(base: DataProvider<TResources>, transport: RealtimeTransport, { locks }?: RealtimeDataProviderOptions): RealtimeDataProvider<TResources>;
12
12
  /** True when `dataProvider` was wrapped with `realtimeDataProvider()`. */
13
13
  export declare function isRealtimeDataProvider<TResources extends ResourceMap = ResourceMap>(dataProvider: DataProvider<TResources>): dataProvider is RealtimeDataProvider<TResources>;
14
+ declare const transportMembers: readonly ["onReconnect", "onStatusChange", "getStatus", "onError"];
15
+ /** The optional transport members a data provider may carry without being a full `RealtimeDataProvider`. */
16
+ export type TransportMembers = Pick<RealtimeDataProvider, (typeof transportMembers)[number]>;
17
+ /**
18
+ * True when every transport member `dataProvider` has is a function. Absent
19
+ * members are fine: they are optional, and a provider with only some of them
20
+ * (no `publish`, say) still reports reconnects and status.
21
+ */
22
+ export declare function hasTransportMembers<TResources extends ResourceMap = ResourceMap>(dataProvider: DataProvider<TResources>): dataProvider is DataProvider<TResources> & TransportMembers;
14
23
  export declare function assertRealtimeDataProvider<TResources extends ResourceMap = ResourceMap>(dataProvider: DataProvider<TResources>, caller: string): asserts dataProvider is RealtimeDataProvider<TResources>;
24
+ export {};
@@ -1,49 +1,52 @@
1
1
  import { copyMembers } from "./copy-members.js";
2
- const noLockProvider = (method) => Promise.reject(
3
- new Error(
4
- `realtimeDataProvider: ${method}() called but no LockProvider was configured`
5
- )
6
- );
2
+ // src/realtime/realtime-data-provider.ts
3
+ var noLockProvider = (method) => Promise.reject(/* @__PURE__ */ new Error(`realtimeDataProvider: ${method}() called but no LockProvider was configured`));
4
+ /**
5
+ * Adds `subscribe`, `publish` and the lock methods to any data provider. The
6
+ * optional transport members are copied only when the transport has them, so
7
+ * `"getStatus" in dataProvider` tells a caller whether it can read a status.
8
+ * Wrap the result of `combineDataProviders`, not the reverse: it keeps only
9
+ * the nine verbs.
10
+ */
7
11
  function realtimeDataProvider(base, transport, { locks } = {}) {
8
- const lockMethods = {
9
- lock: (resource, params) => locks ? locks.lock(resource, params) : noLockProvider("lock"),
10
- unlock: (resource, params) => locks ? locks.unlock(resource, params) : noLockProvider("unlock"),
11
- getLock: (resource, params) => locks ? locks.getLock(resource, params) : noLockProvider("getLock"),
12
- getLocks: (resource, params) => locks ? locks.getLocks(resource, params) : noLockProvider("getLocks")
13
- };
14
- const provider = {
15
- ...copyMembers(base),
16
- ...lockMethods,
17
- subscribe: (topic, callback) => transport.subscribe(topic, callback),
18
- publish: (topic, event) => transport.publish(topic, event)
19
- };
20
- if (transport.onReconnect) {
21
- provider.onReconnect = transport.onReconnect.bind(transport);
22
- }
23
- if (transport.onStatusChange) {
24
- provider.onStatusChange = transport.onStatusChange.bind(transport);
25
- }
26
- if (transport.getStatus) {
27
- provider.getStatus = transport.getStatus.bind(transport);
28
- }
29
- if (transport.onError) {
30
- provider.onError = transport.onError.bind(transport);
31
- }
32
- return provider;
12
+ const lockMethods = {
13
+ lock: (resource, params) => locks ? locks.lock(resource, params) : noLockProvider("lock"),
14
+ unlock: (resource, params) => locks ? locks.unlock(resource, params) : noLockProvider("unlock"),
15
+ getLock: (resource, params) => locks ? locks.getLock(resource, params) : noLockProvider("getLock"),
16
+ getLocks: (resource, params) => locks ? locks.getLocks(resource, params) : noLockProvider("getLocks")
17
+ };
18
+ const provider = {
19
+ ...copyMembers(base),
20
+ ...lockMethods,
21
+ subscribe: (topic, callback) => transport.subscribe(topic, callback),
22
+ publish: (topic, event) => transport.publish(topic, event)
23
+ };
24
+ if (transport.onReconnect) provider.onReconnect = transport.onReconnect.bind(transport);
25
+ if (transport.onStatusChange) provider.onStatusChange = transport.onStatusChange.bind(transport);
26
+ if (transport.getStatus) provider.getStatus = transport.getStatus.bind(transport);
27
+ if (transport.onError) provider.onError = transport.onError.bind(transport);
28
+ return provider;
33
29
  }
30
+ /** True when `dataProvider` was wrapped with `realtimeDataProvider()`. */
34
31
  function isRealtimeDataProvider(dataProvider) {
35
- const candidate = dataProvider;
36
- return typeof candidate.subscribe === "function" && typeof candidate.publish === "function";
32
+ const candidate = dataProvider;
33
+ return typeof candidate.subscribe === "function" && typeof candidate.publish === "function";
34
+ }
35
+ var transportMembers = [
36
+ "onReconnect",
37
+ "onStatusChange",
38
+ "getStatus",
39
+ "onError"
40
+ ];
41
+ /**
42
+ * True when every transport member `dataProvider` has is a function. Absent
43
+ * members are fine: they are optional, and a provider with only some of them
44
+ * (no `publish`, say) still reports reconnects and status.
45
+ */
46
+ function hasTransportMembers(dataProvider) {
47
+ return transportMembers.every((name) => !(name in dataProvider) || typeof Reflect.get(dataProvider, name) === "function");
37
48
  }
38
49
  function assertRealtimeDataProvider(dataProvider, caller) {
39
- if (!isRealtimeDataProvider(dataProvider)) {
40
- throw new Error(
41
- `${caller}: dataProvider does not implement subscribe(). Wrap it with realtimeDataProvider().`
42
- );
43
- }
50
+ if (!isRealtimeDataProvider(dataProvider)) throw new Error(`${caller}: dataProvider does not implement subscribe(). Wrap it with realtimeDataProvider().`);
44
51
  }
45
- export {
46
- assertRealtimeDataProvider,
47
- isRealtimeDataProvider,
48
- realtimeDataProvider
49
- };
52
+ export { assertRealtimeDataProvider, hasTransportMembers, isRealtimeDataProvider, realtimeDataProvider };
@@ -0,0 +1,15 @@
1
+ export interface RealtimeReconnectConfig {
2
+ /** Whether a dropped connection is retried. Default `true`. */
3
+ enabled?: boolean;
4
+ /** The wait before the first retry. Default 1000. */
5
+ initialDelayMs?: number;
6
+ /** The wait never grows past this. Default 30000. */
7
+ maxDelayMs?: number;
8
+ /** Retries before giving up. Default `Infinity`. */
9
+ maxAttempts?: number;
10
+ /** Share of each wait that is randomised, up or down. Default 0.3. */
11
+ jitter?: number;
12
+ }
13
+ export declare function resolveReconnectConfig(config?: RealtimeReconnectConfig): Required<RealtimeReconnectConfig>;
14
+ /** The wait before retry number `attempt` (from 0): exponential, capped, then jittered. */
15
+ export declare function reconnectDelay(attempt: number, config: Required<RealtimeReconnectConfig>, random?: () => number): number;
@@ -0,0 +1,17 @@
1
+ // src/realtime/reconnect-backoff.ts
2
+ function resolveReconnectConfig(config = {}) {
3
+ return {
4
+ enabled: config.enabled ?? true,
5
+ initialDelayMs: config.initialDelayMs ?? 1e3,
6
+ maxDelayMs: config.maxDelayMs ?? 3e4,
7
+ maxAttempts: config.maxAttempts ?? Number.POSITIVE_INFINITY,
8
+ jitter: config.jitter ?? .3
9
+ };
10
+ }
11
+ /** The wait before retry number `attempt` (from 0): exponential, capped, then jittered. */
12
+ function reconnectDelay(attempt, config, random = Math.random) {
13
+ const base = Math.min(config.initialDelayMs * 2 ** attempt, config.maxDelayMs);
14
+ const spread = base * config.jitter;
15
+ return Math.max(0, base + (random() * 2 - 1) * spread);
16
+ }
17
+ export { reconnectDelay, resolveReconnectConfig };
@@ -0,0 +1,31 @@
1
+ import { type RealtimeReconnectConfig } from "./reconnect-backoff.js";
2
+ import type { RealtimeTransport, RealtimeTransportError } from "./types.js";
3
+ export interface SseTransportOptions {
4
+ /** The stream URL; the topic filter and the token are appended to its query. */
5
+ url: string;
6
+ /** Where `publish` POSTs. Without it `publish` rejects. */
7
+ publishUrl?: string;
8
+ /** The bearer token for the stream (as `token`) and for the publish POST. */
9
+ getAuthToken?: () => string | null | undefined | Promise<string | null | undefined>;
10
+ /** Sent to `EventSource` and as `credentials: "include"` on the publish POST. */
11
+ withCredentials?: boolean;
12
+ reconnect?: RealtimeReconnectConfig;
13
+ /** How long the stream stays open with no subscriber. Default 30000. */
14
+ idleDisconnectMs?: number;
15
+ /** The query parameter that carries the topics. Default `"topics"`. */
16
+ topicFilterParam?: string;
17
+ /** Called for every transport error, like a listener from `onError()`. */
18
+ onError?: (error: RealtimeTransportError) => void;
19
+ /** Used for the publish POST. Default the global `fetch`. */
20
+ fetch?: typeof fetch;
21
+ }
22
+ /** A `RealtimeTransport` with every optional member, plus `close()`. */
23
+ export interface SseTransport extends Required<RealtimeTransport> {
24
+ /** Closes the stream and sets `"disconnected"`; a later subscribe opens again. */
25
+ close(): void;
26
+ }
27
+ /**
28
+ * Receives realtime events over server-sent events, one named event per topic,
29
+ * and publishes with a POST.
30
+ */
31
+ export declare function sseTransport({ url, publishUrl, getAuthToken, withCredentials, reconnect, idleDisconnectMs, topicFilterParam, onError, fetch: fetchOption, }: SseTransportOptions): SseTransport;
@@ -0,0 +1,235 @@
1
+ import { isRecord } from "../guards.js";
2
+ import { reconnectDelay, resolveReconnectConfig } from "./reconnect-backoff.js";
3
+ import { createTopicHub } from "./topic-hub.js";
4
+ // src/realtime/sse-transport.ts
5
+ function isWireEvent(value) {
6
+ return isRecord(value) && typeof value.type === "string" && (value.meta === void 0 || isRecord(value.meta));
7
+ }
8
+ var DEFAULT_IDLE_DISCONNECT_MS = 3e4;
9
+ /**
10
+ * Receives realtime events over server-sent events, one named event per topic,
11
+ * and publishes with a POST.
12
+ */
13
+ function sseTransport({ url, publishUrl, getAuthToken, withCredentials = false, reconnect, idleDisconnectMs = DEFAULT_IDLE_DISCONNECT_MS, topicFilterParam = "topics", onError, fetch: fetchOption }) {
14
+ const hub = createTopicHub({ onError });
15
+ hub.setStatus("idle");
16
+ const config = resolveReconnectConfig(reconnect);
17
+ let source = null;
18
+ let filter = /* @__PURE__ */ new Set();
19
+ let active = false;
20
+ let generation = 0;
21
+ let attempt = 0;
22
+ let dropped = false;
23
+ let openQueued = false;
24
+ let resolvingToken = false;
25
+ let retryTimer;
26
+ let idleTimer;
27
+ const readToken = async () => await getAuthToken?.() || void 0;
28
+ const stopStream = () => {
29
+ clearTimeout(retryTimer);
30
+ retryTimer = void 0;
31
+ source?.close();
32
+ source = null;
33
+ filter = /* @__PURE__ */ new Set();
34
+ };
35
+ const streamUrl = (topics, token) => {
36
+ const query = [`${encodeURIComponent(topicFilterParam)}=${encodeURIComponent(topics.join(","))}`, ...token === void 0 ? [] : [`token=${encodeURIComponent(token)}`]].join("&");
37
+ return `${url}${url.includes("?") ? "&" : "?"}${query}`;
38
+ };
39
+ const retryOrGiveUp = (failure) => {
40
+ const retrying = config.enabled && attempt < config.maxAttempts;
41
+ hub.report(failure(retrying));
42
+ if (!retrying) {
43
+ active = false;
44
+ hub.setStatus("disconnected");
45
+ return;
46
+ }
47
+ hub.setStatus("reconnecting");
48
+ retryTimer = setTimeout(() => {
49
+ retryTimer = void 0;
50
+ open(true);
51
+ }, reconnectDelay(attempt, config));
52
+ attempt += 1;
53
+ };
54
+ const handleError = (stream, opened) => {
55
+ if (source !== stream) return;
56
+ stopStream();
57
+ dropped ||= opened;
58
+ retryOrGiveUp((retrying) => ({
59
+ kind: "connect_failed",
60
+ retrying
61
+ }));
62
+ };
63
+ async function open(isRetry = false) {
64
+ stopStream();
65
+ const current = ++generation;
66
+ active = true;
67
+ resolvingToken = true;
68
+ let token;
69
+ try {
70
+ token = await readToken();
71
+ } catch (cause) {
72
+ if (current !== generation) return;
73
+ resolvingToken = false;
74
+ if (isRetry) {
75
+ retryOrGiveUp((retrying) => ({
76
+ kind: "auth_failed",
77
+ cause,
78
+ retrying
79
+ }));
80
+ return;
81
+ }
82
+ active = false;
83
+ hub.report({
84
+ kind: "auth_failed",
85
+ cause,
86
+ retrying: false
87
+ });
88
+ hub.setStatus("disconnected");
89
+ return;
90
+ }
91
+ if (current !== generation) return;
92
+ resolvingToken = false;
93
+ const topics = hub.topics();
94
+ if (topics.length === 0) {
95
+ goIdle();
96
+ return;
97
+ }
98
+ let stream;
99
+ try {
100
+ stream = new EventSource(streamUrl(topics, token), { withCredentials });
101
+ } catch {
102
+ active = false;
103
+ hub.report({
104
+ kind: "connect_failed",
105
+ retrying: false
106
+ });
107
+ hub.setStatus("disconnected");
108
+ return;
109
+ }
110
+ source = stream;
111
+ filter = new Set(topics);
112
+ let opened = false;
113
+ stream.addEventListener("open", () => {
114
+ if (source !== stream) return;
115
+ opened = true;
116
+ attempt = 0;
117
+ hub.setStatus("connected");
118
+ if (dropped) {
119
+ dropped = false;
120
+ hub.reconnected();
121
+ }
122
+ });
123
+ stream.addEventListener("error", () => handleError(stream, opened));
124
+ for (const topic of topics) stream.addEventListener(topic, (message) => {
125
+ if (source !== stream || !("data" in message)) return;
126
+ if (typeof message.data === "string") receive(topic, message.data);
127
+ });
128
+ }
129
+ const receive = (topic, data) => {
130
+ let parsed;
131
+ try {
132
+ parsed = JSON.parse(data);
133
+ } catch (cause) {
134
+ hub.report({
135
+ kind: "parse_failed",
136
+ topic,
137
+ cause,
138
+ retrying: false
139
+ });
140
+ return;
141
+ }
142
+ if (isWireEvent(parsed)) hub.dispatch({
143
+ ...parsed,
144
+ topic
145
+ });
146
+ };
147
+ const queueOpen = () => {
148
+ if (openQueued) return;
149
+ openQueued = true;
150
+ Promise.resolve().then(() => {
151
+ if (!openQueued) return;
152
+ openQueued = false;
153
+ if (!active) attempt = 0;
154
+ return open();
155
+ });
156
+ };
157
+ function goIdle() {
158
+ generation += 1;
159
+ resolvingToken = false;
160
+ clearTimeout(idleTimer);
161
+ idleTimer = void 0;
162
+ stopStream();
163
+ active = false;
164
+ dropped = false;
165
+ if (hub.getStatus() !== "idle") hub.setStatus("idle");
166
+ }
167
+ return {
168
+ subscribe(topic, callback) {
169
+ const stop = hub.subscribe(topic, callback);
170
+ clearTimeout(idleTimer);
171
+ idleTimer = void 0;
172
+ const covered = retryTimer !== void 0 || resolvingToken || filter.has(topic);
173
+ if (!active || !covered) queueOpen();
174
+ return () => {
175
+ stop();
176
+ if (hub.topics().length > 0 || !active) return;
177
+ clearTimeout(idleTimer);
178
+ idleTimer = setTimeout(goIdle, idleDisconnectMs);
179
+ };
180
+ },
181
+ async publish(topic, event) {
182
+ if (!publishUrl) throw new Error("sseTransport needs the publishUrl option to publish");
183
+ const headers = { "Content-Type": "application/json" };
184
+ try {
185
+ const token = await readToken();
186
+ if (token !== void 0) headers.Authorization = `Bearer ${token}`;
187
+ } catch (cause) {
188
+ hub.report({
189
+ kind: "auth_failed",
190
+ topic,
191
+ cause,
192
+ retrying: false
193
+ });
194
+ throw cause;
195
+ }
196
+ const doFetch = fetchOption ?? ((input, init) => globalThis.fetch(input, init));
197
+ try {
198
+ const response = await doFetch(publishUrl, {
199
+ method: "POST",
200
+ headers,
201
+ body: JSON.stringify({
202
+ topic,
203
+ event
204
+ }),
205
+ credentials: withCredentials ? "include" : void 0
206
+ });
207
+ if (!response.ok) throw new Error(`Publishing to ${topic} failed: ${response.status}`);
208
+ } catch (cause) {
209
+ hub.report({
210
+ kind: "send_failed",
211
+ topic,
212
+ cause,
213
+ retrying: false
214
+ });
215
+ throw cause;
216
+ }
217
+ },
218
+ onReconnect: hub.onReconnect,
219
+ onStatusChange: hub.onStatusChange,
220
+ onError: hub.onError,
221
+ getStatus: hub.getStatus,
222
+ close() {
223
+ openQueued = false;
224
+ generation += 1;
225
+ resolvingToken = false;
226
+ clearTimeout(idleTimer);
227
+ idleTimer = void 0;
228
+ stopStream();
229
+ active = false;
230
+ dropped = false;
231
+ hub.setStatus("disconnected");
232
+ }
233
+ };
234
+ }
235
+ export { sseTransport };