@chelone/core 0.6.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 (242) 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 +21 -17
  10. package/dist/auth/auth-options.js +80 -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 +60 -0
  15. package/dist/block-editor/block-document.js +143 -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.js +8 -11
  21. package/dist/command-menu/registry.d.ts +9 -5
  22. package/dist/command-menu/registry.js +36 -46
  23. package/dist/controller/calendar-dates.js +82 -113
  24. package/dist/controller/calendar-events.js +93 -88
  25. package/dist/controller/client-list-controller.d.ts +2 -2
  26. package/dist/controller/client-list-controller.js +251 -254
  27. package/dist/controller/data-table-columns.d.ts +2 -0
  28. package/dist/controller/data-table-columns.js +50 -56
  29. package/dist/controller/deep-equal.d.ts +1 -0
  30. package/dist/controller/deep-equal.js +14 -0
  31. package/dist/controller/filter-toggle.js +36 -44
  32. package/dist/controller/in-place-editor.d.ts +42 -0
  33. package/dist/controller/in-place-editor.js +42 -0
  34. package/dist/controller/infinite-list-controller.js +219 -189
  35. package/dist/controller/inspector.d.ts +49 -0
  36. package/dist/controller/inspector.js +64 -0
  37. package/dist/controller/job-monitor.d.ts +3 -4
  38. package/dist/controller/job-monitor.js +37 -47
  39. package/dist/controller/kanban-board.d.ts +2 -3
  40. package/dist/controller/kanban-board.js +43 -24
  41. package/dist/controller/list-controller.d.ts +5 -5
  42. package/dist/controller/list-controller.js +444 -400
  43. package/dist/controller/list-params.js +96 -87
  44. package/dist/controller/pivot-grid.js +89 -95
  45. package/dist/controller/prev-next.d.ts +2 -2
  46. package/dist/controller/prev-next.js +74 -54
  47. package/dist/controller/record-controllers.d.ts +14 -6
  48. package/dist/controller/record-controllers.js +288 -257
  49. package/dist/controller/record-diff.d.ts +21 -0
  50. package/dist/controller/record-diff.js +50 -0
  51. package/dist/controller/record-timeline.js +42 -30
  52. package/dist/controller/record-tree.js +57 -47
  53. package/dist/controller/record-workflow.d.ts +38 -16
  54. package/dist/controller/record-workflow.js +85 -64
  55. package/dist/controller/reference-many-controller.js +198 -192
  56. package/dist/controller/reference-one-controller.d.ts +1 -1
  57. package/dist/controller/reference-one-controller.js +63 -56
  58. package/dist/controller/saved-queries.js +104 -91
  59. package/dist/controller/translate-fallback.js +10 -6
  60. package/dist/csv/export.d.ts +2 -1
  61. package/dist/csv/export.js +43 -40
  62. package/dist/csv/import-controller.js +185 -169
  63. package/dist/csv/match-headers.js +29 -22
  64. package/dist/csv/parse-with.js +60 -54
  65. package/dist/csv/run-import.js +69 -72
  66. package/dist/csv/validate-rows.d.ts +1 -1
  67. package/dist/csv/validate-rows.js +65 -52
  68. package/dist/data/cache-ops.d.ts +5 -4
  69. package/dist/data/cache-ops.js +208 -188
  70. package/dist/data/combine-data-providers.js +25 -23
  71. package/dist/data/data-provider-log.d.ts +45 -0
  72. package/dist/data/data-provider-log.js +93 -0
  73. package/dist/data/errors.js +14 -10
  74. package/dist/data/fake-provider.d.ts +6 -3
  75. package/dist/data/fake-provider.js +129 -114
  76. package/dist/data/fetch-utils.js +61 -44
  77. package/dist/data/filter-branch.d.ts +5 -0
  78. package/dist/data/filter-branch.js +10 -0
  79. package/dist/data/get-many-aggregate.js +84 -76
  80. package/dist/data/http-error.js +17 -12
  81. package/dist/data/keys.js +50 -36
  82. package/dist/data/mutation-options.d.ts +14 -9
  83. package/dist/data/mutation-options.js +93 -109
  84. package/dist/data/query-options.js +73 -68
  85. package/dist/data/remove-empty.d.ts +3 -0
  86. package/dist/data/remove-empty.js +11 -0
  87. package/dist/data/undoable.d.ts +2 -2
  88. package/dist/data/undoable.js +190 -174
  89. package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
  90. package/dist/data/with-lifecycle-callbacks.js +66 -52
  91. package/dist/environment/application-update-store.js +58 -55
  92. package/dist/environment/offline-store.js +21 -21
  93. package/dist/form/array-binding.d.ts +30 -11
  94. package/dist/form/array-binding.js +184 -166
  95. package/dist/form/choice-ids.d.ts +7 -0
  96. package/dist/form/choice-ids.js +11 -0
  97. package/dist/form/choices-controller.d.ts +3 -2
  98. package/dist/form/choices-controller.js +302 -275
  99. package/dist/form/date-format.d.ts +38 -0
  100. package/dist/form/date-format.js +89 -0
  101. package/dist/form/dirty-values.d.ts +2 -2
  102. package/dist/form/dirty-values.js +22 -11
  103. package/dist/form/duration.d.ts +22 -0
  104. package/dist/form/duration.js +49 -0
  105. package/dist/form/edit-create-form.d.ts +12 -5
  106. package/dist/form/edit-create-form.js +243 -188
  107. package/dist/form/event-value.d.ts +6 -0
  108. package/dist/form/event-value.js +17 -0
  109. package/dist/form/field-error.d.ts +14 -0
  110. package/dist/form/field-error.js +28 -0
  111. package/dist/form/field-name.d.ts +20 -0
  112. package/dist/form/field-name.js +6 -0
  113. package/dist/form/filter-form-values.js +63 -49
  114. package/dist/form/form-controller.d.ts +9 -1
  115. package/dist/form/form-controller.js +165 -160
  116. package/dist/form/form-groups.d.ts +4 -3
  117. package/dist/form/form-groups.js +79 -79
  118. package/dist/form/form-handle.d.ts +175 -0
  119. package/dist/form/form-handle.js +68 -0
  120. package/dist/form/input-binding.d.ts +56 -31
  121. package/dist/form/input-binding.js +169 -118
  122. package/dist/form/input-values.d.ts +130 -0
  123. package/dist/form/input-values.js +177 -0
  124. package/dist/form/json-text.js +51 -37
  125. package/dist/form/save-pipeline.d.ts +6 -8
  126. package/dist/form/save-pipeline.js +91 -82
  127. package/dist/form/set-form-values.d.ts +6 -0
  128. package/dist/form/set-form-values.js +10 -0
  129. package/dist/form/source.js +97 -65
  130. package/dist/form/text-of.d.ts +2 -0
  131. package/dist/form/text-of.js +7 -0
  132. package/dist/form/validators.d.ts +58 -19
  133. package/dist/form/validators.js +97 -71
  134. package/dist/geo/bbox.d.ts +5 -4
  135. package/dist/geo/bbox.js +69 -48
  136. package/dist/geo/bounds.d.ts +14 -3
  137. package/dist/geo/bounds.js +98 -68
  138. package/dist/geo/constraints.js +88 -93
  139. package/dist/geo/coordinates.js +31 -23
  140. package/dist/geo/editor.d.ts +13 -13
  141. package/dist/geo/editor.js +176 -153
  142. package/dist/geo/geocoding.d.ts +4 -4
  143. package/dist/geo/geocoding.js +38 -41
  144. package/dist/geo/geojson.d.ts +12 -74
  145. package/dist/geo/geojson.js +52 -40
  146. package/dist/geo/geometry-math.d.ts +4 -4
  147. package/dist/geo/geometry-math.js +100 -93
  148. package/dist/geo/index.d.ts +7 -6
  149. package/dist/geo/map-layers.d.ts +42 -7
  150. package/dist/geo/map-layers.js +88 -62
  151. package/dist/geo/map-style.js +31 -32
  152. package/dist/geo/messages.js +48 -45
  153. package/dist/geo/nominatim.js +88 -80
  154. package/dist/geo/osm-presets.d.ts +4 -3
  155. package/dist/geo/osm-presets.js +108 -86
  156. package/dist/geo/osm-tags.js +14 -12
  157. package/dist/geo/overpass.d.ts +14 -2
  158. package/dist/geo/overpass.js +83 -90
  159. package/dist/geo/shape-value.d.ts +9 -5
  160. package/dist/geo/shape-value.js +169 -146
  161. package/dist/geo/snap-to-roads.d.ts +3 -3
  162. package/dist/geo/snap-to-roads.js +55 -50
  163. package/dist/geo/throttle.js +36 -39
  164. package/dist/guards.d.ts +37 -0
  165. package/dist/guards.js +62 -0
  166. package/dist/i18n/default-messages.js +34 -29
  167. package/dist/i18n/english-messages.js +187 -189
  168. package/dist/i18n/i18n.d.ts +2 -2
  169. package/dist/i18n/i18n.js +47 -60
  170. package/dist/i18n/inflection.d.ts +34 -0
  171. package/dist/i18n/inflection.js +82 -0
  172. package/dist/i18n/messages.d.ts +10 -2
  173. package/dist/i18n/messages.js +17 -4
  174. package/dist/i18n/missing-keys.js +66 -62
  175. package/dist/i18n/translatable.d.ts +16 -1
  176. package/dist/i18n/translatable.js +40 -47
  177. package/dist/index.d.ts +71 -24
  178. package/dist/index.js +40 -288
  179. package/dist/json/deep-equal.d.ts +8 -0
  180. package/dist/json/deep-equal.js +24 -0
  181. package/dist/json/strip-json-comments.js +38 -42
  182. package/dist/noop.d.ts +2 -0
  183. package/dist/noop.js +4 -0
  184. package/dist/notification/error-message.d.ts +2 -0
  185. package/dist/notification/error-message.js +10 -0
  186. package/dist/notification/notification-store.d.ts +3 -2
  187. package/dist/notification/notification-store.js +41 -45
  188. package/dist/onboarding-tour/placement.js +63 -78
  189. package/dist/onboarding-tour/tour-controller.js +46 -54
  190. package/dist/realtime/add-events-for-mutations.js +75 -81
  191. package/dist/realtime/announce-lock.js +16 -13
  192. package/dist/realtime/broadcast-channel-transport.js +65 -59
  193. package/dist/realtime/copy-members.js +22 -14
  194. package/dist/realtime/in-memory-lock-provider.js +40 -48
  195. package/dist/realtime/live.js +95 -101
  196. package/dist/realtime/lock-options.js +16 -16
  197. package/dist/realtime/lock-session.js +91 -78
  198. package/dist/realtime/memory-transport.js +25 -25
  199. package/dist/realtime/presence.js +120 -122
  200. package/dist/realtime/realtime-data-provider.d.ts +10 -0
  201. package/dist/realtime/realtime-data-provider.js +45 -42
  202. package/dist/realtime/reconnect-backoff.js +17 -0
  203. package/dist/realtime/sse-transport.d.ts +31 -0
  204. package/dist/realtime/sse-transport.js +235 -0
  205. package/dist/realtime/status-store.js +23 -28
  206. package/dist/realtime/topic-hub.js +62 -71
  207. package/dist/realtime/topics.js +10 -12
  208. package/dist/realtime/types.js +10 -12
  209. package/dist/realtime/unread-count.js +21 -21
  210. package/dist/realtime/websocket-transport.d.ts +32 -0
  211. package/dist/realtime/websocket-transport.js +306 -0
  212. package/dist/resource/describe-resource.d.ts +2 -0
  213. package/dist/resource/describe-resource.js +232 -219
  214. package/dist/resource/get-at-path.js +29 -14
  215. package/dist/resource/identifier.d.ts +18 -0
  216. package/dist/resource/identifier.js +27 -0
  217. package/dist/resource/record.d.ts +4 -0
  218. package/dist/resource/record.js +10 -0
  219. package/dist/resource/register.d.ts +7 -0
  220. package/dist/resource/registry-store.d.ts +45 -15
  221. package/dist/resource/registry-store.js +67 -60
  222. package/dist/resource/registry.d.ts +10 -0
  223. package/dist/resource/registry.js +8 -4
  224. package/dist/resource/source-path.d.ts +16 -1
  225. package/dist/rich-text/rich-text-extras.d.ts +132 -0
  226. package/dist/rich-text/rich-text-extras.js +179 -0
  227. package/dist/rich-text/rich-text-image.d.ts +124 -0
  228. package/dist/rich-text/rich-text-image.js +163 -0
  229. package/dist/rich-text/rich-text.d.ts +6 -0
  230. package/dist/rich-text/rich-text.js +125 -88
  231. package/dist/router/location.d.ts +22 -4
  232. package/dist/router/location.js +41 -18
  233. package/dist/router/match.js +47 -39
  234. package/dist/router/paths.js +40 -34
  235. package/dist/router/record-from-location.d.ts +1 -1
  236. package/dist/router/record-from-location.js +25 -17
  237. package/dist/store/adapters.js +51 -55
  238. package/dist/store/micro-store.js +57 -59
  239. package/dist/store/shallow-equal.d.ts +12 -0
  240. package/dist/store/shallow-equal.js +20 -0
  241. package/dist/store/store.js +54 -57
  242. package/package.json +13 -11
