mtrl-addons 0.1.2 → 0.2.2
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/AI.md +28 -230
- package/CLAUDE.md +882 -0
- package/build.js +253 -24
- package/package.json +14 -4
- package/scripts/debug/vlist-selection.ts +121 -0
- package/src/components/index.ts +5 -41
- package/src/components/{list → vlist}/config.ts +66 -95
- package/src/components/vlist/constants.ts +23 -0
- package/src/components/vlist/features/api.ts +626 -0
- package/src/components/vlist/features/index.ts +10 -0
- package/src/components/vlist/features/selection.ts +436 -0
- package/src/components/vlist/features/viewport.ts +59 -0
- package/src/components/vlist/index.ts +17 -0
- package/src/components/{list → vlist}/types.ts +242 -32
- package/src/components/vlist/vlist.ts +92 -0
- package/src/core/compose/features/gestures/index.ts +227 -0
- package/src/core/compose/features/gestures/longpress.ts +383 -0
- package/src/core/compose/features/gestures/pan.ts +424 -0
- package/src/core/compose/features/gestures/pinch.ts +475 -0
- package/src/core/compose/features/gestures/rotate.ts +485 -0
- package/src/core/compose/features/gestures/swipe.ts +492 -0
- package/src/core/compose/features/gestures/tap.ts +334 -0
- package/src/core/compose/features/index.ts +2 -38
- package/src/core/compose/index.ts +13 -29
- package/src/core/gestures/index.ts +31 -0
- package/src/core/gestures/longpress.ts +68 -0
- package/src/core/gestures/manager.ts +418 -0
- package/src/core/gestures/pan.ts +48 -0
- package/src/core/gestures/pinch.ts +58 -0
- package/src/core/gestures/rotate.ts +58 -0
- package/src/core/gestures/swipe.ts +66 -0
- package/src/core/gestures/tap.ts +45 -0
- package/src/core/gestures/types.ts +387 -0
- package/src/core/gestures/utils.ts +128 -0
- package/src/core/index.ts +27 -151
- package/src/core/layout/schema.ts +153 -72
- package/src/core/layout/types.ts +5 -2
- package/src/core/viewport/constants.ts +145 -0
- package/src/core/viewport/features/base.ts +73 -0
- package/src/core/viewport/features/collection.ts +1182 -0
- package/src/core/viewport/features/events.ts +130 -0
- package/src/core/viewport/features/index.ts +20 -0
- package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
- package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
- package/src/core/viewport/features/momentum.ts +269 -0
- package/src/core/viewport/features/placeholders.ts +335 -0
- package/src/core/viewport/features/rendering.ts +962 -0
- package/src/core/viewport/features/scrollbar.ts +434 -0
- package/src/core/viewport/features/scrolling.ts +634 -0
- package/src/core/viewport/features/utils.ts +94 -0
- package/src/core/viewport/features/virtual.ts +525 -0
- package/src/core/viewport/index.ts +31 -0
- package/src/core/viewport/types.ts +133 -0
- package/src/core/viewport/utils/speed-tracker.ts +79 -0
- package/src/core/viewport/viewport.ts +265 -0
- package/src/index.ts +0 -7
- package/src/styles/components/_vlist.scss +352 -0
- package/src/styles/index.scss +1 -1
- package/test/components/vlist-selection.test.ts +240 -0
- package/test/components/vlist.test.ts +63 -0
- package/test/core/collection/adapter.test.ts +161 -0
- package/bun.lock +0 -792
- package/src/components/list/api.ts +0 -314
- package/src/components/list/constants.ts +0 -56
- package/src/components/list/features/api.ts +0 -428
- package/src/components/list/features/index.ts +0 -31
- package/src/components/list/features/list-manager.ts +0 -502
- package/src/components/list/index.ts +0 -39
- package/src/components/list/list.ts +0 -234
- package/src/core/collection/base-collection.ts +0 -100
- package/src/core/collection/collection-composer.ts +0 -178
- package/src/core/collection/collection.ts +0 -745
- package/src/core/collection/constants.ts +0 -172
- package/src/core/collection/events.ts +0 -428
- package/src/core/collection/features/api/loading.ts +0 -279
- package/src/core/collection/features/operations/data-operations.ts +0 -147
- package/src/core/collection/index.ts +0 -104
- package/src/core/collection/state.ts +0 -497
- package/src/core/collection/types.ts +0 -404
- package/src/core/compose/features/collection.ts +0 -119
- package/src/core/compose/features/selection.ts +0 -213
- package/src/core/compose/features/styling.ts +0 -108
- package/src/core/list-manager/api.ts +0 -599
- package/src/core/list-manager/config.ts +0 -593
- package/src/core/list-manager/constants.ts +0 -268
- package/src/core/list-manager/features/api.ts +0 -58
- package/src/core/list-manager/features/collection/collection.ts +0 -705
- package/src/core/list-manager/features/collection/index.ts +0 -17
- package/src/core/list-manager/features/viewport/constants.ts +0 -42
- package/src/core/list-manager/features/viewport/index.ts +0 -16
- package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
- package/src/core/list-manager/features/viewport/rendering.ts +0 -575
- package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
- package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
- package/src/core/list-manager/features/viewport/template.ts +0 -220
- package/src/core/list-manager/features/viewport/viewport.ts +0 -654
- package/src/core/list-manager/features/viewport/virtual.ts +0 -309
- package/src/core/list-manager/index.ts +0 -279
- package/src/core/list-manager/list-manager.ts +0 -206
- package/src/core/list-manager/types.ts +0 -439
- package/src/core/list-manager/utils/calculations.ts +0 -290
- package/src/core/list-manager/utils/range-calculator.ts +0 -349
- package/src/core/list-manager/utils/speed-tracker.ts +0 -273
- package/src/styles/components/_list.scss +0 -244
- package/src/types/mtrl.d.ts +0 -6
- package/test/components/list.test.ts +0 -256
- package/test/core/collection/failed-ranges.test.ts +0 -270
- package/test/core/compose/features.test.ts +0 -183
- package/test/core/list-manager/features/collection.test.ts +0 -704
- package/test/core/list-manager/features/viewport.test.ts +0 -698
- package/test/core/list-manager/list-manager.test.ts +0 -593
- package/test/core/list-manager/utils/calculations.test.ts +0 -433
- package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
- package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
- package/tsconfig.build.json +0 -23
- /package/src/components/{list → vlist}/features.ts +0 -0
- /package/src/core/{compose → viewport}/features/performance.ts +0 -0
|
@@ -1,745 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core collection implementation (Pure Data Layer)
|
|
3
|
-
*
|
|
4
|
-
* Main collection factory function that creates a pure data management
|
|
5
|
-
* collection with zero UI concerns.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
Collection,
|
|
10
|
-
CollectionConfig,
|
|
11
|
-
CollectionItem,
|
|
12
|
-
CollectionObserver,
|
|
13
|
-
CollectionUnsubscribe,
|
|
14
|
-
CollectionAdapter,
|
|
15
|
-
AdapterParams,
|
|
16
|
-
AdapterResponse,
|
|
17
|
-
CollectionPlugin,
|
|
18
|
-
AggregateOperation,
|
|
19
|
-
} from "./types";
|
|
20
|
-
import { CollectionDataEvents } from "./types";
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
createCollectionState,
|
|
24
|
-
createInitialDataState,
|
|
25
|
-
type StateStore,
|
|
26
|
-
type CollectionDataState,
|
|
27
|
-
} from "./state";
|
|
28
|
-
|
|
29
|
-
import {
|
|
30
|
-
createCollectionEventEmitter,
|
|
31
|
-
CollectionEvents,
|
|
32
|
-
createEventPayload,
|
|
33
|
-
type CollectionEventEmitter,
|
|
34
|
-
} from "./events";
|
|
35
|
-
|
|
36
|
-
import {
|
|
37
|
-
DATA_PAGINATION,
|
|
38
|
-
DATA_LOGGING,
|
|
39
|
-
COLLECTION_DEFAULTS,
|
|
40
|
-
API_ADAPTER,
|
|
41
|
-
DATA_SEARCH,
|
|
42
|
-
DATA_AGGREGATION,
|
|
43
|
-
} from "./constants";
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Creates a new collection instance (Pure Data Management)
|
|
47
|
-
*/
|
|
48
|
-
export function createCollection<T extends CollectionItem = CollectionItem>(
|
|
49
|
-
config: CollectionConfig<T> = {}
|
|
50
|
-
): Collection<T> {
|
|
51
|
-
// Initialize data state
|
|
52
|
-
const initialState = createInitialDataState({
|
|
53
|
-
items: config.items || [],
|
|
54
|
-
pageSize: COLLECTION_DEFAULTS.PAGE_SIZE,
|
|
55
|
-
initialCapacity: config.initialCapacity,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Create state store for data management
|
|
59
|
-
const stateStore = createCollectionState<T>(initialState);
|
|
60
|
-
|
|
61
|
-
// Create event emitter for data events
|
|
62
|
-
const eventEmitter = createCollectionEventEmitter<T>();
|
|
63
|
-
|
|
64
|
-
// Track if collection is destroyed
|
|
65
|
-
let isDestroyed = false;
|
|
66
|
-
|
|
67
|
-
// Track pagination state
|
|
68
|
-
let currentPage: number = COLLECTION_DEFAULTS.CURRENT_PAGE;
|
|
69
|
-
let pageSize: number = COLLECTION_DEFAULTS.PAGE_SIZE;
|
|
70
|
-
let hasMore = true;
|
|
71
|
-
let isLoadingMore = false;
|
|
72
|
-
|
|
73
|
-
// Track what data we have loaded (simple approach)
|
|
74
|
-
let totalItemsExpected = 0;
|
|
75
|
-
|
|
76
|
-
// Installed plugins
|
|
77
|
-
const installedPlugins = new Map<string, CollectionPlugin>();
|
|
78
|
-
|
|
79
|
-
// Subscribe to state changes for data event emission
|
|
80
|
-
const stateUnsubscribe = stateStore.subscribe((state) => {
|
|
81
|
-
if (!isDestroyed) {
|
|
82
|
-
// Emit data events based on state changes
|
|
83
|
-
if (state.loading && !stateStore.get().loading) {
|
|
84
|
-
eventEmitter.emit(CollectionDataEvents.LOADING_END, {
|
|
85
|
-
reason: "state-change",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (state.error) {
|
|
90
|
-
eventEmitter.emit(CollectionEvents.ERROR_OCCURRED, {
|
|
91
|
-
error: state.error,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Load more items using adapter (Pure Data Operation)
|
|
99
|
-
*/
|
|
100
|
-
const loadMoreItems = async (): Promise<AdapterResponse<T>> => {
|
|
101
|
-
if (!config.adapter || isLoadingMore) {
|
|
102
|
-
throw new Error("No adapter configured or already loading");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
isLoadingMore = true;
|
|
106
|
-
const nextPage = currentPage + 1;
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
console.log(`${DATA_LOGGING.PREFIX} Loading page ${nextPage}...`);
|
|
110
|
-
|
|
111
|
-
// Emit loading start event
|
|
112
|
-
eventEmitter.emit(
|
|
113
|
-
CollectionEvents.LOADING_START,
|
|
114
|
-
createEventPayload.loadingStart(`Loading page ${nextPage}`)
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
// Update loading state
|
|
118
|
-
stateStore.set({ loading: true });
|
|
119
|
-
|
|
120
|
-
const response = await config.adapter.read({
|
|
121
|
-
page: nextPage,
|
|
122
|
-
pageSize,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
if (response.error) {
|
|
126
|
-
throw new Error(response.error.message);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Transform and validate items if configured
|
|
130
|
-
let processedItems = response.items;
|
|
131
|
-
|
|
132
|
-
if (config.transform) {
|
|
133
|
-
processedItems = processedItems.map(config.transform);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (config.validate) {
|
|
137
|
-
processedItems = processedItems.filter(config.validate);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Update data state
|
|
141
|
-
const currentState = stateStore.get();
|
|
142
|
-
const newItems = [...currentState.items, ...processedItems];
|
|
143
|
-
|
|
144
|
-
stateStore.set({
|
|
145
|
-
items: newItems,
|
|
146
|
-
totalCount: response.meta?.total || newItems.length,
|
|
147
|
-
loading: false,
|
|
148
|
-
error: null,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Update pagination state
|
|
152
|
-
currentPage = nextPage;
|
|
153
|
-
hasMore = determineHasMore(response, processedItems.length);
|
|
154
|
-
|
|
155
|
-
// Emit data events
|
|
156
|
-
eventEmitter.emit(
|
|
157
|
-
CollectionEvents.ITEMS_LOADED,
|
|
158
|
-
createEventPayload.itemsLoaded(processedItems, response.meta)
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
eventEmitter.emit(
|
|
162
|
-
CollectionEvents.LOADING_END,
|
|
163
|
-
createEventPayload.loadingEnd(`Loaded page ${nextPage}`)
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
console.log(
|
|
167
|
-
`${DATA_LOGGING.PREFIX} Loaded page ${nextPage}, hasMore: ${hasMore}`
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
return response;
|
|
171
|
-
} catch (error) {
|
|
172
|
-
console.error(
|
|
173
|
-
`${DATA_LOGGING.PREFIX} Error loading page ${nextPage}:`,
|
|
174
|
-
error
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
const errorObj = error as Error;
|
|
178
|
-
stateStore.set({
|
|
179
|
-
loading: false,
|
|
180
|
-
error: errorObj,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
eventEmitter.emit(
|
|
184
|
-
CollectionEvents.ERROR_OCCURRED,
|
|
185
|
-
createEventPayload.errorOccurred(errorObj, { page: nextPage })
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
throw error;
|
|
189
|
-
} finally {
|
|
190
|
-
isLoadingMore = false;
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Determine if there's more data to load
|
|
196
|
-
*/
|
|
197
|
-
const determineHasMore = (
|
|
198
|
-
response: AdapterResponse<T>,
|
|
199
|
-
itemsLength: number
|
|
200
|
-
): boolean => {
|
|
201
|
-
const apiHasNext = response.meta?.hasNext;
|
|
202
|
-
const gotFullPage = itemsLength === pageSize;
|
|
203
|
-
const apiTotal = response.meta?.total;
|
|
204
|
-
|
|
205
|
-
console.log(`${DATA_LOGGING.PREFIX} determineHasMore debug:`, {
|
|
206
|
-
apiTotal,
|
|
207
|
-
apiHasNext,
|
|
208
|
-
gotFullPage,
|
|
209
|
-
itemsLength,
|
|
210
|
-
pageSize,
|
|
211
|
-
currentItemsLength: stateStore.get().items.length,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// Use multiple indicators to determine if there's more data
|
|
215
|
-
if (apiTotal !== undefined) {
|
|
216
|
-
const currentState = stateStore.get();
|
|
217
|
-
const result = currentState.items.length + itemsLength < apiTotal;
|
|
218
|
-
console.log(
|
|
219
|
-
`${DATA_LOGGING.PREFIX} Using apiTotal logic: ${currentState.items.length} + ${itemsLength} < ${apiTotal} = ${result}`
|
|
220
|
-
);
|
|
221
|
-
return result;
|
|
222
|
-
} else if (apiHasNext !== undefined) {
|
|
223
|
-
console.log(
|
|
224
|
-
`${DATA_LOGGING.PREFIX} Using apiHasNext logic: ${apiHasNext}`
|
|
225
|
-
);
|
|
226
|
-
return apiHasNext;
|
|
227
|
-
} else {
|
|
228
|
-
console.log(
|
|
229
|
-
`${DATA_LOGGING.PREFIX} Using gotFullPage logic: ${gotFullPage}`
|
|
230
|
-
);
|
|
231
|
-
return gotFullPage;
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Apply data transformations (normalize, transform, validate)
|
|
237
|
-
*/
|
|
238
|
-
const applyDataTransformations = (items: any[]): T[] => {
|
|
239
|
-
let processedItems = [...items];
|
|
240
|
-
|
|
241
|
-
// Apply normalization if configured
|
|
242
|
-
if (config.normalize) {
|
|
243
|
-
processedItems = config.normalize(processedItems);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Apply transformation if configured
|
|
247
|
-
if (config.transform) {
|
|
248
|
-
processedItems = processedItems.map(config.transform);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Apply validation if configured
|
|
252
|
-
if (config.validate) {
|
|
253
|
-
processedItems = processedItems.filter(config.validate);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return processedItems;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Apply search to items
|
|
261
|
-
*/
|
|
262
|
-
const applySearch = (items: T[], query: string, fields: string[]): T[] => {
|
|
263
|
-
if (!query || fields.length === 0) {
|
|
264
|
-
return items;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const searchQuery = query.toLowerCase();
|
|
268
|
-
return items.filter((item) => {
|
|
269
|
-
return fields.some((field) => {
|
|
270
|
-
const fieldValue = (item as any)[field];
|
|
271
|
-
return (
|
|
272
|
-
fieldValue &&
|
|
273
|
-
fieldValue.toString().toLowerCase().includes(searchQuery)
|
|
274
|
-
);
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
// Return the pure data collection interface
|
|
280
|
-
const collection: Collection<T> = {
|
|
281
|
-
// Data operations
|
|
282
|
-
getItems(): T[] {
|
|
283
|
-
return stateStore.get().items;
|
|
284
|
-
},
|
|
285
|
-
|
|
286
|
-
getItem(id: string): T | undefined {
|
|
287
|
-
return stateStore.get().items.find((item) => item.id === id);
|
|
288
|
-
},
|
|
289
|
-
|
|
290
|
-
async addItems(items: T[]): Promise<T[]> {
|
|
291
|
-
const processedItems = applyDataTransformations(items);
|
|
292
|
-
const currentState = stateStore.get();
|
|
293
|
-
|
|
294
|
-
// Add to existing items
|
|
295
|
-
const newItems = [...currentState.items, ...processedItems];
|
|
296
|
-
|
|
297
|
-
stateStore.set({
|
|
298
|
-
items: newItems,
|
|
299
|
-
totalCount: newItems.length,
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Emit data event
|
|
303
|
-
eventEmitter.emit(
|
|
304
|
-
CollectionEvents.ITEMS_ADDED,
|
|
305
|
-
createEventPayload.itemsAdded(processedItems, [
|
|
306
|
-
currentState.items.length,
|
|
307
|
-
])
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
return processedItems;
|
|
311
|
-
},
|
|
312
|
-
|
|
313
|
-
async updateItems(items: Partial<T>[]): Promise<T[]> {
|
|
314
|
-
const currentState = stateStore.get();
|
|
315
|
-
const updatedItems: T[] = [];
|
|
316
|
-
|
|
317
|
-
// Update existing items
|
|
318
|
-
const newItems = currentState.items.map((item) => {
|
|
319
|
-
const update = items.find((u) => u.id === item.id);
|
|
320
|
-
if (update) {
|
|
321
|
-
const updatedItem = { ...item, ...update };
|
|
322
|
-
updatedItems.push(updatedItem);
|
|
323
|
-
return updatedItem;
|
|
324
|
-
}
|
|
325
|
-
return item;
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
stateStore.set({ items: newItems });
|
|
329
|
-
|
|
330
|
-
// Emit data event
|
|
331
|
-
eventEmitter.emit(
|
|
332
|
-
CollectionEvents.ITEMS_UPDATED,
|
|
333
|
-
createEventPayload.itemsUpdated(updatedItems, [])
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
return updatedItems;
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
async removeItems(ids: string[]): Promise<void> {
|
|
340
|
-
const currentState = stateStore.get();
|
|
341
|
-
|
|
342
|
-
// Remove items by ID
|
|
343
|
-
const newItems = currentState.items.filter(
|
|
344
|
-
(item) => !ids.includes(item.id)
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
stateStore.set({
|
|
348
|
-
items: newItems,
|
|
349
|
-
totalCount: newItems.length,
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
// Emit data event
|
|
353
|
-
eventEmitter.emit(
|
|
354
|
-
CollectionEvents.ITEMS_REMOVED,
|
|
355
|
-
createEventPayload.itemsRemoved(ids)
|
|
356
|
-
);
|
|
357
|
-
},
|
|
358
|
-
|
|
359
|
-
async clearItems(): Promise<void> {
|
|
360
|
-
stateStore.set({
|
|
361
|
-
items: [],
|
|
362
|
-
filteredItems: [],
|
|
363
|
-
totalCount: 0,
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// Reset pagination
|
|
367
|
-
currentPage = COLLECTION_DEFAULTS.CURRENT_PAGE;
|
|
368
|
-
hasMore = true;
|
|
369
|
-
|
|
370
|
-
// Emit data event
|
|
371
|
-
eventEmitter.emit(CollectionEvents.ITEMS_CLEARED, {});
|
|
372
|
-
},
|
|
373
|
-
|
|
374
|
-
// Data queries
|
|
375
|
-
filter(predicate: (item: T) => boolean): T[] {
|
|
376
|
-
return stateStore.get().items.filter(predicate);
|
|
377
|
-
},
|
|
378
|
-
|
|
379
|
-
sort(compareFn: (a: T, b: T) => number): T[] {
|
|
380
|
-
const items = [...stateStore.get().items];
|
|
381
|
-
return items.sort(compareFn);
|
|
382
|
-
},
|
|
383
|
-
|
|
384
|
-
search(query: string, fields: string[] = DATA_SEARCH.DEFAULT_FIELDS): T[] {
|
|
385
|
-
if (query.length < DATA_SEARCH.MIN_SEARCH_LENGTH) {
|
|
386
|
-
return stateStore.get().items;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return applySearch(stateStore.get().items, query, fields);
|
|
390
|
-
},
|
|
391
|
-
|
|
392
|
-
aggregate(operations: AggregateOperation[]): any {
|
|
393
|
-
const items = stateStore.get().items;
|
|
394
|
-
const results: Record<string, any> = {};
|
|
395
|
-
|
|
396
|
-
operations.forEach((op) => {
|
|
397
|
-
const alias = op.alias || `${op.operation}_${op.field}`;
|
|
398
|
-
|
|
399
|
-
switch (op.operation) {
|
|
400
|
-
case "count":
|
|
401
|
-
results[alias] = items.length;
|
|
402
|
-
break;
|
|
403
|
-
case "sum":
|
|
404
|
-
results[alias] = items.reduce(
|
|
405
|
-
(sum, item) => sum + ((item as any)[op.field] || 0),
|
|
406
|
-
0
|
|
407
|
-
);
|
|
408
|
-
break;
|
|
409
|
-
case "avg":
|
|
410
|
-
const sum = items.reduce(
|
|
411
|
-
(sum, item) => sum + ((item as any)[op.field] || 0),
|
|
412
|
-
0
|
|
413
|
-
);
|
|
414
|
-
results[alias] = items.length > 0 ? sum / items.length : 0;
|
|
415
|
-
break;
|
|
416
|
-
case "min":
|
|
417
|
-
results[alias] = Math.min(
|
|
418
|
-
...items.map((item) => (item as any)[op.field] || 0)
|
|
419
|
-
);
|
|
420
|
-
break;
|
|
421
|
-
case "max":
|
|
422
|
-
results[alias] = Math.max(
|
|
423
|
-
...items.map((item) => (item as any)[op.field] || 0)
|
|
424
|
-
);
|
|
425
|
-
break;
|
|
426
|
-
case "distinct":
|
|
427
|
-
results[alias] = [
|
|
428
|
-
...new Set(items.map((item) => (item as any)[op.field])),
|
|
429
|
-
];
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
return results;
|
|
435
|
-
},
|
|
436
|
-
|
|
437
|
-
// Data loading
|
|
438
|
-
async loadPage(page: number): Promise<AdapterResponse<T>> {
|
|
439
|
-
if (!config.adapter) {
|
|
440
|
-
throw new Error("No adapter configured");
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Calculate what data range this page represents
|
|
444
|
-
const startIndex = (page - 1) * pageSize;
|
|
445
|
-
const endIndex = startIndex + pageSize - 1;
|
|
446
|
-
|
|
447
|
-
const currentState = stateStore.get();
|
|
448
|
-
const currentItems = currentState.items;
|
|
449
|
-
|
|
450
|
-
// Check if we already have this data
|
|
451
|
-
const hasData = currentItems.length > endIndex;
|
|
452
|
-
|
|
453
|
-
if (hasData) {
|
|
454
|
-
console.log(
|
|
455
|
-
`${DATA_LOGGING.PREFIX} Data already loaded for page ${page} (items ${startIndex}-${endIndex})`
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
// Return the subset we already have
|
|
459
|
-
const pageItems = currentItems.slice(startIndex, startIndex + pageSize);
|
|
460
|
-
|
|
461
|
-
currentPage = page;
|
|
462
|
-
|
|
463
|
-
// Emit items loaded event
|
|
464
|
-
eventEmitter.emit(
|
|
465
|
-
CollectionEvents.ITEMS_LOADED,
|
|
466
|
-
createEventPayload.itemsLoaded(pageItems, {
|
|
467
|
-
page,
|
|
468
|
-
total: totalItemsExpected,
|
|
469
|
-
})
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
return {
|
|
473
|
-
items: pageItems,
|
|
474
|
-
meta: {
|
|
475
|
-
total: totalItemsExpected,
|
|
476
|
-
page: page,
|
|
477
|
-
hasNext: hasMore,
|
|
478
|
-
hasPrev: page > 1,
|
|
479
|
-
},
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
console.log(`${DATA_LOGGING.PREFIX} Loading page ${page} from adapter`);
|
|
484
|
-
|
|
485
|
-
try {
|
|
486
|
-
eventEmitter.emit(
|
|
487
|
-
CollectionEvents.LOADING_START,
|
|
488
|
-
createEventPayload.loadingStart(`Loading page ${page}`)
|
|
489
|
-
);
|
|
490
|
-
|
|
491
|
-
stateStore.set({ loading: true });
|
|
492
|
-
|
|
493
|
-
const response = await config.adapter.read({
|
|
494
|
-
page,
|
|
495
|
-
pageSize,
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
if (response.error) {
|
|
499
|
-
throw new Error(response.error.message);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const processedItems = applyDataTransformations(response.items);
|
|
503
|
-
|
|
504
|
-
// Add to existing items (append if sequential)
|
|
505
|
-
let updatedItems: T[];
|
|
506
|
-
if (startIndex === currentItems.length) {
|
|
507
|
-
// Sequential loading - just append
|
|
508
|
-
updatedItems = [...currentItems, ...processedItems];
|
|
509
|
-
} else {
|
|
510
|
-
// Non-sequential - need to handle gaps (could be complex)
|
|
511
|
-
updatedItems = [...currentItems, ...processedItems];
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
totalItemsExpected = response.meta?.total || updatedItems.length;
|
|
515
|
-
|
|
516
|
-
stateStore.set({
|
|
517
|
-
items: updatedItems,
|
|
518
|
-
totalCount: totalItemsExpected,
|
|
519
|
-
loading: false,
|
|
520
|
-
error: null,
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
currentPage = page;
|
|
524
|
-
hasMore = determineHasMore(response, processedItems.length);
|
|
525
|
-
|
|
526
|
-
eventEmitter.emit(
|
|
527
|
-
CollectionEvents.ITEMS_LOADED,
|
|
528
|
-
createEventPayload.itemsLoaded(processedItems, response.meta)
|
|
529
|
-
);
|
|
530
|
-
|
|
531
|
-
return response;
|
|
532
|
-
} catch (error) {
|
|
533
|
-
const errorObj = error as Error;
|
|
534
|
-
stateStore.set({
|
|
535
|
-
loading: false,
|
|
536
|
-
error: errorObj,
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
eventEmitter.emit(
|
|
540
|
-
CollectionEvents.ERROR_OCCURRED,
|
|
541
|
-
createEventPayload.errorOccurred(errorObj, { page })
|
|
542
|
-
);
|
|
543
|
-
|
|
544
|
-
throw error;
|
|
545
|
-
}
|
|
546
|
-
},
|
|
547
|
-
|
|
548
|
-
async loadMore(): Promise<AdapterResponse<T>> {
|
|
549
|
-
return loadMoreItems();
|
|
550
|
-
},
|
|
551
|
-
|
|
552
|
-
async refresh(): Promise<AdapterResponse<T>> {
|
|
553
|
-
// Reset to first page and reload
|
|
554
|
-
currentPage = 0; // Will be incremented to 1 in loadMoreItems
|
|
555
|
-
hasMore = true;
|
|
556
|
-
|
|
557
|
-
stateStore.set({
|
|
558
|
-
items: [],
|
|
559
|
-
filteredItems: [],
|
|
560
|
-
totalCount: 0,
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
return loadMoreItems();
|
|
564
|
-
},
|
|
565
|
-
|
|
566
|
-
async prefetch(pages: number[]): Promise<void> {
|
|
567
|
-
if (!config.adapter) {
|
|
568
|
-
throw new Error("No adapter configured");
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
eventEmitter.emit(
|
|
572
|
-
CollectionEvents.PREFETCH_START,
|
|
573
|
-
createEventPayload.prefetchStart(pages)
|
|
574
|
-
);
|
|
575
|
-
|
|
576
|
-
// Prefetch pages in parallel
|
|
577
|
-
const prefetchPromises = pages.map(async (page) => {
|
|
578
|
-
try {
|
|
579
|
-
const response = await config.adapter!.read({
|
|
580
|
-
page,
|
|
581
|
-
pageSize,
|
|
582
|
-
});
|
|
583
|
-
return response.items;
|
|
584
|
-
} catch (error) {
|
|
585
|
-
console.warn(
|
|
586
|
-
`${DATA_LOGGING.PREFIX} Prefetch failed for page ${page}:`,
|
|
587
|
-
error
|
|
588
|
-
);
|
|
589
|
-
return [];
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
try {
|
|
594
|
-
const results = await Promise.all(prefetchPromises);
|
|
595
|
-
const allItems = results.flat();
|
|
596
|
-
|
|
597
|
-
eventEmitter.emit(
|
|
598
|
-
CollectionEvents.PREFETCH_COMPLETE,
|
|
599
|
-
createEventPayload.prefetchComplete(allItems, pages)
|
|
600
|
-
);
|
|
601
|
-
} catch (error) {
|
|
602
|
-
console.error(`${DATA_LOGGING.PREFIX} Prefetch error:`, error);
|
|
603
|
-
}
|
|
604
|
-
},
|
|
605
|
-
|
|
606
|
-
// Data state
|
|
607
|
-
getSize(): number {
|
|
608
|
-
return stateStore.get().items.length;
|
|
609
|
-
},
|
|
610
|
-
|
|
611
|
-
getTotalCount(): number {
|
|
612
|
-
return stateStore.get().totalCount;
|
|
613
|
-
},
|
|
614
|
-
|
|
615
|
-
isLoading(): boolean {
|
|
616
|
-
return stateStore.get().loading;
|
|
617
|
-
},
|
|
618
|
-
|
|
619
|
-
getError(): Error | null {
|
|
620
|
-
return stateStore.get().error;
|
|
621
|
-
},
|
|
622
|
-
|
|
623
|
-
hasNext(): boolean {
|
|
624
|
-
return hasMore;
|
|
625
|
-
},
|
|
626
|
-
|
|
627
|
-
getCurrentPage(): number {
|
|
628
|
-
return currentPage;
|
|
629
|
-
},
|
|
630
|
-
|
|
631
|
-
// Data persistence (Plugin methods - implemented by plugins)
|
|
632
|
-
async save(): Promise<void> {
|
|
633
|
-
// Will be implemented by persistence plugins
|
|
634
|
-
console.warn(
|
|
635
|
-
`${DATA_LOGGING.PREFIX} Save method requires persistence plugin`
|
|
636
|
-
);
|
|
637
|
-
},
|
|
638
|
-
|
|
639
|
-
async load(): Promise<void> {
|
|
640
|
-
// Will be implemented by persistence plugins
|
|
641
|
-
console.warn(
|
|
642
|
-
`${DATA_LOGGING.PREFIX} Load method requires persistence plugin`
|
|
643
|
-
);
|
|
644
|
-
},
|
|
645
|
-
|
|
646
|
-
async clearCache(): Promise<void> {
|
|
647
|
-
// Clear the multi-page cache
|
|
648
|
-
pageCache.clear();
|
|
649
|
-
console.log(`${DATA_LOGGING.PREFIX} Page cache cleared`);
|
|
650
|
-
|
|
651
|
-
// Emit cache cleared event
|
|
652
|
-
eventEmitter.emit(CollectionEvents.CACHE_CLEARED, {
|
|
653
|
-
reason: "manual-clear",
|
|
654
|
-
timestamp: Date.now(),
|
|
655
|
-
});
|
|
656
|
-
},
|
|
657
|
-
|
|
658
|
-
async sync(): Promise<void> {
|
|
659
|
-
// Will be implemented by sync plugins
|
|
660
|
-
console.warn(`${DATA_LOGGING.PREFIX} Sync method requires sync plugin`);
|
|
661
|
-
},
|
|
662
|
-
|
|
663
|
-
// Events (data events only)
|
|
664
|
-
subscribe(observer: CollectionObserver<T>): CollectionUnsubscribe {
|
|
665
|
-
return eventEmitter.subscribe(observer);
|
|
666
|
-
},
|
|
667
|
-
|
|
668
|
-
emit(event: CollectionDataEvents, data: any): void {
|
|
669
|
-
eventEmitter.emit(event, data);
|
|
670
|
-
},
|
|
671
|
-
|
|
672
|
-
// Lifecycle
|
|
673
|
-
destroy(): void {
|
|
674
|
-
if (isDestroyed) return;
|
|
675
|
-
|
|
676
|
-
// Unsubscribe from state changes
|
|
677
|
-
stateUnsubscribe();
|
|
678
|
-
|
|
679
|
-
// Destroy state store
|
|
680
|
-
stateStore.destroy();
|
|
681
|
-
|
|
682
|
-
// Destroy event emitter
|
|
683
|
-
eventEmitter.destroy();
|
|
684
|
-
|
|
685
|
-
// Uninstall all plugins
|
|
686
|
-
installedPlugins.forEach((plugin) => {
|
|
687
|
-
if (plugin.uninstall) {
|
|
688
|
-
plugin.uninstall(collection);
|
|
689
|
-
}
|
|
690
|
-
});
|
|
691
|
-
installedPlugins.clear();
|
|
692
|
-
|
|
693
|
-
isDestroyed = true;
|
|
694
|
-
console.log(`${DATA_LOGGING.PREFIX} Collection destroyed`);
|
|
695
|
-
},
|
|
696
|
-
|
|
697
|
-
// Plugin system for data features
|
|
698
|
-
use(plugin: CollectionPlugin): Collection<T> {
|
|
699
|
-
if (isDestroyed) {
|
|
700
|
-
throw new Error("Cannot add plugin to destroyed collection");
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (installedPlugins.has(plugin.name)) {
|
|
704
|
-
console.warn(
|
|
705
|
-
`${DATA_LOGGING.PREFIX} Plugin ${plugin.name} already installed`
|
|
706
|
-
);
|
|
707
|
-
return collection;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Check dependencies
|
|
711
|
-
if (plugin.dependencies) {
|
|
712
|
-
const missingDeps = plugin.dependencies.filter(
|
|
713
|
-
(dep) => !installedPlugins.has(dep)
|
|
714
|
-
);
|
|
715
|
-
if (missingDeps.length > 0) {
|
|
716
|
-
throw new Error(
|
|
717
|
-
`Plugin ${plugin.name} requires dependencies: ${missingDeps.join(
|
|
718
|
-
", "
|
|
719
|
-
)}`
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
try {
|
|
725
|
-
plugin.install(collection, {});
|
|
726
|
-
installedPlugins.set(plugin.name, plugin);
|
|
727
|
-
console.log(
|
|
728
|
-
`${DATA_LOGGING.PREFIX} Plugin ${plugin.name} v${plugin.version} installed`
|
|
729
|
-
);
|
|
730
|
-
} catch (error) {
|
|
731
|
-
console.error(
|
|
732
|
-
`${DATA_LOGGING.PREFIX} Failed to install plugin ${plugin.name}:`,
|
|
733
|
-
error
|
|
734
|
-
);
|
|
735
|
-
throw error;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
return collection;
|
|
739
|
-
},
|
|
740
|
-
|
|
741
|
-
// NO UI methods: scrollTo, render, setTemplate, element, container, etc.
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
return collection;
|
|
745
|
-
}
|