@headless-tree/core 0.0.0-20250508233207 → 0.0.0-20250509160455

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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @headless-tree/core
2
2
 
3
- ## 0.0.0-20250508233207
3
+ ## 0.0.0-20250509160455
4
4
 
5
5
  ### Minor Changes
6
6
 
@@ -10,6 +10,7 @@
10
10
 
11
11
  - 29b2c64: improved key-handling behavior for hotkeys while input elements are focused (#98)
12
12
  - da1e757: fixed a bug where alt-tabbing out of browser will break hotkeys feature
13
+ - c283f52: add feature to allow async data invalidation without triggering rerenders with `invalidateItemData(optimistic: true)` (#95)
13
14
  - 29b2c64: added option to completely ignore hotkey events while input elements are focused (`ignoreHotkeysOnInput`) (#98)
14
15
 
15
16
  ## 1.0.1
@@ -11,6 +11,54 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.asyncDataLoaderFeature = void 0;
13
13
  const utils_1 = require("../../utils");
14
+ const getDataRef = (tree) => {
15
+ var _a, _b;
16
+ var _c, _d;
17
+ const dataRef = tree.getDataRef();
18
+ (_a = (_c = dataRef.current).itemData) !== null && _a !== void 0 ? _a : (_c.itemData = {});
19
+ (_b = (_d = dataRef.current).childrenIds) !== null && _b !== void 0 ? _b : (_d.childrenIds = {});
20
+ return dataRef;
21
+ };
22
+ const loadItemData = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
23
+ var _a, _b, _c;
24
+ const config = tree.getConfig();
25
+ const dataRef = getDataRef(tree);
26
+ const item = yield config.dataLoader.getItem(itemId);
27
+ dataRef.current.itemData[itemId] = item;
28
+ (_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, itemId, item);
29
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => id !== itemId));
30
+ (_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[itemId].forEach((cb) => cb());
31
+ (_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[itemId];
32
+ });
33
+ const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
34
+ var _a, _b, _c, _d, _e;
35
+ const config = tree.getConfig();
36
+ const dataRef = getDataRef(tree);
37
+ if ("getChildrenWithData" in config.dataLoader) {
38
+ const children = yield config.dataLoader.getChildrenWithData(itemId);
39
+ const childrenIds = children.map((c) => c.id);
40
+ dataRef.current.childrenIds[itemId] = childrenIds;
41
+ children.forEach(({ id, data }) => {
42
+ var _a, _b, _c;
43
+ dataRef.current.itemData[id] = data;
44
+ (_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, id, data);
45
+ (_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[id].forEach((cb) => cb());
46
+ (_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[id];
47
+ });
48
+ (_a = config.onLoadedChildren) === null || _a === void 0 ? void 0 : _a.call(config, itemId, childrenIds);
49
+ tree.rebuildTree();
50
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => !childrenIds.includes(id)));
51
+ }
52
+ else {
53
+ const childrenIds = yield config.dataLoader.getChildren(itemId);
54
+ dataRef.current.childrenIds[itemId] = childrenIds;
55
+ (_b = config.onLoadedChildren) === null || _b === void 0 ? void 0 : _b.call(config, itemId, childrenIds);
56
+ tree.rebuildTree();
57
+ }
58
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
59
+ (_d = (_c = dataRef.current.awaitingItemChildrensLoading) === null || _c === void 0 ? void 0 : _c[itemId]) === null || _d === void 0 ? void 0 : _d.forEach((cb) => cb());
60
+ (_e = dataRef.current.awaitingItemChildrensLoading) === null || _e === void 0 ? true : delete _e[itemId];
61
+ });
14
62
  exports.asyncDataLoaderFeature = {
15
63
  key: "async-data-loader",
16
64
  getInitialState: (initialState) => (Object.assign({ loadingItemData: [], loadingItemChildrens: [] }, initialState)),
@@ -49,12 +97,9 @@ exports.asyncDataLoaderFeature = {
49
97
  });
50
98
  }),