@@ -1,196 +1,212 @@
1
- import { partialMatchKey } from "@tanstack/query-core";
2
1
  import { removeRecordsFromCache, updateRecordsInCache } from "./cache-ops.js";
3
- const pendingUndoables = /* @__PURE__ */ new Set();
4
- const settledListeners = /* @__PURE__ */ new Set();
5
- let inFlightCommits = 0;
2
+ import { partialMatchKey } from "@tanstack/query-core";
3
+ // src/data/undoable.ts
4
+ /**
5
+ * Module-level registry of still-undoable mutations (those neither committed
6
+ * nor undone). Drives the adapter's `beforeunload` guard: if any remain
7
+ * pending when the page unloads, commit them so the server is not left behind
8
+ * the optimistic cache.
9
+ */
10
+ var pendingUndoables = /* @__PURE__ */ new Set();
11
+ var settledListeners = /* @__PURE__ */ new Set();
12
+ /** Committed undoables whose deferred server call has not settled yet. */
13
+ var inFlightCommits = 0;
14
+ /** Calls the listeners once no undoable is pending: after an undo, or after a commit's server call settles. */
6
15
  function notifySettled() {
7
- if (hasUnsettledUndoables()) return;
8
- for (const listener of [...settledListeners]) listener();
9
- }
10
- const openUndoables = /* @__PURE__ */ new WeakMap();
16
+ if (hasUnsettledUndoables()) return;
17
+ for (const listener of [...settledListeners]) listener();
18
+ }
19
+ /**
20
+ * Per client, the undoables in start order that are pending, mid-commit, or
21
+ * committed behind an earlier one that has not settled. Handles snapshot the
22
+ * whole resource, so a rollback must replay the later ones over its own
23
+ * restore instead of overwriting their changes.
24
+ */
25
+ var openUndoables = /* @__PURE__ */ new WeakMap();
11
26
  function takeSnapshot(queryClient, queryKeys) {
12
- return queryKeys.reduce(
13
- (acc, queryKey) => acc.concat(queryClient.getQueriesData({ queryKey })),
14
- []
15
- );
27
+ return queryKeys.reduce((acc, queryKey) => acc.concat(queryClient.getQueriesData({ queryKey })), []);
16
28
  }
