@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,425 +1,472 @@
1
- import { QueryObserver } from "@tanstack/query-core";
2
1
  import { getListOptions } from "../data/query-options.js";
2
+ import { removeEmpty } from "../data/remove-empty.js";
3
+ import { isRecord } from "../guards.js";
3
4
  import { createMicroStore } from "../store/micro-store.js";
4
- const DEFAULT_PER_PAGE = 10;
5
- const DEFAULT_DEBOUNCE_MS = 500;
6
- const DEFAULT_SELECT_ALL_LIMIT = 250;
7
- const DEFAULT_SORT = [{ field: "id", order: "ASC" }];
5
+ import { QueryObserver } from "@tanstack/query-core";
6
+ // src/controller/list-controller.ts
7
+ /** The `perPage` a list starts at when nothing is persisted or in the URL. The single definition: `prev-next` and both adapters read this. */
8
+ var DEFAULT_PER_PAGE = 10;
9
+ /** Debounce delay for `setFilters(..., debounced=true)` when no `debounceMs` option is given. Shared with the reference-many and choices controllers. */
10
+ var DEFAULT_DEBOUNCE_MS = 500;
11
+ var DEFAULT_SORT = [{
12
+ field: "id",
13
+ order: "ASC"
14
+ }];
8
15
  function normalizeSort(sort, fallback) {
9
- if (!sort) return fallback.map((entry) => ({ ...entry }));
10
- const entries = Array.isArray(sort) ? sort : [sort];
11
- return entries.map((entry) => ({ ...entry }));
12
- }
13
- function removeEmpty(filter) {
14
- const result = {};
15
- for (const [key, value] of Object.entries(filter)) {
16
- if (value === "" || value === null || value === void 0) continue;
17
- result[key] = value;
18
- }
19
- return result;
16
+ if (!sort) return fallback.map((entry) => ({ ...entry }));
17
+ return (Array.isArray(sort) ? sort : [sort]).map((entry) => ({ ...entry }));
20
18
  }
19
+ /**
20
+ * Returns `filter` without the value at `path`: the flat key of that name and,
21
+ * for a dotted path, the nested value, pruning parents it leaves empty. Never
22
+ * mutates `filter`. Shared with the client controller.
23
+ */
21
24
  function removeFilterPath(filter, path) {
22
- const { [path]: _flat, ...rest } = filter;
23
- const keys = path.split(".");
24
- if (keys.length === 1) return rest;
25
- const prune = (node, depth) => {
26
- const key = keys[depth];
27
- const child = node[key];
28
- if (depth === keys.length - 1) {
29
- const { [key]: _removed, ...others } = node;
30
- return others;
31
- }
32
- if (child == null || typeof child !== "object" || Array.isArray(child)) {
33
- return node;
34
- }
35
- const pruned = prune(child, depth + 1);
36
- if (pruned === child) return node;
37
- if (Object.keys(pruned).length === 0) {
38
- const { [key]: _emptied, ...others } = node;
39
- return others;
40
- }
41
- return { ...node, [key]: pruned };
42
- };
43
- return prune(rest, 0);
25
+ const { [path]: _flat, ...rest } = filter;
26
+ const keys = path.split(".");
27
+ if (keys.length === 1) return rest;
28
+ const prune = (node, depth) => {
29
+ const key = keys[depth];
30
+ if (key === void 0) return node;
31
+ const child = node[key];
32
+ if (depth === keys.length - 1) {
33
+ const { [key]: _removed, ...others } = node;
34
+ return others;
35
+ }
36
+ if (!isRecord(child)) return node;
37
+ const pruned = prune(child, depth + 1);
38
+ if (pruned === child) return node;
39
+ if (Object.keys(pruned).length === 0) {
40
+ const { [key]: _emptied, ...others } = node;
41
+ return others;
42
+ }
43
+ return {
44
+ ...node,
45
+ [key]: pruned
46
+ };
47
+ };
48
+ return prune(rest, 0);
44
49
  }