51
99
  retrieveItemData: ({ tree }, itemId) => {
52
- var _a, _b, _c, _d;
53
- var _e, _f;
100
+ var _a, _b;
54
101
  const config = tree.getConfig();
55
- const dataRef = tree.getDataRef();
56
- (_a = (_e = dataRef.current).itemData) !== null && _a !== void 0 ? _a : (_e.itemData = {});
57
- (_b = (_f = dataRef.current).childrenIds) !== null && _b !== void 0 ? _b : (_f.childrenIds = {});
102
+ const dataRef = getDataRef(tree);
58
103
  if (dataRef.current.itemData[itemId]) {
59
104
  return dataRef.current.itemData[itemId];
60
105
  }
@@ -63,25 +108,12 @@ exports.asyncDataLoaderFeature = {
63
108
  ...loadingItemData,
64
109
  itemId,
65
110
  ]);
66
- (() => __awaiter(void 0, void 0, void 0, function* () {
67
- var _a, _b, _c;
68
- const item = yield config.dataLoader.getItem(itemId);
69
- dataRef.current.itemData[itemId] = item;
70
- (_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, itemId, item);
71
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => id !== itemId));
72
- (_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[itemId].forEach((cb) => cb());
73
- (_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[itemId];
74
- }))();
111
+ loadItemData(tree, itemId);
75
112
  }
76
- return (_d = (_c = config.createLoadingItemData) === null || _c === void 0 ? void 0 : _c.call(config)) !== null && _d !== void 0 ? _d : null;
113
+ return (_b = (_a = config.createLoadingItemData) === null || _a === void 0 ? void 0 : _a.call(config)) !== null && _b !== void 0 ? _b : null;
77
114
  },
78
115
  retrieveChildrenIds: ({ tree }, itemId) => {
79
- var _a, _b;
80
- var _c, _d;
81
- const config = tree.getConfig();
82
- const dataRef = tree.getDataRef();
83
- (_a = (_c = dataRef.current).itemData) !== null && _a !== void 0 ? _a : (_c.itemData = {});
84
- (_b = (_d = dataRef.current).childrenIds) !== null && _b !== void 0 ? _b : (_d.childrenIds = {});
116
+ const dataRef = getDataRef(tree);
85
117
  if (dataRef.current.childrenIds[itemId]) {
86
118
  return dataRef.current.childrenIds[itemId];
87
119
  }
@@ -89,51 +121,32 @@ exports.asyncDataLoaderFeature = {
89
121
  return [];
90
122
  }
91
123
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
92
- (() => __awaiter(void 0, void 0, void 0, function* () {
93
- var _a, _b, _c, _d, _e;
94
- if ("getChildrenWithData" in config.dataLoader) {
95
- const children = yield config.dataLoader.getChildrenWithData(itemId);
96
- const childrenIds = children.map((c) => c.id);
97
- dataRef.current.childrenIds[itemId] = childrenIds;
98
- children.forEach(({ id, data }) => {
99
- var _a, _b, _c;
100
- dataRef.current.itemData[id] = data;
101
- (_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, id, data);
102
- (_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[id].forEach((cb) => cb());
103
- (_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[id];
104
- });
105
- (_a = config.onLoadedChildren) === null || _a === void 0 ? void 0 : _a.call(config, itemId, childrenIds);
106
- tree.rebuildTree();
107
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => !childrenIds.includes(id)));
108
- }
109
- else {
110
- const childrenIds = yield config.dataLoader.getChildren(itemId);
111
- dataRef.current.childrenIds[itemId] = childrenIds;
112
- (_b = config.onLoadedChildren) === null || _b === void 0 ? void 0 : _b.call(config, itemId, childrenIds);
113
- tree.rebuildTree();
114
- }
115
- tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
116
- (_d = (_c = dataRef.current.awaitingItemChildrensLoading) === null || _c === void 0 ? void 0 : _c[itemId]) === null || _d === void 0 ? void 0 : _d.forEach((cb) => cb());
117
- (_e = dataRef.current.awaitingItemChildrensLoading) === null || _e === void 0 ? true : delete _e[itemId];
118
- }))();
124
+ loadChildrenIds(tree, itemId);
119
125
  return [];
120
126
  },
121
127
  },
