mtrl-addons 0.1.0 → 0.1.1

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 (92) hide show
  1. package/.cursorrules +117 -0
  2. package/AI.md +241 -0
  3. package/build.js +170 -0
  4. package/bun.lock +792 -0
  5. package/index.ts +7 -0
  6. package/package.json +10 -17
  7. package/scripts/analyze-orphaned-functions.ts +387 -0
  8. package/src/components/index.ts +45 -0
  9. package/src/components/list/api.ts +314 -0
  10. package/src/components/list/config.ts +352 -0
  11. package/src/components/list/constants.ts +56 -0
  12. package/src/components/list/features/api.ts +428 -0
  13. package/src/components/list/features/index.ts +31 -0
  14. package/src/components/list/features/list-manager.ts +502 -0
  15. package/src/components/list/features.ts +112 -0
  16. package/src/components/list/index.ts +39 -0
  17. package/src/components/list/list.ts +234 -0
  18. package/src/components/list/types.ts +513 -0
  19. package/src/core/collection/base-collection.ts +100 -0
  20. package/src/core/collection/collection-composer.ts +178 -0
  21. package/src/core/collection/collection.ts +745 -0
  22. package/src/core/collection/constants.ts +172 -0
  23. package/src/core/collection/events.ts +428 -0
  24. package/src/core/collection/features/api/loading.ts +279 -0
  25. package/src/core/collection/features/operations/data-operations.ts +147 -0
  26. package/src/core/collection/index.ts +104 -0
  27. package/src/core/collection/state.ts +497 -0
  28. package/src/core/collection/types.ts +404 -0
  29. package/src/core/compose/features/collection.ts +119 -0
  30. package/src/core/compose/features/index.ts +39 -0
  31. package/src/core/compose/features/performance.ts +161 -0
  32. package/src/core/compose/features/selection.ts +213 -0
  33. package/src/core/compose/features/styling.ts +108 -0
  34. package/src/core/compose/index.ts +31 -0
  35. package/src/core/index.ts +167 -0
  36. package/src/core/layout/config.ts +102 -0
  37. package/src/core/layout/index.ts +168 -0
  38. package/src/core/layout/jsx.ts +174 -0
  39. package/src/core/layout/schema.ts +963 -0
  40. package/src/core/layout/types.ts +92 -0
  41. package/src/core/list-manager/api.ts +599 -0
  42. package/src/core/list-manager/config.ts +593 -0
  43. package/src/core/list-manager/constants.ts +268 -0
  44. package/src/core/list-manager/features/api.ts +58 -0
  45. package/src/core/list-manager/features/collection/collection.ts +705 -0
  46. package/src/core/list-manager/features/collection/index.ts +17 -0
  47. package/src/core/list-manager/features/viewport/constants.ts +42 -0
  48. package/src/core/list-manager/features/viewport/index.ts +16 -0
  49. package/src/core/list-manager/features/viewport/item-size.ts +274 -0
  50. package/src/core/list-manager/features/viewport/loading.ts +263 -0
  51. package/src/core/list-manager/features/viewport/placeholders.ts +281 -0
  52. package/src/core/list-manager/features/viewport/rendering.ts +575 -0
  53. package/src/core/list-manager/features/viewport/scrollbar.ts +495 -0
  54. package/src/core/list-manager/features/viewport/scrolling.ts +795 -0
  55. package/src/core/list-manager/features/viewport/template.ts +220 -0
  56. package/src/core/list-manager/features/viewport/viewport.ts +654 -0
  57. package/src/core/list-manager/features/viewport/virtual.ts +309 -0
  58. package/src/core/list-manager/index.ts +279 -0
  59. package/src/core/list-manager/list-manager.ts +206 -0
  60. package/src/core/list-manager/types.ts +439 -0
  61. package/src/core/list-manager/utils/calculations.ts +290 -0
  62. package/src/core/list-manager/utils/range-calculator.ts +349 -0
  63. package/src/core/list-manager/utils/speed-tracker.ts +273 -0
  64. package/src/index.ts +17 -0
  65. package/src/styles/components/_list.scss +244 -0
  66. package/src/styles/index.scss +12 -0
  67. package/src/types/mtrl.d.ts +6 -0
  68. package/test/benchmarks/layout/advanced.test.ts +656 -0
  69. package/test/benchmarks/layout/comparison.test.ts +519 -0
  70. package/test/benchmarks/layout/performance-comparison.test.ts +274 -0
  71. package/test/benchmarks/layout/real-components.test.ts +733 -0
  72. package/test/benchmarks/layout/simple.test.ts +321 -0
  73. package/test/benchmarks/layout/stress.test.ts +990 -0
  74. package/test/collection/basic.test.ts +304 -0
  75. package/test/components/list.test.ts +256 -0
  76. package/test/core/collection/collection.test.ts +394 -0
  77. package/test/core/collection/failed-ranges.test.ts +270 -0
  78. package/test/core/compose/features.test.ts +183 -0
  79. package/test/core/layout/layout.test.ts +201 -0
  80. package/test/core/list-manager/features/collection.test.ts +704 -0
  81. package/test/core/list-manager/features/viewport.test.ts +698 -0
  82. package/test/core/list-manager/list-manager.test.ts +593 -0
  83. package/test/core/list-manager/utils/calculations.test.ts +433 -0
  84. package/test/core/list-manager/utils/range-calculator.test.ts +569 -0
  85. package/test/core/list-manager/utils/speed-tracker.test.ts +530 -0
  86. package/test/utils/dom-helpers.ts +275 -0
  87. package/test/utils/performance-helpers.ts +392 -0
  88. package/tsconfig.build.json +14 -0
  89. package/tsconfig.json +20 -0
  90. package/dist/index.d.ts +0 -5
  91. package/dist/index.js +0 -38
  92. package/dist/index.mjs +0 -8