17
29
  function restore(queryClient, snapshot) {
18
- for (const [key, value] of snapshot) {
19
- queryClient.setQueryData(key, value);
20
- }
30
+ for (const [key, value] of snapshot) queryClient.setQueryData(key, value);
21
31
  }
32
+ /** Query-key partial matching is one-way, so overlap is a prefix match either way round. */
22
33
  function keysOverlap(a, b) {
23
- return a.some(
24
- (x) => b.some((y) => partialMatchKey(x, y) || partialMatchKey(y, x))
25
- );
26
- }
27
- function entriesToReplay(later2, entry) {
28
- const affectedKeys = [...entry.queryKeys];
29
- const affected = [];
30
- for (const other of later2) {
31
- if (!keysOverlap(affectedKeys, other.queryKeys)) continue;
32
- affected.push(other);
33
- affectedKeys.push(...other.queryKeys);
34
- }
35
- return affected;
36
- }
34
+ return a.some((x) => b.some((y) => partialMatchKey(x, y) || partialMatchKey(y, x)));
35
+ }
36
+ /**
37
+ * The later entries a rollback of `entry` must unwind and replay: those whose
38
+ * keys overlap `entry`'s, or those of an earlier included entry. Others never
39
+ * saw its change, and restoring their snapshot would revert unrelated cache.
40
+ */
41
+ function entriesToReplay(later, entry) {
42
+ const affectedKeys = [...entry.queryKeys];
43
+ const affected = [];
44
+ for (const other of later) {
45
+ if (!keysOverlap(affectedKeys, other.queryKeys)) continue;
46
+ affected.push(other);
47
+ affectedKeys.push(...other.queryKeys);
48
+ }
49
+ return affected;
50
+ }
51
+ /** Drops committed entries from the front: nothing earlier can roll back over them any more. */
37
52
  function dropSettledPrefix(order) {
38
- while (order[0]?.committed) order.shift();
53
+ while (order[0]?.committed) order.shift();
39
54
  }
