@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,33 +1,28 @@
1
1
  import { createMicroStore } from "../store/micro-store.js";
2
+ // src/realtime/status-store.ts
2
3
  function readStatus(source) {
3
- if (source.getStatus) return source.getStatus();
4
- return source.onStatusChange ? "disconnected" : "idle";
4
+ if (source.getStatus) return source.getStatus();
5
+ return source.onStatusChange ? "disconnected" : "idle";
5
6
  }
7
+ /** Follows the connection while it has a subscriber, and lets go of it at zero. */
6
8
  function createRealtimeStatusStore(source) {
7
- const store = createMicroStore(
8
- { status: readStatus(source), lastError: null },
9
- {
10
- onSubscribed() {
11
- const status = readStatus(source);
12
- store.setState(
13
- status === "connected" ? { status, lastError: null } : { status }
14
- );
15
- const stops = [
16
- source.onStatusChange?.(
17
- (status2) => store.setState(
18
- status2 === "connected" ? { status: status2, lastError: null } : { status: status2 }
19
- )
20
- ),
21
- source.onError?.((lastError) => store.setState({ lastError }))
22
- ];
23
- return () => {
24
- for (const stop of stops) stop?.();
25
- };
26
- }
27
- }
28
- );
29
- return store;
9
+ const store = createMicroStore({
10
+ status: readStatus(source),
11
+ lastError: null
12
+ }, { onSubscribed() {
13
+ const status = readStatus(source);
14
+ store.setState(status === "connected" ? {
15
+ status,
16
+ lastError: null
17
+ } : { status });
18
+ const stops = [source.onStatusChange?.((status) => store.setState(status === "connected" ? {
19
+ status,
20
+ lastError: null
21
+ } : { status })), source.onError?.((lastError) => store.setState({ lastError }))];
22
+ return () => {
23
+ for (const stop of stops) stop?.();
24
+ };
25
+ } });
26
+ return store;
30
27
  }