122
128
  itemInstance: {
123
129
  isLoading: ({ tree, item }) => tree.getState().loadingItemData.includes(item.getItemMeta().itemId) ||
124
130
  tree.getState().loadingItemChildrens.includes(item.getItemMeta().itemId),
125
- invalidateItemData: ({ tree, itemId }) => {
126
- var _a;
127
- const dataRef = tree.getDataRef();
128
- (_a = dataRef.current.itemData) === null || _a === void 0 ? true : delete _a[itemId];
129
- tree.retrieveItemData(itemId);
130
- },
131
- invalidateChildrenIds: ({ tree, itemId }) => {
132
- var _a;
133
- const dataRef = tree.getDataRef();
134
- (_a = dataRef.current.childrenIds) === null || _a === void 0 ? true : delete _a[itemId];
135
- tree.retrieveChildrenIds(itemId);
136
- },
131
+ invalidateItemData: (_a, optimistic_1) => __awaiter(void 0, [_a, optimistic_1], void 0, function* ({ tree, itemId }, optimistic) {
132
+ var _b;
133
+ if (!optimistic) {
134
+ (_b = getDataRef(tree).current.itemData) === null || _b === void 0 ? true : delete _b[itemId];
135
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
136
+ ...loadingItemData,
137
+ itemId,
138
+ ]);
139
+ }
140
+ yield loadItemData(tree, itemId);
141
+ }),
142
+ invalidateChildrenIds: (_a, optimistic_1) => __awaiter(void 0, [_a, optimistic_1], void 0, function* ({ tree, itemId }, optimistic) {
143
+ var _b;
144
+ if (!optimistic) {
145
+ (_b = getDataRef(tree).current.childrenIds) === null || _b === void 0 ? true : delete _b[itemId];
146
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
147
+ }
148
+ yield loadChildrenIds(tree, itemId);
149
+ }),
137
150
  updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
138
151
  const dataRef = tree.getDataRef();
139
152
  dataRef.current.childrenIds[itemId] = childrenIds;
@@ -31,9 +31,14 @@ export type AsyncDataLoaderFeatureDef<T> = {
31
31
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
32
32
  };
33
33
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
34
- /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible */
35
- invalidateItemData: () => void;
36
- invalidateChildrenIds: () => void;
34
+ /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
35
+ * @param optimistic If true, the item will not trigger a state update on `loadingItemData`, and
36
+ * the tree will continue to display the old data until the new data has loaded. */
37
+ invalidateItemData: (optimistic?: boolean) => Promise<void>;
38
+ /** Invalidate fetched children ids for item, and triggers a refetch and subsequent rerender if the item is visible
39
+ * @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
40
+ * the tree will continue to display the old data until the new data has loaded. */
41
+ invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
37
42
  updateCachedChildrenIds: (childrenIds: string[]) => void;
38
43
  isLoading: () => boolean;
39
44
  };
@@ -8,6 +8,54 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { makeStateUpdater } from "../../utils";
11
+ const getDataRef = (tree) => {
12
+ var _a, _b;
13
+ var _c, _d;
14
+ const dataRef = tree.getDataRef();
15
+ (_a = (_c = dataRef.current).itemData) !== null && _a !== void 0 ? _a : (_c.itemData = {});
16
+ (_b = (_d = dataRef.current).childrenIds) !== null && _b !== void 0 ? _b : (_d.childrenIds = {});
17
+ return dataRef;
18
+ };
19
+ const loadItemData = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
20
+ var _a, _b, _c;
21
+ const config = tree.getConfig();
22
+ const dataRef = getDataRef(tree);
23
+ const item = yield config.dataLoader.getItem(itemId);
24
+ dataRef.current.itemData[itemId] = item;
25
+ (_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, itemId, item);
26
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => id !== itemId));
27
+ (_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[itemId].forEach((cb) => cb());
28
+ (_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[itemId];
29
+ });
30
+ const loadChildrenIds = (tree, itemId) => __awaiter(void 0, void 0, void 0, function* () {
31
+ var _a, _b, _c, _d, _e;
32
+ const config = tree.getConfig();
33
+ const dataRef = getDataRef(tree);
34
+ if ("getChildrenWithData" in config.dataLoader) {
35
+ const children = yield config.dataLoader.getChildrenWithData(itemId);
36
+ const childrenIds = children.map((c) => c.id);
37
+ dataRef.current.childrenIds[itemId] = childrenIds;
38
+ children.forEach(({ id, data }) => {
39
+ var _a, _b, _c;
40
+ dataRef.current.itemData[id] = data;
41
+ (_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, id, data);
42
+ (_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[id].forEach((cb) => cb());
43
+ (_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[id];
44
+ });
45
+ (_a = config.onLoadedChildren) === null || _a === void 0 ? void 0 : _a.call(config, itemId, childrenIds);
46
+ tree.rebuildTree();
47
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => !childrenIds.includes(id)));
48
+ }
49
+ else {
50
+ const childrenIds = yield config.dataLoader.getChildren(itemId);
51
+ dataRef.current.childrenIds[itemId] = childrenIds;
52
+ (_b = config.onLoadedChildren) === null || _b === void 0 ? void 0 : _b.call(config, itemId, childrenIds);
53
+ tree.rebuildTree();
54
+ }
55
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
56
+ (_d = (_c = dataRef.current.awaitingItemChildrensLoading) === null || _c === void 0 ? void 0 : _c[itemId]) === null || _d === void 0 ? void 0 : _d.forEach((cb) => cb());
57
+ (_e = dataRef.current.awaitingItemChildrensLoading) === null || _e === void 0 ? true : delete _e[itemId];
58
+ });
11
59
  export const asyncDataLoaderFeature = {
12
60
  key: "async-data-loader",
13
61
  getInitialState: (initialState) => (Object.assign({ loadingItemData: [], loadingItemChildrens: [] }, initialState)),
@@ -46,12 +94,9 @@ export const asyncDataLoaderFeature = {
46
94
  });