55
+ /**
56
+ * Start an undoable mutation: snapshot the affected cache, cancel outgoing
57
+ * refetches, apply the optimistic change, and return a handle whose `undo()` /
58
+ * `commit()` settle it. React/Vue-free — pure query-core.
59
+ */
40
60
  function startUndoableMutation(config) {
41
- const { queryClient, queryKeys, applyOptimistic, commit } = config;
42
- const entry = {
43
- queryKeys,
44
- applyOptimistic,
45
- snapshot: takeSnapshot(queryClient, queryKeys),
46
- committed: false
47
- };
48
- for (const queryKey of queryKeys) {
49
- void queryClient.cancelQueries({ queryKey });
50
- }
51
- applyOptimistic();
52
- let order = openUndoables.get(queryClient);
53
- if (!order) {
54
- order = [];
55
- openUndoables.set(queryClient, order);
56
- }
57
- order.push(entry);
58
- let state = "pending";
59
- let commitPromise;
60
- const rollback = () => {
61
- const open = order;
62
- const index = open.indexOf(entry);
63
- const later2 = entriesToReplay(open.slice(index + 1), entry);
64
- open.splice(index, 1);
65
- for (const other of [...later2].reverse())
66
- restore(queryClient, other.snapshot);
67
- restore(queryClient, entry.snapshot);
68
- for (const other of later2) {
69
- other.snapshot = takeSnapshot(queryClient, other.queryKeys);
70
- other.applyOptimistic();
71
- }
72
- dropSettledPrefix(open);
73
- };
74
- const handle = {
75
- get isPending() {
76
- return state === "pending";
77
- },
78
- undo() {
79
- if (state !== "pending") return;
80
- state = "undone";
81
- pendingUndoables.delete(handle);
82
- rollback();
83
- notifySettled();
84
- },
85
- commit() {
86
- if (state === "committed") {
87
- return commitPromise;
88
- }
89
- if (state === "undone") return Promise.resolve(void 0);
90
- state = "committed";
91
- pendingUndoables.delete(handle);
92
- inFlightCommits += 1;
93
- commitPromise = commit().then(
94
- (result) => {
95
- entry.committed = true;
96
- dropSettledPrefix(order);
97
- return result;
98
- },
99
- (error) => {
100
- rollback();
101
- throw error;
102
- }
103
- );
104
- const settle = () => {
105
- inFlightCommits -= 1;
106
- notifySettled();
107
- };
108
- commitPromise.then(settle, settle);
109
- return commitPromise;
110
- }
111
- };
112
- pendingUndoables.add(handle);
113
- return handle;
114
- }
61
+ const { queryClient, queryKeys, applyOptimistic, commit } = config;
62
+ const entry = {
63
+ queryKeys,
64
+ applyOptimistic,
65
+ snapshot: takeSnapshot(queryClient, queryKeys),
66
+ committed: false
67
+ };
68
+ for (const queryKey of queryKeys) queryClient.cancelQueries({ queryKey });
69
+ applyOptimistic();
70
+ let order = openUndoables.get(queryClient);
71
+ if (!order) {
72
+ order = [];
73
+ openUndoables.set(queryClient, order);
74
+ }
75
+ order.push(entry);
76
+ let state = "pending";
77
+ let commitPromise;
78
+ const rollback = () => {
79
+ const open = order;
80
+ const index = open.indexOf(entry);
81
+ const later = entriesToReplay(open.slice(index + 1), entry);
82
+ open.splice(index, 1);
83
+ for (const other of [...later].reverse()) restore(queryClient, other.snapshot);
84
+ restore(queryClient, entry.snapshot);
85
+ for (const other of later) {
86
+ other.snapshot = takeSnapshot(queryClient, other.queryKeys);
87
+ other.applyOptimistic();
88
+ }
89
+ dropSettledPrefix(open);
90
+ };
91
+ const handle = {
92
+ get isPending() {
93
+ return state === "pending";
94
+ },
95
+ undo() {
96
+ if (state !== "pending") return;
97
+ state = "undone";
98
+ pendingUndoables.delete(handle);
99
+ rollback();
100
+ notifySettled();
101
+ },
102
+ commit() {
103
+ if (commitPromise) return commitPromise;
104
+ if (state !== "pending") return Promise.resolve(void 0);
105
+ state = "committed";
106
+ pendingUndoables.delete(handle);
107
+ inFlightCommits += 1;
108
+ commitPromise = commit().then((result) => {
109
+ entry.committed = true;
110
+ dropSettledPrefix(order);
111
+ return result;
112
+ }, (error) => {
113
+ rollback();
114
+ throw error;
115
+ });
116
+ const settle = () => {
117
+ inFlightCommits -= 1;
118
+ notifySettled();
119
+ };
120
+ commitPromise.then(settle, settle);
121
+ return commitPromise;
122
+ }
123
+ };
124
+ pendingUndoables.add(handle);
125
+ return handle;
126
+ }
127
+ /** Runs `run` after the current microtasks; a throw is logged, not raised. */
115
128
  function later(run) {
116
- setTimeout(() => {
117
- try {
118
- run();
119
- } catch (error) {
120
- console.error(error);
121
- }
122
- }, 0);
123
- }
129
+ setTimeout(() => {
130
+ try {
131
+ run();
132
+ } catch (error) {
133
+ console.error(error);
134
+ }
135
+ }, 0);
136
+ }
137
+ /**
138
+ * Bind an undoable `mutate`'s callbacks to its `params`. The wrapped commit
139
+ * rethrows a failure, so the handle still rolls back and the adapter still
140
+ * notifies; the callbacks wait a macrotask to run after both.
141
+ */
124
142
  function undoableCallbacks(params, callbacks = {}) {
125
- const { onSuccess, onError, onSettled } = callbacks;
126
- return {
127
- success: (data) => onSuccess?.(data, params),
128
- // The callbacks run in a timer, never inside the commit's promise: one that
129
- // throws must not read as a failed commit and roll the cache back.
130
- commit: (commit) => async () => {
131
- let result;
132
- try {
133
- result = await commit();
134
- } catch (error) {
135
- later(() => {
136
- onError?.(error, params);
137
- onSettled?.(params);
138
- });
139
- throw error;
140
- }
141
- later(() => onSettled?.(params));
142
- return result;
143
- }
144
- };
145
- }
143
+ const { onSuccess, onError, onSettled } = callbacks;
144
+ return {
145
+ success: (data) => onSuccess?.(data, params),
146
+ commit: (commit) => async () => {
147
+ let result;
148
+ try {
149
+ result = await commit();
150
+ } catch (error) {
151
+ later(() => {
152
+ onError?.(error, params);
153
+ onSettled?.(params);
154
+ });
155
+ throw error;
156
+ }
157
+ later(() => onSettled?.(params));
158
+ return result;
159
+ }
160
+ };
161
+ }
162
+ /** Patch `ids` in the `resource` caches now; the notification reads "updated". */
146
163
  function undoableUpdateOptions(queryClient, resource, ids, data) {
147
- return {
148
- queryKeys: [[resource]],
149
- applyOptimistic: () => updateRecordsInCache(
150
- queryClient,
151
- resource,
152
- ids,
153
- data
154
- ),
155
- message: "chelone.notification.updated",
156
- messageArgs: { smart_count: ids.length }
157
- };
158
- }
164
+ return {
165
+ queryKeys: [[resource]],
166
+ applyOptimistic: () => updateRecordsInCache(queryClient, resource, ids, data),
167
+ message: "chelone.notification.updated",
168
+ messageArgs: { smart_count: ids.length }
169
+ };
170
+ }
171
+ /** Remove `ids` from the `resource` caches now; the notification reads "deleted". */
159
172
  function undoableDeleteOptions(queryClient, resource, ids) {
160
- return {
161
- queryKeys: [[resource]],
162
- applyOptimistic: () => removeRecordsFromCache(queryClient, resource, ids),
163
- message: "chelone.notification.deleted",
164
- messageArgs: { smart_count: ids.length }
165
- };
166
- }
173
+ return {
174
+ queryKeys: [[resource]],
175
+ applyOptimistic: () => removeRecordsFromCache(queryClient, resource, ids),
176
+ message: "chelone.notification.deleted",
177
+ messageArgs: { smart_count: ids.length }
178
+ };
179
+ }
180
+ /** The record an undoable update shows at once: the previous one with the patch on top. */
167
181
  function patchedRecord(params) {
168
- return { ...params.previousData, ...params.data, id: params.id };
182
+ return {
183
+ ...params.previousData,
184
+ ...params.data,
185
+ id: params.id
186
+ };
169
187
  }