31
- export {
32
- createRealtimeStatusStore
33
- };
28
+ export { createRealtimeStatusStore };
@@ -0,0 +1,24 @@
1
+ import type { RealtimeConnectionStatus, RealtimeEvent, RealtimeTransport, RealtimeTransportError } from "./types.js";
2
+ export interface TopicHubOptions {
3
+ /** Called for every reported error, like a listener from `onError()`. */
4
+ onError?: (error: RealtimeTransportError) => void;
5
+ }
6
+ /**
7
+ * The subscriber bookkeeping every transport needs: per-topic subscriptions,
8
+ * isolated dispatch, and the status, reconnect and error listeners.
9
+ */
10
+ export interface TopicHub extends Required<Pick<RealtimeTransport, "subscribe" | "onReconnect" | "onStatusChange" | "getStatus" | "onError">> {
11
+ /** Calls the subscribers of `event.topic`; a throwing one is reported. */
12
+ dispatch(event: RealtimeEvent): void;
13
+ /** Whether any subscriber is listening on `topic`. */
14
+ has(topic: string): boolean;
15
+ /** Every topic with a subscriber, in the order each was first subscribed. */
16
+ topics(): string[];
17
+ /** Calls the `onError` option and every `onError` listener. */
18
+ report(error: RealtimeTransportError): void;
19
+ setStatus(status: RealtimeConnectionStatus): void;
20
+ /** Calls every `onReconnect` listener. */
21
+ reconnected(): void;
22
+ }
23
+ /** Starts `"connected"`; a transport that connects asynchronously sets its own status. */
24
+ export declare function createTopicHub({ onError }?: TopicHubOptions): TopicHub;
@@ -0,0 +1,66 @@
1
+ // src/realtime/topic-hub.ts
2
+ var addListener = (listeners, listener) => {
3
+ listeners.add(listener);
4
+ return () => {
5
+ listeners.delete(listener);
6
+ };
7
+ };
8
+ var isolate = (call) => {
9
+ try {
10
+ call();
11
+ } catch {}
12
+ };
13
+ /** Starts `"connected"`; a transport that connects asynchronously sets its own status. */
14
+ function createTopicHub({ onError } = {}) {
15
+ const subscribers = /* @__PURE__ */ new Map();
16
+ const reconnectListeners = /* @__PURE__ */ new Set();
17
+ const statusListeners = /* @__PURE__ */ new Set();
18
+ const errorListeners = /* @__PURE__ */ new Set();
19
+ let status = "connected";
20
+ const report = (error) => {
21
+ for (const listener of [...onError ? [onError] : [], ...errorListeners]) isolate(() => listener(error));
22
+ };
23
+ return {
24
+ subscribe(topic, callback) {
25
+ const subscription = { callback };
26
+ const set = subscribers.get(topic) ?? /* @__PURE__ */ new Set();
27
+ set.add(subscription);
28
+ subscribers.set(topic, set);
29
+ return () => {
30
+ set.delete(subscription);
31
+ if (set.size === 0 && subscribers.get(topic) === set) subscribers.delete(topic);
32
+ };
33
+ },
34
+ dispatch(event) {
35
+ const live = subscribers.get(event.topic);
36
+ for (const subscription of [...live ?? []]) {
37
+ if (!live?.has(subscription)) continue;
38
+ try {
39
+ subscription.callback(event);
40
+ } catch (cause) {
41
+ report({
42
+ kind: "handler_threw",
43
+ topic: event.topic,
44
+ cause,
45
+ retrying: false
46
+ });
47
+ }
48
+ }
49
+ },
50
+ has: (topic) => subscribers.has(topic),
51
+ topics: () => [...subscribers.keys()],
52
+ report,
53
+ onReconnect: (callback) => addListener(reconnectListeners, callback),
54
+ onStatusChange: (callback) => addListener(statusListeners, callback),
55
+ onError: (callback) => addListener(errorListeners, callback),
56
+ getStatus: () => status,
57
+ setStatus(next) {
58
+ status = next;
59
+ for (const listener of [...statusListeners]) isolate(() => listener(next));
60
+ },
61
+ reconnected() {
62
+ for (const listener of [...reconnectListeners]) isolate(listener);
63
+ }
64
+ };
65
+ }
66
+ export { createTopicHub };
@@ -1,12 +1,10 @@
1
- const resourceTopic = (resource) => `resource/${resource}`;
2
- const recordTopic = (resource, id) => `resource/${resource}/${id}`;
3
- const lockResourceTopic = (resource) => `lock/${resource}`;
4
- const lockTopic = (resource, id) => `lock/${resource}/${id}`;
5
- const presenceTopic = (resource, id) => `presence/${resource}/${id}`;
6
- export {
7
- lockResourceTopic,
8
- lockTopic,
9
- presenceTopic,
10
- recordTopic,
11
- resourceTopic
12
- };
1
+ // src/realtime/topics.ts
2
+ /** Events for the resource as a whole: creates, and the ids of any write. */
3
+ var resourceTopic = (resource) => `resource/${resource}`;
4
+ /** Events for one record. `1` and `"1"` give the same topic. */
5
+ var recordTopic = (resource, id) => `resource/${resource}/${id}`;
6
+ var lockResourceTopic = (resource) => `lock/${resource}`;
7
+ var lockTopic = (resource, id) => `lock/${resource}/${id}`;
8
+ /** Who is on one record. `1` and `"1"` give the same topic. */
9
+ var presenceTopic = (resource, id) => `presence/${resource}/${id}`;
10
+ export { lockResourceTopic, lockTopic, presenceTopic, recordTopic, resourceTopic };
@@ -1,13 +1,11 @@
1
- class LockConflictError extends Error {
2
- kind = "LockConflictError";
3
- existingLock;
4
- constructor(existingLock) {
5
- super(
6
- `Resource ${existingLock.resource}/${String(existingLock.recordId)} is locked by ${String(existingLock.identity)}`
7
- );
8
- this.existingLock = existingLock;
9
- }
10
- }
11
- export {
12
- LockConflictError
1
+ // src/realtime/types.ts
2
+ /** Rejected by `lock()` when another identity already holds the record. */
3
+ var LockConflictError = class extends Error {
4
+ kind = "LockConflictError";
5
+ existingLock;
6
+ constructor(existingLock) {
7
+ super(`Resource ${existingLock.resource}/${String(existingLock.recordId)} is locked by ${String(existingLock.identity)}`);
8
+ this.existingLock = existingLock;
9
+ }
13
10
  };
11
+ export { LockConflictError };
@@ -1,27 +1,27 @@
1
1
  import { resourceTopic } from "./topics.js";
2
- const DEFAULT_EVENTS = ["created"];
2
+ // src/realtime/unread-count.ts
3
+ var DEFAULT_EVENTS = ["created"];
4
+ /**
5
+ * The store key of a resource's unread count for an event list. The default
6
+ * `["created"]` keeps `realtime.unread.<resource>`; any other list adds its
7
+ * sorted, de-duplicated types as a suffix, so the order of `events` never
8
+ * splits a count and links watching different events never share one.
9
+ */
3
10
  function unreadKey(resource, events = DEFAULT_EVENTS) {
4
- const types = [...new Set(events)].sort();
5
- const base = `realtime.unread.${resource}`;
6
- return types.length === 1 && types[0] === "created" ? base : `${base}.${types.join("+")}`;
11
+ const types = [...new Set(events)].sort();
12
+ const base = `realtime.unread.${resource}`;
13
+ return types.length === 1 && types[0] === "created" ? base : `${base}.${types.join("+")}`;
7
14
  }
8
- function subscribeUnreadCount({
9
- dataProvider,
10
- store,
11
- resource,
12
- events = DEFAULT_EVENTS
13
- }) {
14
- const key = unreadKey(resource, events);
15
- return dataProvider.subscribe(resourceTopic(resource), (event) => {
16
- if (!events.includes(event.type)) return;
17
- store.setItem(key, (store.getItem(key) ?? 0) + 1);
18
- });
15
+ /** Adds one to the count under `unreadKey(resource, events)` for each listed event. Returns the teardown. */
16
+ function subscribeUnreadCount({ dataProvider, store, resource, events = DEFAULT_EVENTS }) {
17
+ const key = unreadKey(resource, events);
18
+ return dataProvider.subscribe(resourceTopic(resource), (event) => {
19
+ if (!events.includes(event.type)) return;
20
+ store.setItem(key, (store.getItem(key) ?? 0) + 1);
21
+ });
19
22
  }
23
+ /** Resets the count of one event list; pass the same `events` the link subscribed with. */
20
24
  function clearUnread(store, resource, events) {
21
- store.setItem(unreadKey(resource, events), 0);
25
+ store.setItem(unreadKey(resource, events), 0);
22
26
  }
23
- export {
24
- clearUnread,
25
- subscribeUnreadCount,
26
- unreadKey
27
- };
27
+ export { clearUnread, subscribeUnreadCount, unreadKey };
@@ -0,0 +1,32 @@
1
+ import { type RealtimeReconnectConfig } from "./reconnect-backoff.js";
2
+ import type { RealtimeTransport, RealtimeTransportError } from "./types.js";
3
+ export interface WebSocketTransportOptions {
4
+ url: string;
5
+ /** Subprotocols offered on every connection. */
6
+ protocols?: string[];
7
+ /** Read before every connection, so a retry carries a fresh token. */
8
+ getAuthToken?: () => string | null | undefined | Promise<string | null | undefined>;
9
+ /**
10
+ * Where the token goes. `"query"` (default) adds a URL-encoded `token`
11
+ * parameter; `"subprotocol"` offers it after `protocols` and keeps it out of
12
+ * the URL.
13
+ */
14
+ authMode?: "query" | "subprotocol";
15
+ reconnect?: RealtimeReconnectConfig;
16
+ /** Close the socket after this long with no subscriber. Default 30000. */
17
+ idleDisconnectMs?: number;
18
+ /** Ping this often; a ping with no reply by the next one is a drop. Default 30000. */
19
+ heartbeatMs?: number;
20
+ /** Called for every transport error, like a listener from `onError()`. */
21
+ onError?: (error: RealtimeTransportError) => void;
22
+ }
23
+ /** A `RealtimeTransport` with every optional member, plus `close()`. */
24
+ export interface WebSocketTransport extends Required<RealtimeTransport> {
25
+ /**
26
+ * Closes the socket, rejects queued publishes and sets `"disconnected"`.
27
+ * A later subscribe or publish opens again.
28
+ */
29
+ close(): void;
30
+ }
31
+ /** Carries realtime events over one WebSocket and reconnects with backoff. */
32
+ export declare function webSocketTransport({ url, protocols, getAuthToken, authMode, reconnect, idleDisconnectMs, heartbeatMs, onError, }: WebSocketTransportOptions): WebSocketTransport;
@@ -0,0 +1,306 @@
1
+ import { reconnectDelay, resolveReconnectConfig } from "./reconnect-backoff.js";
2
+ import { createTopicHub } from "./topic-hub.js";
3
+ // src/realtime/websocket-transport.ts
4
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5
+ function isServerFrame(value) {
6
+ if (!isObject(value)) return false;
7
+ if (!("topic" in value) || typeof value.topic !== "string") return false;
8
+ if (!("type" in value) || typeof value.type !== "string") return false;
9
+ return !("meta" in value) || value.meta === void 0 || isObject(value.meta);
10
+ }
11
+ var isPong = (value) => isObject(value) && "op" in value && value.op === "pong";
12
+ function parseFrame(data) {
13
+ try {
14
+ if (typeof data !== "string") throw new Error("frame is not text");
15
+ const frame = JSON.parse(data);
16
+ if (isPong(frame)) return { kind: "pong" };
17
+ if (isServerFrame(frame)) return {
18
+ kind: "event",
19
+ event: frame
20
+ };
21
+ throw new Error("frame is not a realtime event");
22
+ } catch (cause) {
23
+ return {
24
+ kind: "invalid",
25
+ cause: cause instanceof Error ? cause : /* @__PURE__ */ new Error("frame is invalid")
26
+ };
27
+ }
28
+ }
29
+ /** Carries realtime events over one WebSocket and reconnects with backoff. */
30
+ function webSocketTransport({ url, protocols = [], getAuthToken, authMode = "query", reconnect, idleDisconnectMs = 3e4, heartbeatMs = 3e4, onError }) {
31
+ const hub = createTopicHub({ onError });
32
+ const backoff = resolveReconnectConfig(reconnect);
33
+ hub.setStatus("idle");
34
+ let connection = null;
35
+ let connecting = false;
36
+ let epoch = 0;
37
+ let attempt = 0;
38
+ let lostConnection = false;
39
+ let retryTimer;
40
+ let idleTimer;
41
+ let heartbeatTimer;
42
+ let queue = [];
43
+ const setStatus = (status) => {
44
+ if (hub.getStatus() !== status) hub.setStatus(status);
45
+ };
46
+ const rejectQueue = (reason) => {
47
+ const rejected = queue;
48
+ queue = [];
49
+ for (const publish of rejected) publish.reject(reason);
50
+ };
51
+ const send = (target, frame, topic) => {
52
+ try {
53
+ target.socket.send(JSON.stringify(frame));
54
+ return null;
55
+ } catch (cause) {
56
+ hub.report({
57
+ kind: "send_failed",
58
+ topic,
59
+ cause,
60
+ retrying: false
61
+ });
62
+ return cause instanceof Error ? cause : /* @__PURE__ */ new Error("send failed");
63
+ }
64
+ };
65
+ const stopHeartbeat = () => {
66
+ clearInterval(heartbeatTimer);
67
+ heartbeatTimer = void 0;
68
+ };
69
+ const detach = (target) => {
70
+ stopHeartbeat();
71
+ target.socket.removeEventListener("open", target.onOpen);
72
+ target.socket.removeEventListener("message", target.onMessage);
73
+ target.socket.removeEventListener("close", target.onClose);
74
+ try {
75
+ target.socket.close();
76
+ } catch {}
77
+ if (connection === target) connection = null;
78
+ };
79
+ const teardown = () => {
80
+ epoch += 1;
81
+ connecting = false;
82
+ attempt = 0;
83
+ lostConnection = false;
84
+ clearTimeout(retryTimer);
85
+ retryTimer = void 0;
86
+ clearTimeout(idleTimer);
87
+ idleTimer = void 0;
88
+ if (connection) detach(connection);
89
+ };
90
+ const touchIdle = () => {
91
+ clearTimeout(idleTimer);
92
+ idleTimer = void 0;
93
+ if (hub.topics().length > 0) return;
94
+ idleTimer = setTimeout(() => {
95
+ idleTimer = void 0;
96
+ if (hub.topics().length > 0) return;
97
+ if (!connection && !connecting && retryTimer === void 0) return;
98
+ teardown();
99
+ rejectQueue(/* @__PURE__ */ new Error("webSocketTransport went idle"));
100
+ setStatus("idle");
101
+ }, idleDisconnectMs);
102
+ };
103
+ const giveUp = (reason) => {
104
+ rejectQueue(reason);
105
+ setStatus("disconnected");
106
+ };
107
+ const lose = (target, clean, cause) => {
108
+ detach(target);
109
+ clearTimeout(idleTimer);
110
+ if (target.opened) lostConnection = true;
111
+ if (clean) {
112
+ setStatus("disconnected");
113
+ return;
114
+ }
115
+ if (backoff.enabled && attempt < backoff.maxAttempts) {
116
+ const delay = reconnectDelay(attempt, backoff);
117
+ attempt += 1;
118
+ hub.report({
119
+ kind: "connect_failed",
120
+ cause,
121
+ retrying: true
122
+ });
123
+ setStatus("reconnecting");
124
+ retryTimer = setTimeout(() => {
125
+ retryTimer = void 0;
126
+ connect();
127
+ }, delay);
128
+ touchIdle();
129
+ return;
130
+ }
131
+ hub.report({
132
+ kind: "connect_failed",
133
+ cause,
134
+ retrying: false
135
+ });
136
+ giveUp(/* @__PURE__ */ new Error("webSocketTransport could not connect"));
137
+ };
138
+ const heartbeat = (target) => {
139
+ if (target.awaitingPong) {
140
+ lose(target, false, { reason: "heartbeat missed" });
141
+ return;
142
+ }
143
+ target.awaitingPong = true;
144
+ send(target, { op: "ping" });
145
+ };
146
+ const handleMessage = (target, message) => {
147
+ target.awaitingPong = false;
148
+ const parsed = parseFrame(message.data);
149
+ if (parsed.kind === "pong") return;
150
+ if (parsed.kind === "invalid") {
151
+ hub.report({
152
+ kind: "parse_failed",
153
+ cause: parsed.cause,
154
+ retrying: false
155
+ });
156
+ return;
157
+ }
158
+ const { topic, type, payload, meta } = parsed.event;
159
+ hub.dispatch({
160
+ topic,
161
+ type,
162
+ payload,
163
+ ...meta ? { meta } : {}
164
+ });
165
+ };
166
+ const handleOpen = (target) => {
167
+ target.opened = true;
168
+ attempt = 0;
169
+ setStatus("connected");
170
+ for (const topic of hub.topics()) send(target, {
171
+ op: "subscribe",
172
+ topic
173
+ });
174
+ const flushed = queue;
175
+ queue = [];
176
+ for (const { topic, event, resolve, reject } of flushed) {
177
+ const failure = send(target, {
178
+ op: "publish",
179
+ topic,
180
+ event
181
+ }, topic);
182
+ if (failure) reject(failure);
183
+ else resolve();
184
+ }
185
+ heartbeatTimer = setInterval(() => heartbeat(target), heartbeatMs);
186
+ touchIdle();
187
+ if (lostConnection) {
188
+ lostConnection = false;
189
+ hub.reconnected();
190
+ }
191
+ };
192
+ const open = (token) => {
193
+ let target;
194
+ try {
195
+ const address = token && authMode === "query" ? `${url}${url.includes("?") ? "&" : "?"}token=${encodeURIComponent(token)}` : url;
196
+ const offered = token && authMode === "subprotocol" ? [...protocols, token] : protocols;
197
+ target = {
198
+ socket: offered.length > 0 ? new WebSocket(address, offered) : new WebSocket(address),
199
+ opened: false,
200
+ awaitingPong: false,
201
+ onOpen: () => handleOpen(target),
202
+ onMessage: (message) => handleMessage(target, message),
203
+ onClose: (event) => lose(target, event.wasClean, {
204
+ code: event.code,
205
+ reason: event.reason
206
+ })
207
+ };
208
+ } catch (error) {
209
+ const name = error instanceof Error ? error.name : "Error";
210
+ const cause = /* @__PURE__ */ new Error(`WebSocket constructor failed: ${name}`);
211
+ hub.report({
212
+ kind: "connect_failed",
213
+ cause,
214
+ retrying: false
215
+ });
216
+ giveUp(cause);
217
+ return;
218
+ }
219
+ connection = target;
220
+ target.socket.addEventListener("open", target.onOpen);
221
+ target.socket.addEventListener("message", target.onMessage);
222
+ target.socket.addEventListener("close", target.onClose);
223
+ };
224
+ async function connect() {
225
+ connecting = true;
226
+ const startedAt = epoch;
227
+ let token;
228
+ if (getAuthToken) {
229
+ try {
230
+ token = await getAuthToken();
231
+ } catch (cause) {
232
+ if (epoch !== startedAt) return;
233
+ connecting = false;
234
+ hub.report({
235
+ kind: "auth_failed",
236
+ cause,
237
+ retrying: false
238
+ });
239
+ giveUp(cause instanceof Error ? cause : /* @__PURE__ */ new Error("auth failed"));
240
+ return;
241
+ }
242
+ if (epoch !== startedAt) return;
243
+ }
244
+ connecting = false;
245
+ open(token);
246
+ }
247
+ const ensureOpen = () => {
248
+ if (connection || connecting || retryTimer !== void 0) return;
249
+ attempt = 0;
250
+ connect();
251
+ };
252
+ return {
253
+ subscribe(topic, callback) {
254
+ const first = !hub.has(topic);
255
+ const unsubscribe = hub.subscribe(topic, callback);
256
+ touchIdle();
257
+ if (first && connection?.opened) send(connection, {
258
+ op: "subscribe",
259
+ topic
260
+ }, topic);
261
+ ensureOpen();
262
+ let done = false;
263
+ return () => {
264
+ if (done) return;
265
+ done = true;
266
+ unsubscribe();
267
+ if (!hub.has(topic) && connection?.opened) send(connection, {
268
+ op: "unsubscribe",
269
+ topic
270
+ }, topic);
271
+ touchIdle();
272
+ };
273
+ },
274
+ publish(topic, event) {
275
+ if (connection?.opened) {
276
+ touchIdle();
277
+ const failure = send(connection, {
278
+ op: "publish",
279
+ topic,
280
+ event
281
+ }, topic);
282
+ return failure ? Promise.reject(failure) : Promise.resolve();
283
+ }
284
+ return new Promise((resolve, reject) => {
285
+ queue.push({
286
+ topic,
287
+ event,
288
+ resolve,
289
+ reject
290
+ });
291
+ touchIdle();
292
+ ensureOpen();
293
+ });
294
+ },
295
+ onReconnect: hub.onReconnect,
296
+ onStatusChange: hub.onStatusChange,
297
+ onError: hub.onError,
298
+ getStatus: hub.getStatus,
299
+ close() {
300
+ teardown();
301
+ rejectQueue(/* @__PURE__ */ new Error("webSocketTransport is closed"));
302
+ setStatus("disconnected");
303
+ }
304
+ };
305
+ }
306
+ export { webSocketTransport };
@@ -28,6 +28,8 @@ export interface ResourceDescriptor {
28
28
  resource: string;
29
29
  fields: FieldDescriptor[];
30
30
  }
31
+ /** For core's block editor helpers; not exported from the package index. */
32
+ export declare function describeField(source: string, schema: unknown, references: ResourceConfig["references"]): FieldDescriptor;
31
33
  /**
32
34
  * Turns a resource's registered Zod schema (Zod 3 or Zod 4) into one descriptor per field, in
33
35
  * schema key order. Returns `undefined` when there is no schema to read or it