47
95
  }),
48
96
  retrieveItemData: ({ tree }, itemId) => {
49
- var _a, _b, _c, _d;
50
- var _e, _f;
97
+ var _a, _b;
51
98
  const config = tree.getConfig();
52
- const dataRef = tree.getDataRef();
53
- (_a = (_e = dataRef.current).itemData) !== null && _a !== void 0 ? _a : (_e.itemData = {});
54
- (_b = (_f = dataRef.current).childrenIds) !== null && _b !== void 0 ? _b : (_f.childrenIds = {});
99
+ const dataRef = getDataRef(tree);
55
100
  if (dataRef.current.itemData[itemId]) {
56
101
  return dataRef.current.itemData[itemId];
57
102
  }
@@ -60,25 +105,12 @@ export const asyncDataLoaderFeature = {
60
105
  ...loadingItemData,
61
106
  itemId,
62
107
  ]);
63
- (() => __awaiter(void 0, void 0, void 0, function* () {
64
- var _a, _b, _c;
65
- const item = yield config.dataLoader.getItem(itemId);
66
- dataRef.current.itemData[itemId] = item;
67
- (_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, itemId, item);
68
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => id !== itemId));
69
- (_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[itemId].forEach((cb) => cb());
70
- (_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[itemId];
71
- }))();
108
+ loadItemData(tree, itemId);
72
109
  }
73
- return (_d = (_c = config.createLoadingItemData) === null || _c === void 0 ? void 0 : _c.call(config)) !== null && _d !== void 0 ? _d : null;
110
+ return (_b = (_a = config.createLoadingItemData) === null || _a === void 0 ? void 0 : _a.call(config)) !== null && _b !== void 0 ? _b : null;
74
111
  },
75
112
  retrieveChildrenIds: ({ tree }, itemId) => {
76
- var _a, _b;
77
- var _c, _d;
78
- const config = tree.getConfig();
79
- const dataRef = tree.getDataRef();
80
- (_a = (_c = dataRef.current).itemData) !== null && _a !== void 0 ? _a : (_c.itemData = {});
81
- (_b = (_d = dataRef.current).childrenIds) !== null && _b !== void 0 ? _b : (_d.childrenIds = {});
113
+ const dataRef = getDataRef(tree);
82
114
  if (dataRef.current.childrenIds[itemId]) {
83
115
  return dataRef.current.childrenIds[itemId];
84
116
  }
@@ -86,51 +118,32 @@ export const asyncDataLoaderFeature = {
86
118
  return [];
87
119
  }
88
120
  tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
89
- (() => __awaiter(void 0, void 0, void 0, function* () {
90
- var _a, _b, _c, _d, _e;
91
- if ("getChildrenWithData" in config.dataLoader) {
92
- const children = yield config.dataLoader.getChildrenWithData(itemId);
93
- const childrenIds = children.map((c) => c.id);
94
- dataRef.current.childrenIds[itemId] = childrenIds;
95
- children.forEach(({ id, data }) => {
96
- var _a, _b, _c;
97
- dataRef.current.itemData[id] = data;
98
- (_a = config.onLoadedItem) === null || _a === void 0 ? void 0 : _a.call(config, id, data);
99
- (_b = dataRef.current.awaitingItemDataLoading) === null || _b === void 0 ? void 0 : _b[id].forEach((cb) => cb());
100
- (_c = dataRef.current.awaitingItemDataLoading) === null || _c === void 0 ? true : delete _c[id];
101
- });
102
- (_a = config.onLoadedChildren) === null || _a === void 0 ? void 0 : _a.call(config, itemId, childrenIds);
103
- tree.rebuildTree();
104
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => loadingItemData.filter((id) => !childrenIds.includes(id)));
105
- }
106
- else {
107
- const childrenIds = yield config.dataLoader.getChildren(itemId);
108
- dataRef.current.childrenIds[itemId] = childrenIds;
109
- (_b = config.onLoadedChildren) === null || _b === void 0 ? void 0 : _b.call(config, itemId, childrenIds);
110
- tree.rebuildTree();
111
- }
112
- tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId));
113
- (_d = (_c = dataRef.current.awaitingItemChildrensLoading) === null || _c === void 0 ? void 0 : _c[itemId]) === null || _d === void 0 ? void 0 : _d.forEach((cb) => cb());
114
- (_e = dataRef.current.awaitingItemChildrensLoading) === null || _e === void 0 ? true : delete _e[itemId];
115
- }))();
121
+ loadChildrenIds(tree, itemId);
116
122
  return [];
