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.
Files changed (117) hide show
  1. package/AI.md +28 -230
  2. package/CLAUDE.md +882 -0
  3. package/build.js +253 -24
  4. package/package.json +14 -4
  5. package/scripts/debug/vlist-selection.ts +121 -0
  6. package/src/components/index.ts +5 -41
  7. package/src/components/{list → vlist}/config.ts +66 -95
  8. package/src/components/vlist/constants.ts +23 -0
  9. package/src/components/vlist/features/api.ts +626 -0
  10. package/src/components/vlist/features/index.ts +10 -0
  11. package/src/components/vlist/features/selection.ts +436 -0
  12. package/src/components/vlist/features/viewport.ts +59 -0
  13. package/src/components/vlist/index.ts +17 -0
  14. package/src/components/{list → vlist}/types.ts +242 -32
  15. package/src/components/vlist/vlist.ts +92 -0
  16. package/src/core/compose/features/gestures/index.ts +227 -0
  17. package/src/core/compose/features/gestures/longpress.ts +383 -0
  18. package/src/core/compose/features/gestures/pan.ts +424 -0
  19. package/src/core/compose/features/gestures/pinch.ts +475 -0
  20. package/src/core/compose/features/gestures/rotate.ts +485 -0
  21. package/src/core/compose/features/gestures/swipe.ts +492 -0
  22. package/src/core/compose/features/gestures/tap.ts +334 -0
  23. package/src/core/compose/features/index.ts +2 -38
  24. package/src/core/compose/index.ts +13 -29
  25. package/src/core/gestures/index.ts +31 -0
  26. package/src/core/gestures/longpress.ts +68 -0
  27. package/src/core/gestures/manager.ts +418 -0
  28. package/src/core/gestures/pan.ts +48 -0
  29. package/src/core/gestures/pinch.ts +58 -0
  30. package/src/core/gestures/rotate.ts +58 -0
  31. package/src/core/gestures/swipe.ts +66 -0
  32. package/src/core/gestures/tap.ts +45 -0
  33. package/src/core/gestures/types.ts +387 -0
  34. package/src/core/gestures/utils.ts +128 -0
  35. package/src/core/index.ts +27 -151
  36. package/src/core/layout/schema.ts +153 -72
  37. package/src/core/layout/types.ts +5 -2
  38. package/src/core/viewport/constants.ts +145 -0
  39. package/src/core/viewport/features/base.ts +73 -0
  40. package/src/core/viewport/features/collection.ts +1182 -0
  41. package/src/core/viewport/features/events.ts +130 -0
  42. package/src/core/viewport/features/index.ts +20 -0
  43. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
  44. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  45. package/src/core/viewport/features/momentum.ts +269 -0
  46. package/src/core/viewport/features/placeholders.ts +335 -0
  47. package/src/core/viewport/features/rendering.ts +962 -0
  48. package/src/core/viewport/features/scrollbar.ts +434 -0
  49. package/src/core/viewport/features/scrolling.ts +634 -0
  50. package/src/core/viewport/features/utils.ts +94 -0
  51. package/src/core/viewport/features/virtual.ts +525 -0
  52. package/src/core/viewport/index.ts +31 -0
  53. package/src/core/viewport/types.ts +133 -0
  54. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  55. package/src/core/viewport/viewport.ts +265 -0
  56. package/src/index.ts +0 -7
  57. package/src/styles/components/_vlist.scss +352 -0
  58. package/src/styles/index.scss +1 -1
  59. package/test/components/vlist-selection.test.ts +240 -0
  60. package/test/components/vlist.test.ts +63 -0
  61. package/test/core/collection/adapter.test.ts +161 -0
  62. package/bun.lock +0 -792
  63. package/src/components/list/api.ts +0 -314
  64. package/src/components/list/constants.ts +0 -56
  65. package/src/components/list/features/api.ts +0 -428
  66. package/src/components/list/features/index.ts +0 -31
  67. package/src/components/list/features/list-manager.ts +0 -502
  68. package/src/components/list/index.ts +0 -39
  69. package/src/components/list/list.ts +0 -234
  70. package/src/core/collection/base-collection.ts +0 -100
  71. package/src/core/collection/collection-composer.ts +0 -178
  72. package/src/core/collection/collection.ts +0 -745
  73. package/src/core/collection/constants.ts +0 -172
  74. package/src/core/collection/events.ts +0 -428
  75. package/src/core/collection/features/api/loading.ts +0 -279
  76. package/src/core/collection/features/operations/data-operations.ts +0 -147
  77. package/src/core/collection/index.ts +0 -104
  78. package/src/core/collection/state.ts +0 -497
  79. package/src/core/collection/types.ts +0 -404
  80. package/src/core/compose/features/collection.ts +0 -119
  81. package/src/core/compose/features/selection.ts +0 -213
  82. package/src/core/compose/features/styling.ts +0 -108
  83. package/src/core/list-manager/api.ts +0 -599
  84. package/src/core/list-manager/config.ts +0 -593
  85. package/src/core/list-manager/constants.ts +0 -268
  86. package/src/core/list-manager/features/api.ts +0 -58
  87. package/src/core/list-manager/features/collection/collection.ts +0 -705
  88. package/src/core/list-manager/features/collection/index.ts +0 -17
  89. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  90. package/src/core/list-manager/features/viewport/index.ts +0 -16
  91. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  92. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  93. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  94. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  95. package/src/core/list-manager/features/viewport/template.ts +0 -220
  96. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  97. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  98. package/src/core/list-manager/index.ts +0 -279
  99. package/src/core/list-manager/list-manager.ts +0 -206
  100. package/src/core/list-manager/types.ts +0 -439
  101. package/src/core/list-manager/utils/calculations.ts +0 -290
  102. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  103. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  104. package/src/styles/components/_list.scss +0 -244
  105. package/src/types/mtrl.d.ts +0 -6
  106. package/test/components/list.test.ts +0 -256
  107. package/test/core/collection/failed-ranges.test.ts +0 -270
  108. package/test/core/compose/features.test.ts +0 -183
  109. package/test/core/list-manager/features/collection.test.ts +0 -704
  110. package/test/core/list-manager/features/viewport.test.ts +0 -698
  111. package/test/core/list-manager/list-manager.test.ts +0 -593
  112. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  113. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  114. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  115. package/tsconfig.build.json +0 -23
  116. /package/src/components/{list → vlist}/features.ts +0 -0
  117. /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
- }