50
+ /**
51
+ * Turns a {@link ListParamsPatch} into the state slice it changes. Shared with
52
+ * the client controller so the page-reset rule lives in one place.
53
+ */
45
54
  function listParamsFromPatch(patch) {
46
- const next = {};
47
- if (patch.filter !== void 0) next.filter = removeEmpty(patch.filter);
48
- if (patch.displayedFilters !== void 0) {
49
- next.displayedFilters = { ...patch.displayedFilters };
50
- }
51
- if (patch.sort !== void 0) next.sort = normalizeSort(patch.sort, []);
52
- if (patch.perPage !== void 0) next.perPage = patch.perPage;
53
- if (patch.page !== void 0) {
54
- next.page = patch.page;
55
- } else if (patch.filter !== void 0 || patch.sort !== void 0 || patch.perPage !== void 0) {
56
- next.page = 1;
57
- }
58
- return next;
55
+ const next = {};
56
+ if (patch.filter !== void 0) next.filter = removeEmpty(patch.filter);
57
+ if (patch.displayedFilters !== void 0) next.displayedFilters = { ...patch.displayedFilters };
58
+ if (patch.sort !== void 0) next.sort = normalizeSort(patch.sort, []);
59
+ if (patch.perPage !== void 0) next.perPage = patch.perPage;
60
+ if (patch.page !== void 0) next.page = patch.page;
61
+ else if (patch.filter !== void 0 || patch.sort !== void 0 || patch.perPage !== void 0) next.page = 1;
62
+ return next;
59
63
  }
64
+ /** The notification key for a failed fetch: the error's own message, else `chelone.notification.http_error`. */
60
65
  function errorMessageKey(error) {
61
- const message = error instanceof Error ? error.message : "";
62
- return message || "chelone.notification.http_error";
66
+ return (error instanceof Error ? error.message : "") || "chelone.notification.http_error";
63
67
  }
68
+ /** Notifies a failed fetch under its message, falling back to the generic http error. */
64
69
  function notifyFetchError(notifications, error) {
65
- notifications?.notify(errorMessageKey(error), {
66
- type: "error",
67
- messageArgs: { _: String(error) }
68
- });
70
+ notifications?.notify(errorMessageKey(error), {
71
+ type: "error",
72
+ messageArgs: { _: String(error) }
73
+ });
69
74
  }
75
+ /** Warns that `selectAll` stopped at `limit` rows. A shorter result is complete and stays quiet. */
70
76
  function notifySelectAllLimit(notifications, count, limit) {
71
- if (count < limit) return;
72
- notifications?.notify("chelone.message.select_all_limit_reached", {
73
- type: "warning",
74
- messageArgs: { max: limit }
75
- });
77
+ if (count < limit) return;
78
+ notifications?.notify("chelone.message.select_all_limit_reached", {
79
+ type: "warning",
80
+ messageArgs: { max: limit }
81
+ });
76
82
  }