117
123
  },
118
124
  },
119
125
  itemInstance: {
120
126
  isLoading: ({ tree, item }) => tree.getState().loadingItemData.includes(item.getItemMeta().itemId) ||
121
127
  tree.getState().loadingItemChildrens.includes(item.getItemMeta().itemId),
122
- invalidateItemData: ({ tree, itemId }) => {
123
- var _a;
124
- const dataRef = tree.getDataRef();
125
- (_a = dataRef.current.itemData) === null || _a === void 0 ? true : delete _a[itemId];
126
- tree.retrieveItemData(itemId);
127
- },
128
- invalidateChildrenIds: ({ tree, itemId }) => {
129
- var _a;
130
- const dataRef = tree.getDataRef();
131
- (_a = dataRef.current.childrenIds) === null || _a === void 0 ? true : delete _a[itemId];
132
- tree.retrieveChildrenIds(itemId);
133
- },
128
+ invalidateItemData: (_a, optimistic_1) => __awaiter(void 0, [_a, optimistic_1], void 0, function* ({ tree, itemId }, optimistic) {
129
+ var _b;
130
+ if (!optimistic) {
131
+ (_b = getDataRef(tree).current.itemData) === null || _b === void 0 ? true : delete _b[itemId];
132
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
133
+ ...loadingItemData,
134
+ itemId,
135
+ ]);
136
+ }
137
+ yield loadItemData(tree, itemId);
138
+ }),
139
+ invalidateChildrenIds: (_a, optimistic_1) => __awaiter(void 0, [_a, optimistic_1], void 0, function* ({ tree, itemId }, optimistic) {
140
+ var _b;
141
+ if (!optimistic) {
142
+ (_b = getDataRef(tree).current.childrenIds) === null || _b === void 0 ? true : delete _b[itemId];
143
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [...loadingItemChildrens, itemId]);
144
+ }
145
+ yield loadChildrenIds(tree, itemId);
146
+ }),
134
147
  updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
135
148
  const dataRef = tree.getDataRef();
136
149
  dataRef.current.childrenIds[itemId] = childrenIds;
@@ -31,9 +31,14 @@ export type AsyncDataLoaderFeatureDef<T> = {
31
31
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
32
32
  };
33
33
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
34
- /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible */
35
- invalidateItemData: () => void;
36
- invalidateChildrenIds: () => void;
34
+ /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
35
+ * @param optimistic If true, the item will not trigger a state update on `loadingItemData`, and
36
+ * the tree will continue to display the old data until the new data has loaded. */
37
+ invalidateItemData: (optimistic?: boolean) => Promise<void>;
38
+ /** Invalidate fetched children ids for item, and triggers a refetch and subsequent rerender if the item is visible
39
+ * @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
40
+ * the tree will continue to display the old data until the new data has loaded. */
41
+ invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
37
42
  updateCachedChildrenIds: (childrenIds: string[]) => void;
38
43
  isLoading: () => boolean;
