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