188
+ /** True while at least one undoable mutation is still awaiting commit/undo. */
170
189
  function hasPendingUndoables() {
171
- return pendingUndoables.size > 0;
190
+ return pendingUndoables.size > 0;
172
191
  }
192
+ /** True while an undoable is pending or its committed server call is still in flight. */
173
193
  function hasUnsettledUndoables() {
174
- return pendingUndoables.size > 0 || inFlightCommits > 0;
194
+ return pendingUndoables.size > 0 || inFlightCommits > 0;
175
195
  }
196
+ /** Runs `listener` each time the last pending undoable mutation settles. */
176
197
  function onUndoablesSettled(listener) {
177
- settledListeners.add(listener);
178
- return () => {
179
- settledListeners.delete(listener);
180
- };
181
- }
198
+ settledListeners.add(listener);
199
+ return () => {
200
+ settledListeners.delete(listener);
201
+ };
202
+ }
203
+ /**
204
+ * Commit every still-pending undoable mutation (the `beforeunload` guard).
205
+ * Never rejects — individual commit failures are swallowed here (each already
206
+ * rolled its own cache back); it only guarantees the server calls are fired.
207
+ */
182
208
  async function commitAllUndoables() {
183
- const handles = [...pendingUndoables];
184
- await Promise.allSettled(handles.map((handle) => handle.commit()));
185
- }
186
- export {
187
- commitAllUndoables,
188
- hasPendingUndoables,
189
- hasUnsettledUndoables,
190
- onUndoablesSettled,
191
- patchedRecord,
192
- startUndoableMutation,
193
- undoableCallbacks,
194
- undoableDeleteOptions,
195
- undoableUpdateOptions
196
- };
209
+ const handles = [...pendingUndoables];
210
+ await Promise.allSettled(handles.map((handle) => handle.commit()));
211
+ }
212
+ export { commitAllUndoables, hasPendingUndoables, hasUnsettledUndoables, onUndoablesSettled, patchedRecord, startUndoableMutation, undoableCallbacks, undoableDeleteOptions, undoableUpdateOptions };
@@ -1,17 +1,28 @@
1
- import type { LooseRecord } from "../resource/record.js";
1
+ import type { LooseRecord, ResourceRecord } from "../resource/record.js";
2
2
  import type { RecordOf, ResourceMap } from "../resource/registry.js";