39
44
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-tree/core",
3
- "version": "0.0.0-20250508233207",
3
+ "version": "0.0.0-20250509160455",
4
4
  "type": "module",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/esm/index.js",
@@ -86,6 +86,7 @@ describe("core-feature/selections", () => {
86
86
  suiteTree.resetBeforeEach();
87
87
 
88
88
  it("invalidates item data on item instance", async () => {
89
+ const setLoadingItemData = suiteTree.mockedHandler("setLoadingItemData");
89
90
  getItem.mockClear();
90
91
  await suiteTree.resolveAsyncVisibleItems();
91
92
  getItem.mockResolvedValueOnce("new");
@@ -93,9 +94,14 @@ describe("core-feature/selections", () => {
93
94
  await suiteTree.resolveAsyncVisibleItems();
94
95
  expect(getItem).toHaveBeenCalledWith("x1");
95
96
  expect(suiteTree.item("x1").getItemData()).toBe("new");
97
+ expect(setLoadingItemData).toBeCalledWith(["x1"]);
98
+ expect(setLoadingItemData).toBeCalledWith([]);
96
99
  });
97
100
 
98
101
  it("invalidates children ids on item instance", async () => {
102
+ const setLoadingItemChildrens = suiteTree.mockedHandler(
103
+ "setLoadingItemChildrens",
104
+ );
99
105
  getChildren.mockClear();
100
106
  await suiteTree.resolveAsyncVisibleItems();
101
107
  getChildren.mockResolvedValueOnce(["new1", "new2"]);
@@ -103,6 +109,8 @@ describe("core-feature/selections", () => {
103
109
  await suiteTree.resolveAsyncVisibleItems();
104
110
  expect(getChildren).toHaveBeenCalledWith("x1");
105
111
  suiteTree.expect.hasChildren("x1", ["new1", "new2"]);
112
+ expect(setLoadingItemChildrens).toBeCalledWith(["x1"]);
113
+ expect(setLoadingItemChildrens).toBeCalledWith([]);
106
114
  });
107
115
 
108
116
  it("doesnt call item data getter twice", async () => {
@@ -124,6 +132,34 @@ describe("core-feature/selections", () => {
124
132
  suiteTree.expect.hasChildren("x1", ["x11", "x12", "x13", "x14"]);
125
133
  expect(getChildren).toHaveBeenCalledTimes(1);
126
134
  });
135
+
136
+ it("optimistic invalidates item data on item instance", async () => {
137
+ const setLoadingItemData = suiteTree.mockedHandler("setLoadingItemData");
138
+ getItem.mockClear();
139
+ await suiteTree.resolveAsyncVisibleItems();
140
+ getItem.mockResolvedValueOnce("new");
141
+ suiteTree.item("x1").invalidateItemData(true);
142
+ await suiteTree.resolveAsyncVisibleItems();
143
+ expect(getItem).toHaveBeenCalledWith("x1");
144
+ expect(suiteTree.item("x1").getItemData()).toBe("new");
145
+ expect(setLoadingItemData).toBeCalledTimes(1);
146
+ expect(setLoadingItemData).toBeCalledWith([]);
147
+ });
148
+
149
+ it("optimistic invalidates children ids on item instance", async () => {
150
+ const setLoadingItemChildrens = suiteTree.mockedHandler(
151
+ "setLoadingItemChildrens",
152
+ );
153
+ getChildren.mockClear();
154
+ await suiteTree.resolveAsyncVisibleItems();
155
+ getChildren.mockResolvedValueOnce(["new1", "new2"]);
156
+ suiteTree.item("x1").invalidateChildrenIds(true);
157
+ await suiteTree.resolveAsyncVisibleItems();
158
+ expect(getChildren).toHaveBeenCalledWith("x1");
159
+ suiteTree.expect.hasChildren("x1", ["new1", "new2"]);
160
+ expect(setLoadingItemChildrens).toBeCalledTimes(1);
161
+ expect(setLoadingItemChildrens).toBeCalledWith([]);
162
+ });
127
163
  });
128
164
 
129
165
  describe("getChildrenWithData", () => {
@@ -1,7 +1,64 @@
1
- import { FeatureImplementation } from "../../types/core";
1
+ import { FeatureImplementation, TreeInstance } from "../../types/core";
2
2
  import { AsyncDataLoaderDataRef } from "./types";
3
3
  import { makeStateUpdater } from "../../utils";
4
4
 
5
+ const getDataRef = <T>(tree: TreeInstance<T>) => {
6
+ const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
7
+ dataRef.current.itemData ??= {};
8
+ dataRef.current.childrenIds ??= {};
9
+ return dataRef;
10
+ };
11
+
12
+ const loadItemData = async <T>(tree: TreeInstance<T>, itemId: string) => {
13
+ const config = tree.getConfig();
14
+ const dataRef = getDataRef(tree);
15
+
16
+ const item = await config.dataLoader.getItem(itemId);
17
+ dataRef.current.itemData[itemId] = item;
18
+ config.onLoadedItem?.(itemId, item);
19
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) =>
20
+ loadingItemData.filter((id) => id !== itemId),
21
+ );
22
+
23
+ dataRef.current.awaitingItemDataLoading?.[itemId].forEach((cb) => cb());
24
+ delete dataRef.current.awaitingItemDataLoading?.[itemId];
25
+ };
26
+
27
+ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
28
+ const config = tree.getConfig();
29
+ const dataRef = getDataRef(tree);
30
+
31
+ if ("getChildrenWithData" in config.dataLoader) {
32
+ const children = await config.dataLoader.getChildrenWithData(itemId);
33
+ const childrenIds = children.map((c) => c.id);
34
+ dataRef.current.childrenIds[itemId] = childrenIds;
35
+ children.forEach(({ id, data }) => {
36
+ dataRef.current.itemData[id] = data;
37
+ config.onLoadedItem?.(id, data);
38
+ dataRef.current.awaitingItemDataLoading?.[id].forEach((cb) => cb());
39
+ delete dataRef.current.awaitingItemDataLoading?.[id];
40
+ });
41
+
42
+ config.onLoadedChildren?.(itemId, childrenIds);
43
+ tree.rebuildTree();
44
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) =>
45
+ loadingItemData.filter((id) => !childrenIds.includes(id)),
46
+ );
47
+ } else {
48
+ const childrenIds = await config.dataLoader.getChildren(itemId);
49
+ dataRef.current.childrenIds[itemId] = childrenIds;
50
+ config.onLoadedChildren?.(itemId, childrenIds);
51
+ tree.rebuildTree();
52
+ }
53
+
54
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) =>
55
+ loadingItemChildrens.filter((id) => id !== itemId),
56
+ );
57
+
58
+ dataRef.current.awaitingItemChildrensLoading?.[itemId]?.forEach((cb) => cb());
59
+ delete dataRef.current.awaitingItemChildrensLoading?.[itemId];
60
+ };
61
+
5
62
  export const asyncDataLoaderFeature: FeatureImplementation = {
6
63
  key: "async-data-loader",
7
64
 
@@ -51,9 +108,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
51
108
 
52
109
  retrieveItemData: ({ tree }, itemId) => {
53
110
  const config = tree.getConfig();
54
- const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
55
- dataRef.current.itemData ??= {};
56
- dataRef.current.childrenIds ??= {};
111
+ const dataRef = getDataRef(tree);
57
112
 
58
113
  if (dataRef.current.itemData[itemId]) {
59
114
  return dataRef.current.itemData[itemId];
@@ -65,29 +120,14 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
65
120
  itemId,
66
121
  ]);
67
122
 
68
- (async () => {
69
- const item = await config.dataLoader.getItem(itemId);
70
- dataRef.current.itemData[itemId] = item;
71
- config.onLoadedItem?.(itemId, item);
72
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) =>
73
- loadingItemData.filter((id) => id !== itemId),
74
- );
75
-
76
- dataRef.current.awaitingItemDataLoading?.[itemId].forEach((cb) =>
77
- cb(),
78
- );
79
- delete dataRef.current.awaitingItemDataLoading?.[itemId];
80
- })();
123
+ loadItemData(tree, itemId);
81
124
  }