@@ -0,0 +1,394 @@
1
+ // test/core/collection/collection.test.ts - Core Collection Tests
2
+ import {
3
+ describe,
4
+ test,
5
+ expect,
6
+ mock,
7
+ beforeAll,
8
+ afterAll,
9
+ beforeEach,
10
+ afterEach,
11
+ } from "bun:test";
12
+ import { JSDOM } from "jsdom";
13
+
14
+ // Setup for DOM testing environment
15
+ let dom: JSDOM;
16
+ let window: Window;
17
+ let document: Document;
18
+ let originalGlobalDocument: any;
19
+ let originalGlobalWindow: any;
20
+
21
+ // Setup DOM environment before importing modules
22
+ beforeAll(() => {
23
+ // Create a new JSDOM instance
24
+ dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
25
+ url: "http://localhost/",
26
+ pretendToBeVisual: true,
27
+ resources: "usable",
28
+ });
29
+
30
+ // Get window and document from jsdom
31
+ window = dom.window as any;
32
+ document = window.document;
33
+
34
+ // Store original globals
35
+ originalGlobalDocument = global.document;
36
+ originalGlobalWindow = global.window;
37
+
38
+ // Set globals to use jsdom
39
+ global.document = document;
40
+ global.window = window as any;
41
+ global.Element = (window as any).Element;
42
+ global.HTMLElement = (window as any).HTMLElement;
43
+ global.DocumentFragment = (window as any).DocumentFragment;
44
+ global.requestAnimationFrame = (window as any).requestAnimationFrame;
45
+ global.cancelAnimationFrame = (window as any).cancelAnimationFrame;
46
+
47
+ // Add missing DOM APIs
48
+ global.getComputedStyle =
49
+ (window as any).getComputedStyle ||
50
+ (() => ({
51
+ position: "static",
52
+ getPropertyValue: () => "",
53
+ }));
54
+
55
+ // Add IntersectionObserver mock
56
+ if (!(window as any).IntersectionObserver) {
57
+ const IntersectionObserverMock = class {
58
+ constructor() {}
59
+ observe() {}
60
+ disconnect() {}
61
+ unobserve() {}
62
+ };
63
+ (window as any).IntersectionObserver = IntersectionObserverMock as any;
64
+ (global as any).IntersectionObserver = IntersectionObserverMock as any;
65
+ }
66
+
67
+ // Add ResizeObserver mock
68
+ if (!(window as any).ResizeObserver) {
69
+ const ResizeObserverMock = class {
70
+ constructor() {}
71
+ observe() {}
72
+ disconnect() {}
73
+ unobserve() {}
74
+ };
75
+ (window as any).ResizeObserver = ResizeObserverMock as any;
76
+ (global as any).ResizeObserver = ResizeObserverMock as any;
77
+ }
78
+ });
79
+
80
+ afterAll(() => {
81
+ // Restore original globals
82
+ global.document = originalGlobalDocument;
83
+ global.window = originalGlobalWindow;
84
+
85
+ // Clean up jsdom
86
+ window.close();
87
+ });
88
+
89
+ // Import the collection after DOM setup
90
+ // TODO: Implement these imports as we build the system
91
+ // import { createCollection, pipe, withEvents, withLifecycle } from "../../../src/core/collection";
92
+
93
+ // Test data interfaces
94
+ interface TestItem {
95
+ id: string;
96
+ name: string;
97
+ email: string;
98
+ age: number;
99
+ active: boolean;
100
+ }
101
+
102
+ interface TestProduct {
103
+ id: string;
104
+ title: string;
105
+ price: number;
106
+ category: string;
107
+ }
108
+
109
+ // Helper functions for creating test data
110
+ function createTestItem(
111
+ id: string,
112
+ overrides: Partial<TestItem> = {}
113
+ ): TestItem {
114
+ return {
115
+ id,
116
+ name: `User ${id}`,
117
+ email: `user${id}@example.com`,
118
+ age: 20 + parseInt(id),
119
+ active: true,
120
+ ...overrides,
121
+ };
122
+ }
123
+
124
+ function createTestProduct(
125
+ id: string,
126
+ overrides: Partial<TestProduct> = {}
127
+ ): TestProduct {
128
+ return {
129
+ id,
130
+ title: `Product ${id}`,
131
+ price: parseFloat(id) * 10,
132
+ category: "electronics",
133
+ ...overrides,
134
+ };
135
+ }
136
+
137
+ // Mock template engine
138
+ const mockTemplateEngine = {
139
+ render: mock((template: any, data: any) => {
140
+ const element = document.createElement("div");
141
+ element.className = "test-item";
142
+ element.setAttribute("data-id", data.id);
143
+ element.innerHTML = `
144
+ <div class="item-name">${data.name}</div>
145
+ <div class="item-email">${data.email}</div>
146
+ `;
147
+ return element;
148
+ }),
149
+
150
+ compile: mock((template: any) => {
151
+ return (data: any) => mockTemplateEngine.render(template, data);
152
+ }),
153
+ };
154
+
155
+ // Mock adapter for data loading
156
+ const mockAdapter = {
157
+ read: mock(async (params: any) => {
158
+ const page = params.page || 1;
159
+ const limit = params.limit || params.per_page || 20;
160
+ const startId = (page - 1) * limit + 1;
161
+
162
+ const items: TestItem[] = [];
163
+ for (let i = 0; i < limit; i++) {
164
+ const id = startId + i;
165
+ if (id <= 100) {
166
+ items.push(createTestItem(id.toString()));
167
+ }
168
+ }
169
+
170
+ return {
171
+ items,
172
+ meta: {
173
+ total: 100,
174
+ page,
175
+ hasNext: page * limit < 100,
176
+ nextCursor: items.length > 0 ? items[items.length - 1].id : null,
177
+ },
178
+ };
179
+ }),
180
+ };
181
+
182
+ describe("Collection System", () => {
183
+ let container: HTMLElement;
184
+
185
+ beforeEach(() => {
186
+ // Create fresh container for each test
187
+ container = document.createElement("div");
188
+ container.style.height = "400px";
189
+ container.style.overflow = "auto";
190
+ document.body.appendChild(container);
191
+
192
+ // Reset mocks
193
+ mockTemplateEngine.render.mockClear();
194
+ mockTemplateEngine.compile.mockClear();
195
+ mockAdapter.read.mockClear();
196
+ });
197
+
198
+ afterEach(() => {
199
+ // Cleanup container
200
+ if (container && container.parentNode) {
201
+ container.parentNode.removeChild(container);
202
+ }
203
+ });
204
+
205
+ describe("Basic Infrastructure", () => {
206
+ test("DOM environment is properly set up", () => {
207
+ expect(document).toBeDefined();
208
+ expect(document.createElement).toBeDefined();
209
+ expect(global.IntersectionObserver).toBeDefined();
210
+ expect(global.ResizeObserver).toBeDefined();
211
+ });
212
+
213
+ test("test container is created correctly", () => {
214
+ expect(container).toBeDefined();
215
+ expect(container.style.height).toBe("400px");
216
+ expect(container.style.overflow).toBe("auto");
217
+ expect(container.parentNode).toBe(document.body);
218
+ });
219
+
220
+ test("test data helpers work correctly", () => {
221
+ const item = createTestItem("123");
222
+ expect(item.id).toBe("123");
223
+ expect(item.name).toBe("User 123");
224
+ expect(item.email).toBe("user123@example.com");
225
+ expect(item.age).toBe(143);
226
+ expect(item.active).toBe(true);
227
+
228
+ const customItem = createTestItem("456", { name: "Custom User" });
229
+ expect(customItem.name).toBe("Custom User");
230
+ expect(customItem.id).toBe("456");
231
+ });
232
+
233
+ test("mock template engine renders correctly", () => {
234
+ const template = { tag: "div", text: "{{name}}" };
235
+ const data = { id: "1", name: "John Doe", email: "john@example.com" };
236
+
237
+ const element = mockTemplateEngine.render(template, data);
238
+
239
+ expect(element.tagName).toBe("DIV");
240
+ expect(element.className).toBe("test-item");
241
+ expect(element.getAttribute("data-id")).toBe("1");
242
+ expect(element.querySelector(".item-name")?.textContent).toBe("John Doe");
243
+ });
244
+
245
+ test("mock adapter generates test data correctly", async () => {
246
+ const result = await mockAdapter.read({ page: 1, per_page: 5 });
247
+
248
+ expect(result.items).toHaveLength(5);
249
+ expect(result.meta.total).toBe(100);
250
+ expect(result.meta.page).toBe(1);
251
+ expect(result.meta.hasNext).toBe(true);
252
+
253
+ // Check first item
254
+ expect(result.items[0].id).toBe("1");
255
+ expect(result.items[0].name).toBe("User 1");
256
+ });
257
+ });
258
+
259
+ describe("Collection Core (Placeholder)", () => {
260
+ test("placeholder for collection creation", () => {
261
+ // TODO: Implement when createCollection is available
262
+ // const collection = createCollection({ items: [] });
263
+ // expect(collection).toBeDefined();
264
+
265
+ // Placeholder assertion
266
+ expect(true).toBe(true);
267
+ });
268
+
269
+ test("placeholder for functional composition", () => {
270
+ // TODO: Implement when pipe and features are available
271
+ // const collection = pipe(
272
+ // createCollection(config),
273
+ // withEvents(),
274
+ // withLifecycle()
275
+ // );
276
+ // expect(collection).toBeDefined();
277
+
278
+ // Placeholder assertion
279
+ expect(true).toBe(true);
280
+ });
281
+ });
282
+
283
+ describe("Template Engine Integration (Placeholder)", () => {
284
+ test("placeholder for template compilation", () => {
285
+ // TODO: Implement when template engine is available
286
+ // const engine = createTemplateEngine('object');
287
+ // const template = { tag: 'div', text: '{{name}}' };
288
+ // const compiled = engine.compile(template);
289
+ // expect(compiled).toBeDefined();
290
+
291
+ // Test mock for now
292
+ const compiled = mockTemplateEngine.compile({
293
+ tag: "div",
294
+ text: "{{name}}",
295
+ });
296
+ expect(compiled).toBeDefined();
297
+ expect(mockTemplateEngine.compile).toHaveBeenCalled();
298
+ });
299
+
300
+ test("placeholder for element recycling", () => {
301
+ // TODO: Implement when recycling pool is available
302
+ // const pool = createRecyclingPool({ maxSize: 10 });
303
+ // const element = pool.getElement();
304
+ // expect(element).toBeDefined();
305
+
306
+ // Placeholder assertion
307
+ expect(true).toBe(true);
308
+ });
309
+ });
310
+
311
+ describe("Performance Testing Framework", () => {
312
+ test("measures render time accurately", async () => {
313
+ const start = performance.now();
314
+
315
+ // Simulate some work
316
+ await new Promise((resolve) => setTimeout(resolve, 10));
317
+
318
+ const end = performance.now();
319
+ const duration = end - start;
320
+
321
+ expect(duration).toBeGreaterThan(5);
322
+ expect(duration).toBeLessThan(50);
323
+ });
324
+
325
+ test("creates large datasets for performance testing", () => {
326
+ const items = Array.from({ length: 1000 }, (_, i) =>
327
+ createTestItem(i.toString())
328
+ );
329
+
330
+ expect(items).toHaveLength(1000);
331
+ expect(items[0].id).toBe("0");
332
+ expect(items[999].id).toBe("999");
333
+ });
334
+
335
+ test("simulates scroll events correctly", () => {
336
+ let scrollEventFired = false;
337
+
338
+ container.addEventListener("scroll", () => {
339
+ scrollEventFired = true;
340
+ });
341
+
342
+ container.scrollTop = 100;
343
+ const scrollEvent = new (window as any).Event("scroll", {
344
+ bubbles: true,
345
+ });
346
+ container.dispatchEvent(scrollEvent);
347
+
348
+ expect(scrollEventFired).toBe(true);
349
+ expect(container.scrollTop).toBe(100);
350
+ });
351
+ });
352
+
353
+ describe("Memory Management Testing", () => {
354
+ test("cleans up event listeners", () => {
355
+ const listener = mock(() => {});
356
+
357
+ container.addEventListener("scroll", listener);
358
+
359
+ // Simulate cleanup
360
+ container.removeEventListener("scroll", listener);
361
+
362
+ // Fire event - should not call listener
363
+ const scrollEvent = new (window as any).Event("scroll", {
364
+ bubbles: true,
365
+ });
366
+ container.dispatchEvent(scrollEvent);
367
+
368
+ expect(listener).not.toHaveBeenCalled();
369
+ });
370
+
371
+ test("handles rapid element creation/destruction", () => {
372
+ const elements: HTMLElement[] = [];
373
+
374
+ // Create many elements
375
+ for (let i = 0; i < 100; i++) {
376
+ const element = document.createElement("div");
377
+ element.textContent = `Item ${i}`;
378
+ elements.push(element);
379
+ }
380
+
381
+ expect(elements).toHaveLength(100);
382
+
383
+ // Clean up
384
+ elements.forEach((element) => {
385
+ element.remove();
386
+ });
387
+
388
+ expect(elements[0].isConnected).toBe(false);
389
+ });
390
+ });
391
+ });
392
+
393
+ // Export test utilities for use in other test files
394
+ export { createTestItem, createTestProduct, mockTemplateEngine, mockAdapter };
@@ -0,0 +1,270 @@
1
+ // test/core/collection/failed-ranges.test.ts
2
+
3
+ import { describe, test, expect, beforeEach, mock, afterEach } from "bun:test";
4
+ import { createListManager } from "../../../src/core/list-manager/list-manager";
5
+ import { withCollection } from "../../../src/core/list-manager/features/collection/collection";
6
+ import type { ListManagerConfig } from "../../../src/core/list-manager/types";
7
+
8
+ describe("Collection - Failed Range Tracking", () => {
9
+ let container: HTMLElement;
10
+ let config: ListManagerConfig;
11
+ let mockAdapter: any;
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement("div");
15
+ container.style.height = "600px";
16
+ container.style.width = "400px";
17
+ document.body.appendChild(container);
18
+
19
+ // Mock adapter that can simulate failures
20
+ mockAdapter = {
21
+ read: mock(),
22
+ };
23
+
24
+ config = {
25
+ container,
26
+ items: [],
27
+ template: {
28
+ template: (item: any) => `<div>Item ${item.id}</div>`,
29
+ },
30
+ virtual: {
31
+ enabled: true,
32
+ itemSize: 84,
33
+ estimatedItemSize: 84,
34
+ overscan: 5,
35
+ },
36
+ };
37
+ });
38
+
39
+ afterEach(() => {
40
+ container.remove();
41
+ mock.restore();
42
+ });
43
+
44
+ test("should track failed ranges when data loading fails", async () => {
45
+ // Configure adapter to fail
46
+ mockAdapter.read.mockRejectedValueOnce(new Error("Network error"));
47
+
48
+ const manager = createListManager(config);
49
+ const enhancedManager = withCollection({
50
+ collection: mockAdapter,
51
+ rangeSize: 20,
52
+ })(manager);
53
+
54
+ // Try to load a range
55
+ try {
56
+ await enhancedManager.collection.loadRange(0, 20);
57
+ } catch (error) {
58
+ // Expected to fail
59
+ }
60
+
61
+ // Check that failed range is tracked
62
+ const failedRanges = enhancedManager.collection.getFailedRanges();
63
+ expect(failedRanges.size).toBe(1);
64
+ expect(failedRanges.has(0)).toBe(true);
65
+
66
+ const failedInfo = failedRanges.get(0);
67
+ expect(failedInfo?.attempts).toBe(1);
68
+ expect(failedInfo?.error.message).toBe("Network error");
69
+ });
70
+
71
+ test("should not reload failed ranges immediately", async () => {
72
+ // Configure adapter to fail
73
+ mockAdapter.read.mockRejectedValue(new Error("Network error"));
74
+
75
+ const manager = createListManager(config);
76
+ const enhancedManager = withCollection({
77
+ collection: mockAdapter,
78
+ rangeSize: 20,
79
+ })(manager);
80
+
81
+ // First attempt - should fail
82
+ try {
83
+ await enhancedManager.collection.loadRange(0, 20);
84
+ } catch (error) {
85
+ // Expected
86
+ }
87
+
88
+ // Reset mock
89
+ mockAdapter.read.mockClear();
90
+
91
+ // Try to load missing ranges immediately - should skip failed range
92
+ await enhancedManager.collection.loadMissingRanges({ start: 0, end: 19 });
93
+
94
+ // Should not have tried to load again
95
+ expect(mockAdapter.read).not.toHaveBeenCalled();
96
+ });
97
+
98
+ test("should retry failed ranges after clearing lastAttempt", async () => {
99
+ // Configure adapter to fail first, then succeed
100
+ mockAdapter.read
101
+ .mockRejectedValueOnce(new Error("Network error"))
102
+ .mockResolvedValueOnce({
103
+ items: Array.from({ length: 20 }, (_, i) => ({
104
+ id: i,
105
+ name: `Item ${i}`,
106
+ })),
107
+ meta: { total: 100 },
108
+ });
109
+
110
+ const manager = createListManager(config);
111
+ const enhancedManager = withCollection({
112
+ collection: mockAdapter,
113
+ rangeSize: 20,
114
+ })(manager);
115
+
116
+ // First attempt - should fail
117
+ try {
118
+ await enhancedManager.collection.loadRange(0, 20);
119
+ } catch (error) {
120
+ // Expected
121
+ }
122
+
123
+ // Manually retry failed ranges
124
+ await enhancedManager.collection.retryFailedRanges();
125
+
126
+ // Check that failed range was cleared after success
127
+ const failedRanges = enhancedManager.collection.getFailedRanges();
128
+ expect(failedRanges.size).toBe(0);
129
+
130
+ // Check that data was loaded
131
+ const loadedRanges = enhancedManager.collection.getLoadedRanges();
132
+ expect(loadedRanges.has(0)).toBe(true);
133
+ });
134
+
135
+ test("should track multiple attempts", async () => {
136
+ // Configure adapter to fail multiple times
137
+ mockAdapter.read.mockRejectedValue(new Error("Network error"));
138
+
139
+ const manager = createListManager(config);
140
+ const enhancedManager = withCollection({
141
+ collection: mockAdapter,
142
+ rangeSize: 20,
143
+ })(manager);
144
+
145
+ // Multiple attempts
146
+ for (let i = 0; i < 3; i++) {
147
+ try {
148
+ // Clear lastAttempt to allow immediate retry
149
+ const failedRanges = enhancedManager.collection.getFailedRanges();
150
+ failedRanges.forEach((info) => {
151
+ info.lastAttempt = 0;
152
+ });
153
+
154
+ await enhancedManager.collection.loadRange(0, 20);
155
+ } catch (error) {
156
+ // Expected
157
+ }
158
+ }
159
+
160
+ // Check attempt count
161
+ const failedRanges = enhancedManager.collection.getFailedRanges();
162
+ const failedInfo = failedRanges.get(0);
163
+ expect(failedInfo?.attempts).toBe(3);
164
+ });
165
+
166
+ test("should clear failed ranges", async () => {
167
+ // Configure adapter to fail
168
+ mockAdapter.read.mockRejectedValue(new Error("Network error"));
169
+
170
+ const manager = createListManager(config);
171
+ const enhancedManager = withCollection({
172
+ collection: mockAdapter,
173
+ rangeSize: 20,
174
+ })(manager);
175
+
176
+ // Fail to load some ranges
177
+ try {
178
+ await enhancedManager.collection.loadRange(0, 20);
179
+ } catch (error) {
180
+ // Expected
181
+ }
182
+ try {
183
+ await enhancedManager.collection.loadRange(40, 20);
184
+ } catch (error) {
185
+ // Expected
186
+ }
187
+
188
+ // Should have 2 failed ranges
189
+ expect(enhancedManager.collection.getFailedRanges().size).toBe(2);
190
+
191
+ // Clear failed ranges
192
+ enhancedManager.collection.clearFailedRanges();
193
+
194
+ // Should have no failed ranges
195
+ expect(enhancedManager.collection.getFailedRanges().size).toBe(0);
196
+ });
197
+
198
+ test("should handle concurrent failures correctly", async () => {
199
+ // Configure adapter to fail
200
+ mockAdapter.read.mockRejectedValue(new Error("Network error"));
201
+
202
+ const manager = createListManager(config);
203
+ const enhancedManager = withCollection({
204
+ collection: mockAdapter,
205
+ rangeSize: 20,
206
+ })(manager);
207
+
208
+ // Try to load multiple ranges concurrently
209
+ const promises = [
210
+ enhancedManager.collection.loadRange(0, 20),
211
+ enhancedManager.collection.loadRange(20, 20),
212
+ enhancedManager.collection.loadRange(40, 20),
213
+ ];
214
+
215
+ // All should fail
216
+ const results = await Promise.allSettled(promises);
217
+ expect(results.every((r) => r.status === "rejected")).toBe(true);
218
+
219
+ // Should have 3 failed ranges
220
+ const failedRanges = enhancedManager.collection.getFailedRanges();
221
+ expect(failedRanges.size).toBe(3);
222
+ expect(failedRanges.has(0)).toBe(true);
223
+ expect(failedRanges.has(1)).toBe(true);
224
+ expect(failedRanges.has(2)).toBe(true);
225
+ });
226
+
227
+ test("should respect exponential backoff", async () => {
228
+ // Configure adapter to fail
229
+ mockAdapter.read.mockRejectedValue(new Error("Network error"));
230
+
231
+ const manager = createListManager(config);
232
+ const enhancedManager = withCollection({
233
+ collection: mockAdapter,
234
+ rangeSize: 20,
235
+ })(manager);
236
+
237
+ // First failure
238
+ try {
239
+ await enhancedManager.collection.loadRange(0, 20);
240
+ } catch (error) {
241
+ // Expected
242
+ }
243
+
244
+ // Try again immediately - should be blocked
245
+ mockAdapter.read.mockClear();
246
+ try {
247
+ await enhancedManager.collection.loadRange(0, 20);
248
+ } catch (error) {
249
+ // Should re-throw the same error without calling adapter
250
+ expect(error.message).toBe("Network error");
251
+ }
252
+ expect(mockAdapter.read).not.toHaveBeenCalled();
253
+
254
+ // Simulate time passing (clear lastAttempt)
255
+ const failedRanges = enhancedManager.collection.getFailedRanges();
256
+ const failedInfo = failedRanges.get(0);
257
+ if (failedInfo) {
258
+ failedInfo.lastAttempt = Date.now() - 2000; // 2 seconds ago
259
+ }
260
+
261
+ // Now it should try again
262
+ mockAdapter.read.mockRejectedValueOnce(new Error("Still failing"));
263
+ try {
264
+ await enhancedManager.collection.loadRange(0, 20);
265
+ } catch (error) {
266
+ expect(error.message).toBe("Still failing");
267
+ }
268
+ expect(mockAdapter.read).toHaveBeenCalledTimes(1);
269
+ });
270
+ });