3
3
  import type { CreateParams, CreateResult, DataProvider, DeleteManyParams, DeleteManyResult, DeleteParams, DeleteResult, GetListParams, GetListResult, GetManyParams, GetManyReferenceParams, GetManyReferenceResult, GetManyResult, GetOneParams, GetOneResult, UpdateManyParams, UpdateManyResult, UpdateParams, UpdateResult } from "./types.js";
4
4
  type MaybePromise<T> = T | Promise<T>;
5
+ /**
6
+ * The hooks that do not depend on a record type, so a named handler and the
7
+ * `"*"` handler declare them once.
8
+ */
9
+ interface RecordFreeCallbacks<TResources extends ResourceMap> {
10
+ beforeGetList?(params: GetListParams, dataProvider: DataProvider<TResources>): MaybePromise<GetListParams>;
11
+ beforeGetOne?(params: GetOneParams, dataProvider: DataProvider<TResources>): MaybePromise<GetOneParams>;
12
+ beforeGetMany?(params: GetManyParams, dataProvider: DataProvider<TResources>): MaybePromise<GetManyParams>;
13
+ beforeGetManyReference?(params: GetManyReferenceParams, dataProvider: DataProvider<TResources>): MaybePromise<GetManyReferenceParams>;
14
+ beforeDeleteMany?(params: DeleteManyParams, dataProvider: DataProvider<TResources>): MaybePromise<DeleteManyParams>;
15
+ afterUpdateMany?(result: UpdateManyResult, dataProvider: DataProvider<TResources>): MaybePromise<UpdateManyResult>;
16
+ afterDeleteMany?(result: DeleteManyResult, dataProvider: DataProvider<TResources>): MaybePromise<DeleteManyResult>;
17
+ }
5
18
  /**
6
19
  * The optional hooks of one handler, typed for records of type `TRecord`.
7
20
  * Every callback must return the value: `undefined` is not "unchanged".
8
21
  * `dataProvider` is the provider passed to `withLifecycleCallbacks`, so
9
22
  * calling it never re-enters the callbacks.
10
23
  */