82
125
 
83
126
  return config.createLoadingItemData?.() ?? null;
84
127
  },
85
128
 
86
129
  retrieveChildrenIds: ({ tree }, itemId) => {
87
- const config = tree.getConfig();
88
- const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
89
- dataRef.current.itemData ??= {};
90
- dataRef.current.childrenIds ??= {};
130
+ const dataRef = getDataRef(tree);
91
131
  if (dataRef.current.childrenIds[itemId]) {
92
132
  return dataRef.current.childrenIds[itemId];
93
133
  }
@@ -101,41 +141,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
101
141
  (loadingItemChildrens) => [...loadingItemChildrens, itemId],
102
142
  );
103
143
 
104
- (async () => {
105
- if ("getChildrenWithData" in config.dataLoader) {
106
- const children = await config.dataLoader.getChildrenWithData(itemId);
107
- const childrenIds = children.map((c) => c.id);
108
- dataRef.current.childrenIds[itemId] = childrenIds;
109
- children.forEach(({ id, data }) => {
110
- dataRef.current.itemData[id] = data;
111
- config.onLoadedItem?.(id, data);
112
- dataRef.current.awaitingItemDataLoading?.[id].forEach((cb) => cb());
113
- delete dataRef.current.awaitingItemDataLoading?.[id];
114
- });
115
-
116
- config.onLoadedChildren?.(itemId, childrenIds);
117
- tree.rebuildTree();
118
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) =>
119
- loadingItemData.filter((id) => !childrenIds.includes(id)),
120
- );
121
- } else {
122
- const childrenIds = await config.dataLoader.getChildren(itemId);
123
- dataRef.current.childrenIds[itemId] = childrenIds;
124
- config.onLoadedChildren?.(itemId, childrenIds);
125
- tree.rebuildTree();
126
- }
127
-
128
- tree.applySubStateUpdate(
129
- "loadingItemChildrens",
130
- (loadingItemChildrens) =>
131
- loadingItemChildrens.filter((id) => id !== itemId),
132
- );
133
-
134
- dataRef.current.awaitingItemChildrensLoading?.[itemId]?.forEach((cb) =>
135
- cb(),
136
- );
137
- delete dataRef.current.awaitingItemChildrensLoading?.[itemId];
138
- })();
144
+ loadChildrenIds(tree, itemId);
139
145
 