77
- async function runSelectAll(notifications, fetchPage, apply, isDestroyed, limit = DEFAULT_SELECT_ALL_LIMIT) {
78
- if (isDestroyed()) return;
79
- try {
80
- const rows = await fetchPage(limit);
81
- if (isDestroyed()) return;
82
- apply(rows.map((record) => record.id));
83
- notifySelectAllLimit(notifications, rows.length, limit);
84
- } catch (error) {
85
- if (isDestroyed()) return;
86
- notifyFetchError(notifications, error);
87
- }
83
+ /**
84
+ * Fetches one page with `limit` (default 250), applies its ids, then warns at
85
+ * the limit. Fetch, apply and warning failures use `notifyFetchError`.
86
+ * Destruction skips the fetch or suppresses its result and failure notification.
87
+ */
88
+ async function runSelectAll(notifications, fetchPage, apply, isDestroyed, limit = 250) {
89
+ if (isDestroyed()) return;
90
+ try {
91
+ const rows = await fetchPage(limit);
92
+ if (isDestroyed()) return;
93
+ apply(rows.map((record) => record.id));
94
+ notifySelectAllLimit(notifications, rows.length, limit);
95
+ } catch (error) {
96
+ if (isDestroyed()) return;
97
+ notifyFetchError(notifications, error);
98
+ }
88
99
  }
100
+ /**
101
+ * Notifies a distinct error OBJECT once however many results carry it (a new
102
+ * Error with the same message is a new failure and is announced). `rearm`
103
+ * forgets the last one, so a controller that wants the same error announced
104
+ * again after a success can say so.
105
+ */
89
106
  function createErrorAnnouncer(notifications) {
90
- let last = null;
91
- return {
92
- announce(error) {
93
- if (!error || error === last) return;
94
- last = error;
95
- notifyFetchError(notifications, error);
96
- },
97
- rearm() {
98
- last = null;
99
- }
100
- };
107
+ let last = null;
108
+ return {
109
+ announce(error) {
110
+ if (!error || error === last) return;
111
+ last = error;
112
+ notifyFetchError(notifications, error);
113
+ },
114
+ rearm() {
115
+ last = null;
116
+ }
117
+ };
101
118
  }
119
+ /** `hasNextPage`/`hasPreviousPage` from the provider's `pageInfo` or, failing that, the total. */
102
120
  function pageFlags(page, perPage, total, pageInfo) {
103
- if (pageInfo) {
104
- return {
105
- hasNextPage: pageInfo.hasNextPage,
106
- hasPreviousPage: pageInfo.hasPreviousPage
107
- };
108
- }
109
- if (total !== void 0) {
110
- return { hasNextPage: page * perPage < total, hasPreviousPage: page > 1 };
111
- }
112
- return { hasNextPage: void 0, hasPreviousPage: void 0 };
121
+ if (pageInfo) return {
122
+ hasNextPage: pageInfo.hasNextPage,
123
+ hasPreviousPage: pageInfo.hasPreviousPage
124
+ };
125
+ if (total !== void 0) return {
126
+ hasNextPage: page * perPage < total,
127
+ hasPreviousPage: page > 1
128
+ };
129
+ return {
130
+ hasNextPage: void 0,
131
+ hasPreviousPage: void 0
132
+ };
113
133
  }
134
+ /**
135
+ * The page an out-of-range state should move to, or undefined when it is in
136
+ * range. Only a SETTLED result may move past page 1, so a reset-induced
137
+ * refetch can never trigger another reset.
138
+ */
114
139
  function resetPageTarget(s) {
115
- if (s.page <= 0) return 1;
116
- if (s.isPending || s.isFetching) return void 0;
117
- if (s.total !== void 0) {
118
- const lastPage = Math.max(Math.ceil(s.total / s.perPage), 1);
119
- return s.page > lastPage ? lastPage : void 0;
120
- }
121
- if (s.page > 1 && s.data !== void 0 && s.data.length === 0) return 1;
122
- return void 0;
140
+ if (s.page <= 0) return 1;
141
+ if (s.isPending || s.isFetching) return void 0;
142
+ if (s.total !== void 0) {
143
+ const lastPage = Math.max(Math.ceil(s.total / s.perPage), 1);
144
+ return s.page > lastPage ? lastPage : void 0;
145
+ }
146
+ if (s.page > 1 && s.data !== void 0 && s.data.length === 0) return 1;
123
147
  }
148
+ /**
149
+ * Vanilla list controller: one QueryObserver mapped into a micro-store.
150
+ *
151
+ * Lifecycle (Task 4R): construction is INERT — pure state + closures, zero
152
+ * subscriptions, zero fetching. A controller constructed in a discarded
153
+ * React render (Suspense retry, StrictMode double render, hidden
154
+ * <Activity> pre-render) is plain garbage, never a zombie. Acquisition
155
+ * (QueryObserver + KV selection subscription) happens on the micro-store's
156
+ * FIRST subscriber; the LAST unsubscribe releases everything. Attach/detach
157
+ * cycles are re-entrant; re-attach rebuilds query options from CURRENT
158
+ * state, so params changed while detached take effect on the next attach.
159
+ * `destroy()` remains the hard imperative teardown for non-React consumers.
160
+ *
161
+ * Loop-prevention architecture rule: `observer.setOptions` is called ONLY
162
+ * from param-mutating actions — never directly from the observer result
163
+ * subscription. Out-of-range page resets go through the normal `setPage`
164
+ * action path and are guarded to run only on SETTLED results, so they
165
+ * cannot ping-pong.
166
+ */
167
+ /**
168
+ * The actions of a list controller, for an adapter to spread next to its own
169
+ * reactive state. They are the same functions across state changes.
170
+ */
124
171
  function listControllerActions(controller) {
125
- return {
126
- setPage: controller.setPage,
127
- setPerPage: controller.setPerPage,
128
- setSort: controller.setSort,
129
- setFilters: controller.setFilters,
130
- setParams: controller.setParams,
131
- showFilter: controller.showFilter,
132
- hideFilter: controller.hideFilter,
133
- select: controller.select,
134
- toggleItem: controller.toggleItem,
135
- unselectAll: controller.unselectAll,
136
- selectAll: controller.selectAll,
137
- refetch: controller.refetch
138
- };
172
+ return {
173
+ setPage: controller.setPage,
174
+ setPerPage: controller.setPerPage,
175
+ setSort: controller.setSort,
176
+ setFilters: controller.setFilters,
177
+ setParams: controller.setParams,
178
+ showFilter: controller.showFilter,
179
+ hideFilter: controller.hideFilter,
180
+ select: controller.select,
181
+ toggleItem: controller.toggleItem,
182
+ unselectAll: controller.unselectAll,
183
+ selectAll: controller.selectAll,
184
+ refetch: controller.refetch
185
+ };
139
186
  }
140
187
  function createListController(ctx, resource, options = {}) {
141
- const storeKey = options.storeKey === false ? null : options.storeKey ?? resource;
142
- const paramsKey = storeKey ? `${storeKey}.listParams` : null;
143
- const selectionKey = storeKey ? `${storeKey}.selectedIds` : null;
144
- const kv = ctx.store;
145
- const defaults = {
146
- page: 1,
147
- perPage: options.perPage ?? DEFAULT_PER_PAGE,
148
- sort: normalizeSort(options.sort, DEFAULT_SORT),
149
- filter: { ...options.filterDefaultValues ?? {} },
150
- displayedFilters: {}
151
- };
152
- const persisted = paramsKey && kv ? kv.getItem(paramsKey) : void 0;
153
- const initialParams = persisted ? {
154
- page: persisted.page ?? defaults.page,
155
- perPage: persisted.perPage ?? defaults.perPage,
156
- sort: normalizeSort(persisted.sort, defaults.sort),
157
- filter: { ...persisted.filter ?? defaults.filter },
158
- displayedFilters: { ...persisted.displayedFilters ?? {} }
159
- } : defaults;
160
- const readSelection = () => (selectionKey && kv ? kv.getItem(selectionKey) : void 0) ?? [];
161
- const store = createMicroStore(
162
- {
163
- ...initialParams,
164
- selectedIds: readSelection(),
165
- data: void 0,
166
- total: void 0,
167
- pageInfo: void 0,
168
- isPending: true,
169
- isFetching: false,
170
- error: null,
171
- hasNextPage: void 0,
172
- hasPreviousPage: void 0
173
- },
174
- // Acquisition is subscription-driven: nothing is fetched or subscribed
175
- // until the first subscriber attaches (see `acquire` below, hoisted).
176
- { onSubscribed: acquire }
177
- );
178
- let destroyed = false;
179
- store.onDestroy(() => {
180
- destroyed = true;
181
- });
182
- let observer = null;
183
- const buildOptions = () => {
184
- const s = store.getSnapshot();
185
- return {
186
- ...getListOptions(ctx, resource, {
187
- pagination: { page: s.page, perPage: s.perPage },
188
- // Pass a single object when there is exactly one key (adapter compat).
189
- sort: s.sort.length === 1 ? s.sort[0] : s.sort,
190
- filter: { ...s.filter, ...options.filter ?? {} }
191
- }),
192
- placeholderData: (prev) => prev,
193
- retry: false,
194
- enabled: options.enabled ?? true
195
- };
196
- };
197
- const persistParams = () => {
198
- if (!paramsKey || !kv) return;
199
- const s = store.getSnapshot();
200
- const params = {
201
- page: s.page,
202
- perPage: s.perPage,
203
- sort: s.sort,
204
- filter: s.filter,
205
- displayedFilters: s.displayedFilters
206
- };
207
- kv.setItem(paramsKey, params);
208
- };
209
- const applyParams = (patch) => {
210
- store.setState(patch);
211
- persistParams();
212
- observer?.setOptions(buildOptions());
213
- };
214
- const setPage = (page) => {
215
- if (destroyed) return;
216
- applyParams({ page });
217
- };
218
- const setPerPage = (perPage) => {
219
- if (destroyed) return;
220
- applyParams({ perPage, page: 1 });
221
- };
222
- const setSort = (input) => {
223
- if (destroyed) return;
224
- const current = store.getSnapshot().sort;
225
- const single = current.length === 1 ? current[0] : void 0;
226
- let next;
227
- if (Array.isArray(input)) {
228
- next = input.map((entry) => ({ ...entry }));
229
- } else if (single && single.field === input.field) {
230
- next = [
231
- { field: input.field, order: single.order === "ASC" ? "DESC" : "ASC" }
232
- ];
233
- } else {
234
- next = [{ field: input.field, order: input.order ?? "ASC" }];
235
- }
236
- applyParams({ sort: next, page: 1 });
237
- };
238
- const applyFilters = (filter, displayedFilters) => {
239
- applyParams({
240
- filter: removeEmpty(filter),
241
- displayedFilters: displayedFilters === void 0 ? store.getSnapshot().displayedFilters : { ...displayedFilters },
242
- page: 1
243
- });
244
- };
245
- const setParams = (patch) => {
246
- if (destroyed) return;
247
- const next = listParamsFromPatch(patch);
248
- if (Object.keys(next).length === 0) return;
249
- if (patch.filter !== void 0) clearDebounce();
250
- applyParams(next);
251
- };
252
- let debounceTimer;
253
- let pendingFilters;
254
- const clearDebounce = () => {
255
- if (debounceTimer !== void 0) {
256
- clearTimeout(debounceTimer);
257
- debounceTimer = void 0;
258
- }
259
- pendingFilters = void 0;
260
- };
261
- store.onDestroy(clearDebounce);
262
- const setFilters = (filter, displayedFilters, debounced = false) => {
263
- if (destroyed) return;
264
- clearDebounce();
265
- if (!debounced) {
266
- applyFilters(filter, displayedFilters);
267
- return;
268
- }
269
- pendingFilters = { filter, displayedFilters };
270
- debounceTimer = setTimeout(() => {
271
- debounceTimer = void 0;
272
- pendingFilters = void 0;
273
- if (destroyed) return;
274
- applyFilters(filter, displayedFilters);
275
- }, options.debounceMs ?? DEFAULT_DEBOUNCE_MS);
276
- };
277
- const showFilter = (name, defaultValue) => {
278
- if (destroyed) return;
279
- const s = store.getSnapshot();
280
- applyParams({
281
- // Keep the same filter reference when no default value is supplied.
282
- filter: defaultValue === void 0 ? s.filter : removeEmpty({ ...s.filter, [name]: defaultValue }),
283
- displayedFilters: { ...s.displayedFilters, [name]: true }
284
- });
285
- };
286
- const hideFilter = (name) => {
287
- if (destroyed) return;
288
- const s = store.getSnapshot();
289
- const pending = pendingFilters;
290
- clearDebounce();
291
- const filter = removeFilterPath(
292
- pending ? removeEmpty(pending.filter) : s.filter,
293
- name
294
- );
295
- const { [name]: _shown, ...displayedFilters } = pending?.displayedFilters ?? s.displayedFilters;
296
- applyParams({
297
- filter,
298
- displayedFilters,
299
- ...pending ? { page: 1 } : {}
300
- });
301
- };
302
- const writeSelection = (ids) => {
303
- if (selectionKey && kv) {
304
- kv.setItem(selectionKey, ids);
305
- if (!observer) store.setState({ selectedIds: ids });
306
- } else {
307
- store.setState({ selectedIds: ids });
308
- }
309
- };
310
- const select = (ids) => {
311
- if (destroyed) return;
312
- writeSelection([...ids]);
313
- };
314
- const toggleItem = (id) => {
315
- if (destroyed) return;
316
- const current = store.getSnapshot().selectedIds;
317
- writeSelection(
318
- current.includes(id) ? current.filter((existing) => existing !== id) : [...current, id]
319
- );
320
- };
321
- const unselectAll = () => {
322
- if (destroyed) return;
323
- writeSelection([]);
324
- };
325
- const selectAll = (opts) => runSelectAll(
326
- ctx.notifications,
327
- async (limit) => {
328
- const s = store.getSnapshot();
329
- const result = await ctx.queryClient.fetchQuery({
330
- ...getListOptions(ctx, resource, {
331
- pagination: { page: 1, perPage: limit },
332
- sort: s.sort.length === 1 ? s.sort[0] : s.sort,
333
- filter: { ...s.filter, ...options.filter ?? {} }
334
- }),
335
- retry: false
336
- });
337
- return result.data;
338
- },
339
- writeSelection,
340
- () => destroyed,
341
- opts?.limit
342
- );
343
- const refetch = () => {
344
- if (destroyed) return;
345
- void observer?.refetch();
346
- };
347
- const maybeResetPage = () => {
348
- const target = resetPageTarget(store.getSnapshot());
349
- if (target !== void 0) setPage(target);
350
- };
351
- const errors = createErrorAnnouncer(ctx.notifications);
352
- const onResult = (result) => {
353
- const list = result.data;
354
- const { page, perPage } = store.getSnapshot();
355
- store.setState({
356
- data: list?.data,
357
- total: list?.total,
358
- pageInfo: list?.pageInfo,
359
- isPending: result.isPending,
360
- isFetching: result.isFetching,
361
- error: result.error ?? null,
362
- ...pageFlags(page, perPage, list?.total, list?.pageInfo)
363
- });
364
- if (result.error) errors.announce(result.error);
365
- else errors.rearm();
366
- maybeResetPage();
367
- };
368
- function acquire() {
369
- const attached = new QueryObserver(
370
- ctx.queryClient,
371
- buildOptions()
372
- );
373
- observer = attached;
374
- const unsubscribeObserver = attached.subscribe(onResult);
375
- onResult(attached.getCurrentResult());
376
- let unsubscribeSelection;
377
- if (selectionKey && kv) {
378
- const syncKey = selectionKey;
379
- const syncSelection = () => {
380
- store.setState({
381
- selectedIds: kv.getItem(syncKey) ?? []
382
- });
383
- };
384
- unsubscribeSelection = kv.subscribe(syncKey, syncSelection);
385
- syncSelection();
386
- }
387
- return () => {
388
- unsubscribeObserver();
389
- unsubscribeSelection?.();
390
- observer = null;
391
- clearDebounce();
392
- };
393
- }
394
- return {
395
- ...store,
396
- defaultPerPage: defaults.perPage,
397
- setPage,
398
- setPerPage,
399
- setSort,
400
- setFilters,
401
- setParams,
402
- showFilter,
403
- hideFilter,
404
- select,
405
- toggleItem,
406
- unselectAll,
407
- selectAll,
408
- refetch
409
- };
188
+ const storeKey = options.storeKey === false ? null : options.storeKey ?? resource;
189
+ const paramsKey = storeKey ? `${storeKey}.listParams` : null;
190
+ const selectionKey = storeKey ? `${storeKey}.selectedIds` : null;
191
+ const kv = ctx.store;
192
+ const defaults = {
193
+ page: 1,
194
+ perPage: options.perPage ?? 10,
195
+ sort: normalizeSort(options.sort, DEFAULT_SORT),
196
+ filter: { ...options.filterDefaultValues ?? {} },
197
+ displayedFilters: {}
198
+ };
199
+ const persisted = paramsKey && kv ? kv.getItem(paramsKey) : void 0;
200
+ const initialParams = persisted ? {
201
+ page: persisted.page ?? defaults.page,
202
+ perPage: persisted.perPage ?? defaults.perPage,
203
+ sort: normalizeSort(persisted.sort, defaults.sort),
204
+ filter: { ...persisted.filter ?? defaults.filter },
205
+ displayedFilters: { ...persisted.displayedFilters ?? {} }
206
+ } : defaults;
207
+ const readSelection = () => (selectionKey && kv ? kv.getItem(selectionKey) : void 0) ?? [];
208
+ const store = createMicroStore({
209
+ ...initialParams,
210
+ selectedIds: readSelection(),
211
+ data: void 0,
212
+ total: void 0,
213
+ pageInfo: void 0,
214
+ isPending: true,
215
+ isFetching: false,
216
+ error: null,
217
+ hasNextPage: void 0,
218
+ hasPreviousPage: void 0
219
+ }, { onSubscribed: acquire });
220
+ let destroyed = false;
221
+ store.onDestroy(() => {
222
+ destroyed = true;
223
+ });
224
+ /** Non-null exactly while attached — doubles as the attached flag. */
225
+ let observer = null;
226
+ const buildOptions = () => {
227
+ const s = store.getSnapshot();
228
+ return {
229
+ ...getListOptions(ctx, resource, {
230
+ pagination: {
231
+ page: s.page,
232
+ perPage: s.perPage
233
+ },
234
+ sort: s.sort.length === 1 ? s.sort[0] : s.sort,
235
+ filter: {
236
+ ...s.filter,
237
+ ...options.filter ?? {}
238
+ }
239
+ }),
240
+ placeholderData: (prev) => prev,
241
+ retry: false,
242
+ enabled: options.enabled ?? true
243
+ };
244
+ };
245
+ const persistParams = () => {
246
+ if (!paramsKey || !kv) return;
247
+ const s = store.getSnapshot();
248
+ const params = {
249
+ page: s.page,
250
+ perPage: s.perPage,
251
+ sort: s.sort,
252
+ filter: s.filter,
253
+ displayedFilters: s.displayedFilters
254
+ };
255
+ kv.setItem(paramsKey, params);
256
+ };
257
+ /**
258
+ * The ONLY path that reconfigures the observer — param-mutating actions.
259
+ * Detached, the observer does not exist: state still updates and persists,
260
+ * and the next attach rebuilds options from CURRENT state.
261
+ */
262
+ const applyParams = (patch) => {
263
+ store.setState(patch);
264
+ persistParams();
265
+ observer?.setOptions(buildOptions());
266
+ };
267
+ const setPage = (page) => {
268
+ if (destroyed) return;
269
+ applyParams({ page });
270
+ };
271
+ const setPerPage = (perPage) => {
272
+ if (destroyed) return;
273
+ applyParams({
274
+ perPage,
275
+ page: 1
276
+ });
277
+ };
278
+ const setSort = (input) => {
279
+ if (destroyed) return;
280
+ const current = store.getSnapshot().sort;
281
+ const single = current.length === 1 ? current[0] : void 0;
282
+ let next;
283
+ if (Array.isArray(input)) next = input.map((entry) => ({ ...entry }));
284
+ else if (single && single.field === input.field) next = [{
285
+ field: input.field,
286
+ order: single.order === "ASC" ? "DESC" : "ASC"
287
+ }];
288
+ else next = [{
289
+ field: input.field,
290
+ order: input.order ?? "ASC"
291
+ }];
292
+ applyParams({
293
+ sort: next,
294
+ page: 1
295
+ });
296
+ };
297
+ const applyFilters = (filter, displayedFilters) => {
298
+ applyParams({
299
+ filter: removeEmpty(filter),
300
+ displayedFilters: displayedFilters === void 0 ? store.getSnapshot().displayedFilters : { ...displayedFilters },
301
+ page: 1
302
+ });
303
+ };
304
+ const setParams = (patch) => {
305
+ if (destroyed) return;
306
+ const next = listParamsFromPatch(patch);
307
+ if (Object.keys(next).length === 0) return;
308
+ if (patch.filter !== void 0) clearDebounce();
309
+ applyParams(next);
310
+ };
311
+ let debounceTimer;
312
+ let pendingFilters;
313
+ const clearDebounce = () => {
314
+ if (debounceTimer !== void 0) {
315
+ clearTimeout(debounceTimer);
316
+ debounceTimer = void 0;
317
+ }
318
+ pendingFilters = void 0;
319
+ };
320
+ store.onDestroy(clearDebounce);
321
+ const setFilters = (filter, displayedFilters, debounced = false) => {
322
+ if (destroyed) return;
323
+ clearDebounce();
324
+ if (!debounced) {
325
+ applyFilters(filter, displayedFilters);
326
+ return;
327
+ }
328
+ pendingFilters = {
329
+ filter,
330
+ displayedFilters
331
+ };
332
+ debounceTimer = setTimeout(() => {
333
+ debounceTimer = void 0;
334
+ pendingFilters = void 0;
335
+ if (destroyed) return;
336
+ applyFilters(filter, displayedFilters);
337
+ }, options.debounceMs ?? 500);
338
+ };
339
+ const showFilter = (name, defaultValue) => {
340
+ if (destroyed) return;
341
+ const s = store.getSnapshot();
342
+ applyParams({
343
+ filter: defaultValue === void 0 ? s.filter : removeEmpty({
344
+ ...s.filter,
345
+ [name]: defaultValue
346
+ }),
347
+ displayedFilters: {
348
+ ...s.displayedFilters,
349
+ [name]: true
350
+ }
351
+ });
352
+ };
353
+ const hideFilter = (name) => {
354
+ if (destroyed) return;
355
+ const s = store.getSnapshot();
356
+ const pending = pendingFilters;
357
+ clearDebounce();
358
+ const filter = removeFilterPath(pending ? removeEmpty(pending.filter) : s.filter, name);
359
+ const { [name]: _shown, ...displayedFilters } = pending?.displayedFilters ?? s.displayedFilters;
360
+ applyParams({
361
+ filter,
362
+ displayedFilters,
363
+ ...pending ? { page: 1 } : {}
364
+ });
365
+ };
366
+ const writeSelection = (ids) => {
367
+ if (selectionKey && kv) {
368
+ kv.setItem(selectionKey, ids);
369
+ if (!observer) store.setState({ selectedIds: ids });
370
+ } else store.setState({ selectedIds: ids });
371
+ };
372
+ const select = (ids) => {
373
+ if (destroyed) return;
374
+ writeSelection([...ids]);
375
+ };
376
+ const toggleItem = (id) => {
377
+ if (destroyed) return;
378
+ const current = store.getSnapshot().selectedIds;
379
+ writeSelection(current.includes(id) ? current.filter((existing) => existing !== id) : [...current, id]);
380
+ };
381
+ const unselectAll = () => {
382
+ if (destroyed) return;
383
+ writeSelection([]);
384
+ };
385
+ const selectAll = (opts) => runSelectAll(ctx.notifications, async (limit) => {
386
+ const s = store.getSnapshot();
387
+ return (await ctx.queryClient.fetchQuery({
388
+ ...getListOptions(ctx, resource, {
389
+ pagination: {
390
+ page: 1,
391
+ perPage: limit
392
+ },
393
+ sort: s.sort.length === 1 ? s.sort[0] : s.sort,
394
+ filter: {
395
+ ...s.filter,
396
+ ...options.filter ?? {}
397
+ }
398
+ }),
399
+ retry: false
400
+ })).data;
401
+ }, writeSelection, () => destroyed, opts?.limit);
402
+ const refetch = () => {
403
+ if (destroyed) return;
404
+ observer?.refetch();
405
+ };
406
+ const maybeResetPage = () => {
407
+ const target = resetPageTarget(store.getSnapshot());
408
+ if (target !== void 0) setPage(target);
409
+ };
410
+ const errors = createErrorAnnouncer(ctx.notifications);
411
+ const onResult = (result) => {
412
+ const list = result.data;
413
+ const { page, perPage } = store.getSnapshot();
414
+ store.setState({
415
+ data: list?.data,
416
+ total: list?.total,
417
+ pageInfo: list?.pageInfo,
418
+ isPending: result.isPending,
419
+ isFetching: result.isFetching,
420
+ error: result.error ?? null,
421
+ ...pageFlags(page, perPage, list?.total, list?.pageInfo)
422
+ });
423
+ if (result.error) errors.announce(result.error);
424
+ else errors.rearm();
425
+ maybeResetPage();
426
+ };
427
+ /**
428
+ * Attach: runs on the micro-store's FIRST subscriber (hoisted into the
429
+ * `createMicroStore` options above). Creates and subscribes the observer
430
+ * with options built from CURRENT state, arms the KV selection sync, and
431
+ * returns the teardown that the micro-store runs on last-unsubscribe AND
432
+ * on destroy-while-attached.
433
+ */
434
+ function acquire() {
435
+ const attached = new QueryObserver(ctx.queryClient, buildOptions());
436
+ observer = attached;
437
+ const unsubscribeObserver = attached.subscribe(onResult);
438
+ onResult(attached.getCurrentResult());
439
+ let unsubscribeSelection;
440
+ if (selectionKey && kv) {
441
+ const syncKey = selectionKey;
442
+ const syncSelection = () => {
443
+ store.setState({ selectedIds: kv.getItem(syncKey) ?? [] });
444
+ };
445
+ unsubscribeSelection = kv.subscribe(syncKey, syncSelection);
446
+ syncSelection();
447
+ }
448
+ return () => {
449
+ unsubscribeObserver();
450
+ unsubscribeSelection?.();
451
+ observer = null;
452
+ clearDebounce();
453
+ };
454
+ }
455
+ return {
456
+ ...store,
457
+ defaultPerPage: defaults.perPage,
458
+ setPage,
459
+ setPerPage,
460
+ setSort,
461
+ setFilters,
462
+ setParams,
463
+ showFilter,
464
+ hideFilter,
465
+ select,
466
+ toggleItem,
467
+ unselectAll,
468
+ selectAll,
469
+ refetch
470
+ };
410
471
  }
411
- export {
412
- DEFAULT_DEBOUNCE_MS,
413
- DEFAULT_SELECT_ALL_LIMIT,
414
- createErrorAnnouncer,
415
- createListController,
416
- errorMessageKey,
417
- listControllerActions,
418
- listParamsFromPatch,
419
- notifyFetchError,
420
- notifySelectAllLimit,
421
- pageFlags,
422
- removeFilterPath,
423
- resetPageTarget,
424
- runSelectAll
425
- };
472
+ export { DEFAULT_DEBOUNCE_MS, DEFAULT_PER_PAGE, createErrorAnnouncer, createListController, errorMessageKey, listControllerActions, listParamsFromPatch, notifyFetchError, notifySelectAllLimit, pageFlags, removeFilterPath, resetPageTarget, runSelectAll };