11
- export interface LifecycleCallbacks<TResources extends ResourceMap, TRecord extends LooseRecord> {
12
- beforeGetList?(params: GetListParams, dataProvider: DataProvider<TResources>): MaybePromise<GetListParams>;
24
+ export interface LifecycleCallbacks<TResources extends ResourceMap, TRecord extends LooseRecord> extends RecordFreeCallbacks<TResources> {
13
25
  afterGetList?(result: GetListResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<GetListResult<TRecord>>;
14
- beforeGetOne?(params: GetOneParams, dataProvider: DataProvider<TResources>): MaybePromise<GetOneParams>;
15
26
  afterGetOne?(result: GetOneResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<GetOneResult<TRecord>>;
16
27
  beforeCreate?(params: CreateParams<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<CreateParams<TRecord>>;
17
28
  afterCreate?(result: CreateResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<CreateResult<TRecord>>;
@@ -19,26 +30,43 @@ export interface LifecycleCallbacks<TResources extends ResourceMap, TRecord exte
19
30
  afterUpdate?(result: UpdateResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<UpdateResult<TRecord>>;
20
31
  beforeDelete?(params: DeleteParams<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<DeleteParams<TRecord>>;
21
32
  afterDelete?(result: DeleteResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<DeleteResult<TRecord>>;
22
- beforeGetMany?(params: GetManyParams, dataProvider: DataProvider<TResources>): MaybePromise<GetManyParams>;
23
33
  afterGetMany?(result: GetManyResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<GetManyResult<TRecord>>;
24
- beforeGetManyReference?(params: GetManyReferenceParams, dataProvider: DataProvider<TResources>): MaybePromise<GetManyReferenceParams>;
25
34
  afterGetManyReference?(result: GetManyReferenceResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<GetManyReferenceResult<TRecord>>;
26
35
  beforeUpdateMany?(params: UpdateManyParams<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<UpdateManyParams<TRecord>>;
27
- afterUpdateMany?(result: UpdateManyResult, dataProvider: DataProvider<TResources>): MaybePromise<UpdateManyResult>;
28
- beforeDeleteMany?(params: DeleteManyParams, dataProvider: DataProvider<TResources>): MaybePromise<DeleteManyParams>;
29
- afterDeleteMany?(result: DeleteManyResult, dataProvider: DataProvider<TResources>): MaybePromise<DeleteManyResult>;
30
36
  }
31
37
  /**
32
- * A handler: the callbacks for one registry resource, typed with that
33
- * resource's record, or for `"*"` (every resource), typed with `LooseRecord`.
38
+ * The hooks of the `"*"` handler. Each one is generic over the record, so it
39
+ * hands back the record type it received: the callback runs for every
40
+ * resource and cannot name one.
34
41
  */
35
- export type LifecycleHandler<TResources extends ResourceMap = ResourceMap> = {
36
- [TName in keyof TResources & string]: {
37
- resource: TName;
38
- } & LifecycleCallbacks<TResources, RecordOf<TResources, TName>>;
39
- }[keyof TResources & string] | ({
42
+ export interface WildcardCallbacks<TResources extends ResourceMap> extends RecordFreeCallbacks<TResources> {
43
+ afterGetList?<TRecord extends ResourceRecord>(result: GetListResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<GetListResult<TRecord>>;
44
+ afterGetOne?<TRecord extends ResourceRecord>(result: GetOneResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<GetOneResult<TRecord>>;
45
+ beforeCreate?<TRecord extends ResourceRecord>(params: CreateParams<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<CreateParams<TRecord>>;
46
+ afterCreate?<TRecord extends ResourceRecord>(result: CreateResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<CreateResult<TRecord>>;
47
+ beforeUpdate?<TRecord extends ResourceRecord>(params: UpdateParams<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<UpdateParams<TRecord>>;
48
+ afterUpdate?<TRecord extends ResourceRecord>(result: UpdateResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<UpdateResult<TRecord>>;
49
+ beforeDelete?<TRecord extends ResourceRecord>(params: DeleteParams<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<DeleteParams<TRecord>>;
50
+ afterDelete?<TRecord extends ResourceRecord>(result: DeleteResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<DeleteResult<TRecord>>;
51
+ afterGetMany?<TRecord extends ResourceRecord>(result: GetManyResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<GetManyResult<TRecord>>;
52
+ afterGetManyReference?<TRecord extends ResourceRecord>(result: GetManyReferenceResult<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<GetManyReferenceResult<TRecord>>;
53
+ beforeUpdateMany?<TRecord extends ResourceRecord>(params: UpdateManyParams<TRecord>, dataProvider: DataProvider<TResources>): MaybePromise<UpdateManyParams<TRecord>>;
54
+ }
55
+ /** One handler per registry resource, typed with that resource's record. */
56
+ type HandlerMap<TResources extends ResourceMap> = {
57
+ [N in keyof TResources & string]: {
58
+ resource: N;
59
+ } & LifecycleCallbacks<TResources, RecordOf<TResources, N>>;
60
+ };
61
+ type WildcardHandler<TResources extends ResourceMap> = {
40
62
  resource: "*";
41
- } & LifecycleCallbacks<TResources, LooseRecord>);
63
+ } & WildcardCallbacks<TResources>;
64
+ /**
65
+ * A handler: the callbacks for one registry resource, typed with that
66
+ * resource's record, or for `"*"` (every resource), which hands back the
67
+ * record type it receives.
68
+ */
69
+ export type LifecycleHandler<TResources extends ResourceMap = ResourceMap> = HandlerMap<TResources>[keyof TResources & string] | WildcardHandler<TResources>;
42
70
  /**
43
71
  * Wraps `dataProvider` with before/after hooks per resource, without touching
44
72
  * it. For a call on resource `r`, the handlers whose `resource` is `r` or
@@ -1,54 +1,68 @@
1
- const VERBS = [
2
- "getList",
3
- "getOne",
4
- "create",
5
- "update",
6
- "delete",
7
- "getMany",
8
- "getManyReference",
9
- "updateMany",
10
- "deleteMany"
11
- ];
12
- const capitalize = (verb) => verb.charAt(0).toUpperCase() + verb.slice(1);
1
+ // src/data/with-lifecycle-callbacks.ts
2
+ /**
3
+ * Runs `befores` over `params` in order, calls the provider with the result,
4
+ * then runs `afters` over the provider's result. A rejecting `call` skips the
5
+ * `afters`. With no hooks at all the provider is called directly.
6
+ */
7
+ function runChain(befores, call, afters, params) {
8
+ if (befores.length === 0 && afters.length === 0) return call(params);
9
+ return (async () => {
10
+ let nextParams = params;
11
+ for (const before of befores) nextParams = await before(nextParams);
12
+ let result = await call(nextParams);
13
+ for (const after of afters) result = await after(result);
14
+ return result;
15
+ })();
16
+ }
17
+ /**
18
+ * Wraps `dataProvider` with before/after hooks per resource, without touching
19
+ * it. For a call on resource `r`, the handlers whose `resource` is `r` or
20
+ * `"*"` run in array order: the `before` chain feeds the provider and the
21
+ * `after` chain feeds the caller. A rejecting provider skips the `after`
22
+ * callbacks. A verb with no matching callback calls the provider directly.
23
+ */
13
24
  function withLifecycleCallbacks(dataProvider, handlers) {
14
- const loose = handlers;
15
- const wrapped = { ...dataProvider };
16
- for (const verb of VERBS) {
17
- const beforeName = `before${capitalize(verb)}`;
18
- const afterName = `after${capitalize(verb)}`;
19
- const withBefore = loose.filter((h) => typeof h[beforeName] === "function");
20
- const withAfter = loose.filter((h) => typeof h[afterName] === "function");
21
- const callProvider = (resource, params) => dataProvider[verb](resource, params);
22
- wrapped[verb] = (resource, params) => {
23
- const applies = (h) => h.resource === resource || h.resource === "*";
24
- const befores = withBefore.filter(applies);
25
- const afters = withAfter.filter(applies);
26
- if (befores.length === 0 && afters.length === 0) {
27
- return callProvider(resource, params);
28
- }
29
- return (async () => {
30
- let nextParams = params;
31
- for (const handler of befores) {
32
- nextParams = await handler[beforeName].call(
33
- handler,
34
- nextParams,
35
- dataProvider
36
- );
37
- }
38
- let result = await callProvider(resource, nextParams);
39
- for (const handler of afters) {
40
- result = await handler[afterName].call(
41
- handler,
42
- result,
43
- dataProvider
44
- );
45
- }
46
- return result;
47
- })();
48
- };
49
- }
50
- return wrapped;
25
+ const snapshot = [...handlers];
26
+ const handlersFor = (resource) => snapshot.filter((h) => h.resource === resource || h.resource === "*");
27
+ const hook = (handler, callback) => callback ? [(value) => callback.call(handler, value, dataProvider)] : [];
28
+ return {
29
+ ...dataProvider,
30
+ getList: (resource, params) => {
31
+ const matching = handlersFor(resource);
32
+ return runChain(matching.flatMap((h) => hook(h, h.beforeGetList)), (nextParams) => dataProvider.getList(resource, nextParams), matching.flatMap((h) => hook(h, h.afterGetList)), params);
33
+ },
34
+ getOne: (resource, params) => {
35
+ const matching = handlersFor(resource);
36
+ return runChain(matching.flatMap((h) => hook(h, h.beforeGetOne)), (nextParams) => dataProvider.getOne(resource, nextParams), matching.flatMap((h) => hook(h, h.afterGetOne)), params);
37
+ },
38
+ create: (resource, params) => {
39
+ const matching = handlersFor(resource);
40
+ return runChain(matching.flatMap((h) => hook(h, h.beforeCreate)), (nextParams) => dataProvider.create(resource, nextParams), matching.flatMap((h) => hook(h, h.afterCreate)), params);
41
+ },
42
+ update: (resource, params) => {
43
+ const matching = handlersFor(resource);
44
+ return runChain(matching.flatMap((h) => hook(h, h.beforeUpdate)), (nextParams) => dataProvider.update(resource, nextParams), matching.flatMap((h) => hook(h, h.afterUpdate)), params);
45
+ },
46
+ delete: (resource, params) => {
47
+ const matching = handlersFor(resource);
48
+ return runChain(matching.flatMap((h) => hook(h, h.beforeDelete)), (nextParams) => dataProvider.delete(resource, nextParams), matching.flatMap((h) => hook(h, h.afterDelete)), params);
49
+ },
50
+ getMany: (resource, params) => {
51
+ const matching = handlersFor(resource);
52
+ return runChain(matching.flatMap((h) => hook(h, h.beforeGetMany)), (nextParams) => dataProvider.getMany(resource, nextParams), matching.flatMap((h) => hook(h, h.afterGetMany)), params);
53
+ },
54
+ getManyReference: (resource, params) => {
55
+ const matching = handlersFor(resource);
56
+ return runChain(matching.flatMap((h) => hook(h, h.beforeGetManyReference)), (nextParams) => dataProvider.getManyReference(resource, nextParams), matching.flatMap((h) => hook(h, h.afterGetManyReference)), params);
57
+ },
58
+ updateMany: (resource, params) => {
59
+ const matching = handlersFor(resource);
60
+ return runChain(matching.flatMap((h) => hook(h, h.beforeUpdateMany)), (nextParams) => dataProvider.updateMany(resource, nextParams), matching.flatMap((h) => hook(h, h.afterUpdateMany)), params);
61
+ },
62
+ deleteMany: (resource, params) => {
63
+ const matching = handlersFor(resource);
64
+ return runChain(matching.flatMap((h) => hook(h, h.beforeDeleteMany)), (nextParams) => dataProvider.deleteMany(resource, nextParams), matching.flatMap((h) => hook(h, h.afterDeleteMany)), params);
65
+ }
66
+ };
51
67
  }
52
- export {
53
- withLifecycleCallbacks
54
- };
68
+ export { withLifecycleCallbacks };