@chelone/core 0.6.0 → 0.9.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 (255) hide show
  1. package/dist/assistant/assistant-session.d.ts +6 -0
  2. package/dist/assistant/assistant-session.js +204 -181
  3. package/dist/assistant/echo-transport.js +26 -29
  4. package/dist/assistant/http-transport.d.ts +10 -0
  5. package/dist/assistant/http-transport.js +98 -0
  6. package/dist/assistant/types.d.ts +1 -7
  7. package/dist/auth/access-control.d.ts +13 -5
  8. package/dist/auth/access-control.js +25 -21
  9. package/dist/auth/auth-options.d.ts +23 -17
  10. package/dist/auth/auth-options.js +82 -48
  11. package/dist/auth/permission-matrix.d.ts +12 -0
  12. package/dist/auth/permission-matrix.js +67 -0
  13. package/dist/auth/types.d.ts +14 -4
  14. package/dist/block-editor/block-document.d.ts +64 -0
  15. package/dist/block-editor/block-document.js +147 -0
  16. package/dist/block-editor/data-blocks.d.ts +63 -0
  17. package/dist/block-editor/data-blocks.js +117 -0
  18. package/dist/chart/chart-data.js +46 -44
  19. package/dist/command-menu/hotkey.js +58 -47
  20. package/dist/command-menu/recents.d.ts +4 -0
  21. package/dist/command-menu/recents.js +12 -11
  22. package/dist/command-menu/registry.d.ts +9 -5
  23. package/dist/command-menu/registry.js +36 -46
  24. package/dist/controller/calendar-dates.d.ts +12 -0
  25. package/dist/controller/calendar-dates.js +94 -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 +43 -0
  35. package/dist/controller/in-place-editor.js +43 -0
  36. package/dist/controller/infinite-list-controller.js +219 -189
  37. package/dist/controller/inspector.d.ts +55 -0
  38. package/dist/controller/inspector.js +70 -0
  39. package/dist/controller/job-monitor.d.ts +10 -4
  40. package/dist/controller/job-monitor.js +46 -47
  41. package/dist/controller/kanban-board.js +45 -24
  42. package/dist/controller/list-controller.d.ts +8 -5
  43. package/dist/controller/list-controller.js +447 -400
  44. package/dist/controller/list-params.js +96 -87
  45. package/dist/controller/pivot-grid.d.ts +4 -4
  46. package/dist/controller/pivot-grid.js +89 -95
  47. package/dist/controller/prev-next.d.ts +2 -2
  48. package/dist/controller/prev-next.js +74 -54
  49. package/dist/controller/record-controllers.d.ts +14 -6
  50. package/dist/controller/record-controllers.js +288 -257
  51. package/dist/controller/record-diff.d.ts +21 -0
  52. package/dist/controller/record-diff.js +50 -0
  53. package/dist/controller/record-timeline.d.ts +5 -5
  54. package/dist/controller/record-timeline.js +42 -30
  55. package/dist/controller/record-tree.js +57 -47
  56. package/dist/controller/record-workflow.d.ts +38 -16
  57. package/dist/controller/record-workflow.js +85 -64
  58. package/dist/controller/reference-many-controller.d.ts +2 -0
  59. package/dist/controller/reference-many-controller.js +199 -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 +4 -3
  65. package/dist/csv/export.js +43 -40
  66. package/dist/csv/import-controller.d.ts +4 -0
  67. package/dist/csv/import-controller.js +189 -169
  68. package/dist/csv/match-headers.js +29 -22
  69. package/dist/csv/parse-with.js +60 -54
  70. package/dist/csv/run-import.js +69 -72
  71. package/dist/csv/validate-rows.d.ts +1 -1
  72. package/dist/csv/validate-rows.js +65 -52
  73. package/dist/data/cache-ops.d.ts +5 -4
  74. package/dist/data/cache-ops.js +208 -188
  75. package/dist/data/combine-data-providers.js +25 -23
  76. package/dist/data/data-provider-log.d.ts +45 -0
  77. package/dist/data/data-provider-log.js +93 -0
  78. package/dist/data/errors.js +14 -10
  79. package/dist/data/fake-provider.d.ts +6 -3
  80. package/dist/data/fake-provider.js +135 -114
  81. package/dist/data/fetch-utils.js +61 -44
  82. package/dist/data/filter-branch.d.ts +5 -0
  83. package/dist/data/filter-branch.js +10 -0
  84. package/dist/data/get-many-aggregate.js +84 -76
  85. package/dist/data/http-error.js +17 -12
  86. package/dist/data/keys.js +50 -36
  87. package/dist/data/mutation-options.d.ts +14 -9
  88. package/dist/data/mutation-options.js +93 -109
  89. package/dist/data/query-options.js +73 -68
  90. package/dist/data/remove-empty.d.ts +3 -0
  91. package/dist/data/remove-empty.js +11 -0
  92. package/dist/data/undoable.d.ts +2 -2
  93. package/dist/data/undoable.js +190 -174
  94. package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
  95. package/dist/data/with-lifecycle-callbacks.js +66 -52
  96. package/dist/environment/application-update-store.js +58 -55
  97. package/dist/environment/offline-store.js +21 -21
  98. package/dist/form/array-binding.d.ts +30 -11
  99. package/dist/form/array-binding.js +184 -166
  100. package/dist/form/choice-ids.d.ts +7 -0
  101. package/dist/form/choice-ids.js +11 -0
  102. package/dist/form/choices-controller.d.ts +22 -2
  103. package/dist/form/choices-controller.js +315 -275
  104. package/dist/form/date-format.d.ts +38 -0
  105. package/dist/form/date-format.js +89 -0
  106. package/dist/form/dirty-values.d.ts +2 -2
  107. package/dist/form/dirty-values.js +22 -11
  108. package/dist/form/duration.d.ts +22 -0
  109. package/dist/form/duration.js +49 -0
  110. package/dist/form/edit-create-form.d.ts +12 -5
  111. package/dist/form/edit-create-form.js +243 -188
  112. package/dist/form/event-value.d.ts +6 -0
  113. package/dist/form/event-value.js +17 -0
  114. package/dist/form/field-error.d.ts +14 -0
  115. package/dist/form/field-error.js +28 -0
  116. package/dist/form/field-name.d.ts +20 -0
  117. package/dist/form/field-name.js +6 -0
  118. package/dist/form/filter-form-values.js +63 -49
  119. package/dist/form/form-controller.d.ts +9 -1
  120. package/dist/form/form-controller.js +165 -160
  121. package/dist/form/form-groups.d.ts +4 -3
  122. package/dist/form/form-groups.js +79 -79
  123. package/dist/form/form-handle.d.ts +175 -0
  124. package/dist/form/form-handle.js +68 -0
  125. package/dist/form/input-binding.d.ts +56 -31
  126. package/dist/form/input-binding.js +171 -118
  127. package/dist/form/input-values.d.ts +140 -0
  128. package/dist/form/input-values.js +206 -0
  129. package/dist/form/json-text.d.ts +11 -0
  130. package/dist/form/json-text.js +62 -37
  131. package/dist/form/save-pipeline.d.ts +6 -8
  132. package/dist/form/save-pipeline.js +91 -82
  133. package/dist/form/set-form-values.d.ts +6 -0
  134. package/dist/form/set-form-values.js +10 -0
  135. package/dist/form/source.js +97 -65
  136. package/dist/form/text-of.d.ts +2 -0
  137. package/dist/form/text-of.js +7 -0
  138. package/dist/form/validators.d.ts +58 -19
  139. package/dist/form/validators.js +97 -71
  140. package/dist/geo/bbox.d.ts +5 -4
  141. package/dist/geo/bbox.js +69 -48
  142. package/dist/geo/bounds.d.ts +14 -3
  143. package/dist/geo/bounds.js +98 -68
  144. package/dist/geo/constraints.js +88 -93
  145. package/dist/geo/coordinates.d.ts +10 -0
  146. package/dist/geo/coordinates.js +44 -23
  147. package/dist/geo/editor.d.ts +13 -13
  148. package/dist/geo/editor.js +176 -153
  149. package/dist/geo/geocoding.d.ts +4 -4
  150. package/dist/geo/geocoding.js +38 -41
  151. package/dist/geo/geojson.d.ts +12 -74
  152. package/dist/geo/geojson.js +52 -40
  153. package/dist/geo/geometry-math.d.ts +4 -4
  154. package/dist/geo/geometry-math.js +100 -93
  155. package/dist/geo/index.d.ts +9 -8
  156. package/dist/geo/map-layers.d.ts +42 -7
  157. package/dist/geo/map-layers.js +88 -62
  158. package/dist/geo/map-style.d.ts +12 -0
  159. package/dist/geo/map-style.js +43 -32
  160. package/dist/geo/messages.js +48 -45
  161. package/dist/geo/nominatim.js +88 -80
  162. package/dist/geo/osm-presets.d.ts +4 -3
  163. package/dist/geo/osm-presets.js +108 -86
  164. package/dist/geo/osm-tags.js +14 -12
  165. package/dist/geo/overpass.d.ts +14 -2
  166. package/dist/geo/overpass.js +83 -90
  167. package/dist/geo/shape-value.d.ts +9 -5
  168. package/dist/geo/shape-value.js +169 -146
  169. package/dist/geo/snap-to-roads.d.ts +3 -3
  170. package/dist/geo/snap-to-roads.js +55 -50
  171. package/dist/geo/throttle.js +36 -39
  172. package/dist/guards.d.ts +44 -0
  173. package/dist/guards.js +77 -0
  174. package/dist/i18n/default-messages.js +34 -29
  175. package/dist/i18n/english-messages.d.ts +172 -0
  176. package/dist/i18n/english-messages.js +359 -189
  177. package/dist/i18n/i18n.d.ts +2 -2
  178. package/dist/i18n/i18n.js +47 -60
  179. package/dist/i18n/inflection.d.ts +34 -0
  180. package/dist/i18n/inflection.js +82 -0
  181. package/dist/i18n/messages.d.ts +10 -2
  182. package/dist/i18n/messages.js +17 -4
  183. package/dist/i18n/missing-keys.d.ts +4 -0
  184. package/dist/i18n/missing-keys.js +70 -62
  185. package/dist/i18n/translatable.d.ts +16 -1
  186. package/dist/i18n/translatable.js +40 -47
  187. package/dist/index.d.ts +87 -39
  188. package/dist/index.js +54 -301
  189. package/dist/json/deep-equal.d.ts +8 -0
  190. package/dist/json/deep-equal.js +24 -0
  191. package/dist/json/strip-json-comments.js +38 -42
  192. package/dist/noop.d.ts +2 -0
  193. package/dist/noop.js +4 -0
  194. package/dist/notification/error-message.d.ts +2 -0
  195. package/dist/notification/error-message.js +10 -0
  196. package/dist/notification/notification-store.d.ts +3 -2
  197. package/dist/notification/notification-store.js +41 -45
  198. package/dist/onboarding-tour/placement.d.ts +2 -0
  199. package/dist/onboarding-tour/placement.js +65 -78
  200. package/dist/onboarding-tour/tour-controller.js +46 -54
  201. package/dist/realtime/add-events-for-mutations.js +75 -81
  202. package/dist/realtime/announce-lock.js +16 -13
  203. package/dist/realtime/broadcast-channel-transport.js +65 -59
  204. package/dist/realtime/copy-members.js +22 -14
  205. package/dist/realtime/in-memory-lock-provider.js +40 -48
  206. package/dist/realtime/live.js +95 -101
  207. package/dist/realtime/lock-options.js +16 -16
  208. package/dist/realtime/lock-session.js +91 -78
  209. package/dist/realtime/memory-transport.js +25 -25
  210. package/dist/realtime/presence.js +120 -122
  211. package/dist/realtime/realtime-data-provider.d.ts +10 -0
  212. package/dist/realtime/realtime-data-provider.js +45 -42
  213. package/dist/realtime/reconnect-backoff.js +17 -0
  214. package/dist/realtime/sse-transport.d.ts +31 -0
  215. package/dist/realtime/sse-transport.js +235 -0
  216. package/dist/realtime/status-store.js +23 -28
  217. package/dist/realtime/topic-hub.js +62 -71
  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 +247 -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 +23 -2
  236. package/dist/rich-text/rich-text-extras.d.ts +134 -0
  237. package/dist/rich-text/rich-text-extras.js +181 -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 +6 -0
  241. package/dist/rich-text/rich-text.js +125 -88
  242. package/dist/router/location.d.ts +24 -4
  243. package/dist/router/location.js +43 -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/dist/ui-defaults.d.ts +15 -0
  254. package/dist/ui-defaults.js +17 -0
  255. package/package.json +13 -11