140
146
  return [];
141
147
  },
@@ -145,15 +151,25 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
145
151
  isLoading: ({ tree, item }) =>
146
152
  tree.getState().loadingItemData.includes(item.getItemMeta().itemId) ||
147
153
  tree.getState().loadingItemChildrens.includes(item.getItemMeta().itemId),
148
- invalidateItemData: ({ tree, itemId }) => {
149
- const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
150
- delete dataRef.current.itemData?.[itemId];
151
- tree.retrieveItemData(itemId);
154
+ invalidateItemData: async ({ tree, itemId }, optimistic) => {
155
+ if (!optimistic) {
156
+ delete getDataRef(tree).current.itemData?.[itemId];
157
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
158
+ ...loadingItemData,
159
+ itemId,
160
+ ]);
161
+ }
162
+ await loadItemData(tree, itemId);
152
163
  },
153
- invalidateChildrenIds: ({ tree, itemId }) => {
154
- const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
155
- delete dataRef.current.childrenIds?.[itemId];
156
- tree.retrieveChildrenIds(itemId);
164
+ invalidateChildrenIds: async ({ tree, itemId }, optimistic) => {
165
+ if (!optimistic) {
166
+ delete getDataRef(tree).current.childrenIds?.[itemId];
167
+ tree.applySubStateUpdate(
168
+ "loadingItemChildrens",
169
+ (loadingItemChildrens) => [...loadingItemChildrens, itemId],
170
+ );
171
+ }
172
+ await loadChildrenIds(tree, itemId);
157
173
  },
158
174
  updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
159
175
  const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
@@ -36,9 +36,16 @@ export type AsyncDataLoaderFeatureDef<T> = {
36
36
  waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
37
37
  };
38
38
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
39
- /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible */
40
- invalidateItemData: () => void;
41
- invalidateChildrenIds: () => void;
39
+ /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
40
+ * @param optimistic If true, the item will not trigger a state update on `loadingItemData`, and
41
+ * the tree will continue to display the old data until the new data has loaded. */
42
+ invalidateItemData: (optimistic?: boolean) => Promise<void>;
43
+
44
+ /** Invalidate fetched children ids for item, and triggers a refetch and subsequent rerender if the item is visible
45
+ * @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
46
+ * the tree will continue to display the old data until the new data has loaded. */
47
+ invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
48
+
42
49
  updateCachedChildrenIds: (childrenIds: string[]) => void;
43
50
  isLoading: () => boolean;
44
51
  };