@@ -1,82 +1,95 @@
1
1
  import { createMicroStore } from "../store/micro-store.js";
2
- const toError = (error) => error instanceof Error ? error : new Error(String(error));
3
- function createLockSession({
4
- dataProvider,
5
- resource,
6
- id,
7
- getIdentity,
8
- meta,
9
- onError
10
- }) {
11
- const store = createMicroStore({
12
- lock: null,
13
- isLocking: false,
14
- error: null
15
- });
16
- const metaParam = meta ? { meta } : {};
17
- let held = null;
18
- let running = false;
19
- let epoch = 0;
20
- let queue = Promise.resolve();
21
- const enqueue = (step) => {
22
- queue = queue.then(step).catch(() => {
23
- });
24
- return queue;
25
- };
26
- const acquire = async (myEpoch) => {
27
- const isCurrent = () => epoch === myEpoch;
28
- try {
29
- const identity = await getIdentity();
30
- const lock = await dataProvider.lock(resource, {
31
- id,
32
- identity,
33
- ...metaParam
34
- });
35
- held = { identity };
36
- if (isCurrent()) store.setState({ lock, isLocking: false });
37
- } catch (cause) {
38
- if (!isCurrent()) return;
39
- const error = toError(cause);
40
- store.setState({ isLocking: false, error });
41
- onError?.(error);
42
- }
43
- };
44
- const release = async () => {
45
- if (!held) return;
46
- const { identity } = held;
47
- held = null;
48
- try {
49
- await dataProvider.unlock(resource, { id, identity, ...metaParam });
50
- } catch (cause) {
51
- onError?.(toError(cause));
52
- }
53
- };
54
- return {
55
- store,
56
- start() {
57
- if (running) return queue;
58
- running = true;
59
- const myEpoch = ++epoch;
60
- store.setState({ isLocking: true, error: null });
61
- return enqueue(() => acquire(myEpoch));
62
- },
63
- stop() {
64
- if (!running) return queue;
65
- running = false;
66
- epoch++;
67
- store.setState({ lock: null, isLocking: false, error: null });
68
- return enqueue(release);
69
- }
70
- };
2
+ // src/realtime/lock-session.ts
3
+ var toError = (error) => error instanceof Error ? error : new Error(String(error));
4
+ /**
5
+ * Takes a record's lock on `start()` and gives it back on `stop()`. Steps run
6
+ * one after another, so a `stop()` that arrives while `lock` is in flight
7
+ * releases the lock once it lands, and `start()`, `stop()`, `start()` in one
8
+ * tick ends holding it. Locks carry no expiry: a crashed tab leaves its lock
9
+ * until the lock provider drops it.
10
+ */
11
+ function createLockSession({ dataProvider, resource, id, getIdentity, meta, onError }) {
12
+ const store = createMicroStore({
13
+ lock: null,
14
+ isLocking: false,
15
+ error: null
16
+ });
17
+ const metaParam = meta ? { meta } : {};
18
+ let held = null;
19
+ let running = false;
20
+ let epoch = 0;
21
+ let queue = Promise.resolve();
22
+ const enqueue = (step) => {
23
+ queue = queue.then(step).catch(() => {});
24
+ return queue;
25
+ };
26
+ const acquire = async (myEpoch) => {
27
+ const isCurrent = () => epoch === myEpoch;
28
+ try {
29
+ const identity = await getIdentity();
30
+ const lock = await dataProvider.lock(resource, {
31
+ id,
32
+ identity,
33
+ ...metaParam
34
+ });
35
+ held = { identity };
36
+ if (isCurrent()) store.setState({
37
+ lock,
38
+ isLocking: false
39
+ });
40
+ } catch (cause) {
41
+ if (!isCurrent()) return;
42
+ const error = toError(cause);
43
+ store.setState({
44
+ isLocking: false,
45
+ error
46
+ });
47
+ onError?.(error);
48
+ }
49
+ };
50
+ const release = async () => {
51
+ if (!held) return;
52
+ const { identity } = held;
53
+ held = null;
54
+ try {
55
+ await dataProvider.unlock(resource, {
56
+ id,
57
+ identity,
58
+ ...metaParam
59
+ });
60
+ } catch (cause) {
61
+ onError?.(toError(cause));
62
+ }
63
+ };
64
+ return {
65
+ store,
66
+ start() {
67
+ if (running) return queue;
68
+ running = true;
69
+ const myEpoch = ++epoch;
70
+ store.setState({
71
+ isLocking: true,
72
+ error: null
73
+ });
74
+ return enqueue(() => acquire(myEpoch));
75
+ },
76
+ stop() {
77
+ if (!running) return queue;
78
+ running = false;
79
+ epoch++;
80
+ store.setState({
81
+ lock: null,
82
+ isLocking: false,
83
+ error: null
84
+ });
85
+ return enqueue(release);
86
+ }
87
+ };
71
88
  }
89
+ /** The explicit identity wins; otherwise the auth provider's. */
72
90
  async function resolveLockIdentity(authProvider, explicit) {
73
- if (explicit !== void 0) return explicit;
74
- if (authProvider?.getIdentity) return (await authProvider.getIdentity()).id;
75
- throw new Error(
76
- "no identity available. Pass identity explicitly or configure an authProvider."
77
- );
91
+ if (explicit !== void 0) return explicit;
92
+ if (authProvider?.getIdentity) return (await authProvider.getIdentity()).id;
93
+ throw new Error("no identity available. Pass identity explicitly or configure an authProvider.");
78
94
  }
79
- export {
80
- createLockSession,
81
- resolveLockIdentity
82
- };
95
+ export { createLockSession, resolveLockIdentity };
@@ -1,27 +1,27 @@
1
1
  import { createTopicHub } from "./topic-hub.js";
2
- function memoryTransport({
3
- delayMs,
4
- onError
5
- } = {}) {
6
- const hub = createTopicHub({ onError });
7
- const publishedEvents = [];
8
- return {
9
- publishedEvents,
10
- subscribe: hub.subscribe,
11
- async publish(topic, event) {
12
- const full = { topic, ...event };
13
- publishedEvents.push(full);
14
- if (delayMs) await new Promise((resolve) => setTimeout(resolve, delayMs));
15
- hub.dispatch(full);
16
- },
17
- onReconnect: hub.onReconnect,
18
- onStatusChange: hub.onStatusChange,
19
- onError: hub.onError,
20
- getStatus: hub.getStatus,
21
- simulateReconnect: hub.reconnected,
22
- setStatus: hub.setStatus
23
- };
2
+ // src/realtime/memory-transport.ts
3
+ /** Routes events by topic inside one process, with no server or client library. */
4
+ function memoryTransport({ delayMs, onError } = {}) {
5
+ const hub = createTopicHub({ onError });
6
+ const publishedEvents = [];
7
+ return {
8
+ publishedEvents,
9
+ subscribe: hub.subscribe,
10
+ async publish(topic, event) {
11
+ const full = {
12
+ topic,
13
+ ...event
14
+ };
15
+ publishedEvents.push(full);
16
+ if (delayMs) await new Promise((resolve) => setTimeout(resolve, delayMs));
17
+ hub.dispatch(full);
18
+ },
19
+ onReconnect: hub.onReconnect,
20
+ onStatusChange: hub.onStatusChange,
21
+ onError: hub.onError,
22
+ getStatus: hub.getStatus,
23
+ simulateReconnect: hub.reconnected,
24
+ setStatus: hub.setStatus
25
+ };
24
26
  }
25
- export {
26
- memoryTransport
27
- };
27
+ export { memoryTransport };
@@ -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,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;