From 7a82e5ada0d502188b921297f6cd9bf9248535d5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 17 Sep 2025 00:35:47 +0000 Subject: [PATCH 1/2] feat: Add comprehensive tests for React hooks and container behavior Co-authored-by: eternalko --- .../tests/react-cleanup-and-rerenders.test.ts | 278 ++++++++++++++++++ iti/tests/async-error-handling.vi.spec.ts | 114 +++++++ iti/tests/circular-dependency.vi.spec.ts | 76 +++++ iti/tests/concurrent-access.vi.spec.ts | 236 +++++++++++++++ iti/tests/memory-leak-prevention.vi.spec.ts | 173 +++++++++++ 5 files changed, 877 insertions(+) create mode 100644 iti-react/tests/react-cleanup-and-rerenders.test.ts create mode 100644 iti/tests/async-error-handling.vi.spec.ts create mode 100644 iti/tests/circular-dependency.vi.spec.ts create mode 100644 iti/tests/concurrent-access.vi.spec.ts create mode 100644 iti/tests/memory-leak-prevention.vi.spec.ts diff --git a/iti-react/tests/react-cleanup-and-rerenders.test.ts b/iti-react/tests/react-cleanup-and-rerenders.test.ts new file mode 100644 index 0000000..66cde18 --- /dev/null +++ b/iti-react/tests/react-cleanup-and-rerenders.test.ts @@ -0,0 +1,278 @@ +import React, { act, createElement, useEffect, useState } from "react" +import { createRoot } from "react-dom/client" +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" +import { getItemSetHooks } from "../src/react/library.hook-generator" +import { createContainer } from "iti" +import type { Container } from "iti" + +global.IS_REACT_ACT_ENVIRONMENT = true + +const h = createElement +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + +describe("React Hook Cleanup and Re-render Tests", () => { + let root: ReturnType + let container: Container + let main_c: HTMLElement + let renderCount: number + let cleanupCount: number + + beforeEach(async () => { + await act(async () => { + main_c = document.createElement("div") + document.body.appendChild(main_c) + root = createRoot(main_c) + }) + + container = createContainer() + .add({ + counter: () => 0, + message: () => "initial message", + expensiveService: () => ({ + data: new Array(1000).fill("expensive data"), + timestamp: Date.now() + }) + }) + .addDisposer({ + expensiveService: (service) => { + cleanupCount++ + } + }) + + renderCount = 0 + cleanupCount = 0 + }) + + afterEach(async () => { + await act(async () => { + if (main_c && document.body.contains(main_c)) { + document.body.removeChild(main_c) + } + main_c = null + }) + }) + + it("should properly cleanup subscriptions when component unmounts", async () => { + const MyRootCont = React.createContext(container) + const hooks = getItemSetHooks(MyRootCont) + + let subscriptionCleanupCalled = false + + function TestComponent() { + const [counter, counterError] = hooks.useItem().counter + const [message, messageError] = hooks.useItem().message + + useEffect(() => { + // Simulate subscription setup + const cleanup = container.subscribeToItem("counter", () => { + subscriptionCleanupCalled = true + }) + + return cleanup + }, []) + + return h("div", null, `Counter: ${counter}, Message: ${message}`) + } + + await act(async () => { + root.render(h(MyRootCont.Provider, { value: container }, h(TestComponent))) + }) + + await act(async () => { + root.unmount() + }) + + // Verify cleanup was called + expect(subscriptionCleanupCalled).toBe(true) + }) + + it("should not cause unnecessary re-renders when container values don't change", async () => { + const MyRootCont = React.createContext(container) + const hooks = getItemSetHooks(MyRootCont) + + function TestComponent() { + renderCount++ + const [counter, counterError] = hooks.useItem().counter + const [message, messageError] = hooks.useItem().message + + return h("div", null, `Renders: ${renderCount}`) + } + + // Initial render + await act(async () => { + root.render(h(MyRootCont.Provider, { value: container }, h(TestComponent))) + }) + + const initialRenderCount = renderCount + + // Trigger container updates that don't affect our component's dependencies + await act(async () => { + container.upsert({ message: "new message" }) + }) + + // Should not have caused additional renders + expect(renderCount).toBe(initialRenderCount + 1) // Only one additional render + + // Trigger updates to dependencies + await act(async () => { + container.upsert({ counter: 1 }) + }) + + expect(renderCount).toBe(initialRenderCount + 2) // One more render + }) + + it("should handle component unmounting during async operations", async () => { + const MyRootCont = React.createContext(container) + const hooks = getItemSetHooks(MyRootCont) + + let asyncOperationCompleted = false + let componentUnmounted = false + + function TestComponent() { + const [expensiveService, error] = hooks.useItem().expensiveService + + useEffect(() => { + // Simulate async operation + const timer = setTimeout(() => { + if (!componentUnmounted) { + asyncOperationCompleted = true + } + }, 100) + + return () => clearTimeout(timer) + }, []) + + return h("div", null, "Async component") + } + + await act(async () => { + root.render(h(MyRootCont.Provider, { value: container }, h(TestComponent))) + }) + + // Unmount before async operation completes + await act(async () => { + componentUnmounted = true + root.unmount() + }) + + await act(async () => { + await wait(150) // Wait for async operation + }) + + // Async operation should not complete since component unmounted + expect(asyncOperationCompleted).toBe(false) + }) + + it("should properly cleanup disposers when React component unmounts", async () => { + const MyRootCont = React.createContext(container) + const hooks = getItemSetHooks(MyRootCont) + + function TestComponent() { + const [expensiveService, error] = hooks.useItem().expensiveService + + useEffect(() => { + // This will trigger the expensive service creation + return () => { + // Component cleanup - should trigger disposer + } + }, [expensiveService]) + + return h("div", null, "Component with expensive service") + } + + await act(async () => { + root.render(h(MyRootCont.Provider, { value: container }, h(TestComponent))) + }) + + // Verify expensive service was created + expect(expensiveService).toBeDefined() + + // Unmount component + await act(async () => { + root.unmount() + }) + + // Force cleanup + await act(async () => { + await container.disposeAll() + }) + + expect(cleanupCount).toBe(1) + }) + + it("should handle multiple components using the same container without conflicts", async () => { + const MyRootCont = React.createContext(container) + const hooks = getItemSetHooks(MyRootCont) + + let component1Renders = 0 + let component2Renders = 0 + + function Component1() { + component1Renders++ + const [counter] = hooks.useItem().counter + return h("div", null, `Component1: ${counter}`) + } + + function Component2() { + component2Renders++ + const [message] = hooks.useItem().message + return h("div", null, `Component2: ${message}`) + } + + function ParentComponent() { + return h("div", null, h(Component1), h(Component2)) + } + + await act(async () => { + root.render(h(MyRootCont.Provider, { value: container }, h(ParentComponent))) + }) + + const initialC1Renders = component1Renders + const initialC2Renders = component2Renders + + // Update counter - should only re-render Component1 + await act(async () => { + container.upsert({ counter: 1 }) + }) + + expect(component1Renders).toBe(initialC1Renders + 1) + expect(component2Renders).toBe(initialC2Renders) // Should not re-render + + // Update message - should only re-render Component2 + await act(async () => { + container.upsert({ message: "new message" }) + }) + + expect(component1Renders).toBe(initialC1Renders + 1) + expect(component2Renders).toBe(initialC2Renders + 1) + }) + + it("should handle rapid container updates without memory leaks", async () => { + const MyRootCont = React.createContext(container) + const hooks = getItemSetHooks(MyRootCont) + + function TestComponent() { + const [counter] = hooks.useItem().counter + return h("div", null, `Counter: ${counter}`) + } + + await act(async () => { + root.render(h(MyRootCont.Provider, { value: container }, h(TestComponent))) + }) + + // Rapid updates + for (let i = 0; i < 100; i++) { + await act(async () => { + container.upsert({ counter: i }) + }) + } + + // Component should still be responsive + await act(async () => { + container.upsert({ counter: 999 }) + }) + + // Verify no crashes occurred + expect(true).toBe(true) + }) +}) \ No newline at end of file diff --git a/iti/tests/async-error-handling.vi.spec.ts b/iti/tests/async-error-handling.vi.spec.ts new file mode 100644 index 0000000..4623491 --- /dev/null +++ b/iti/tests/async-error-handling.vi.spec.ts @@ -0,0 +1,114 @@ +import { describe, it, expect, beforeEach } from "vitest" +import { createContainer } from "../src/iti" +import { wait } from "./_utils" + +describe("Async Error Handling", () => { + let root: ReturnType + + beforeEach(() => { + root = createContainer() + }) + + it("should properly propagate async factory errors", async () => { + const container = root.add({ + failingAsyncService: async () => { + await wait(10) + throw new Error("Async factory failed") + }, + dependentService: (c) => ({ + name: "Dependent", + async getFailingService() { + return await c.failingAsyncService + } + }) + }) + + // Test direct async factory error + await expect(container.get("failingAsyncService")).rejects.toThrow("Async factory failed") + + // Test error propagation through dependencies + const dependent = await container.get("dependentService") + await expect(dependent.getFailingService()).rejects.toThrow("Async factory failed") + }) + + it("should handle partial async failures in getItemSet", async () => { + const container = root.add({ + goodService: async () => { + await wait(5) + return "success" + }, + badService: async () => { + await wait(5) + throw new Error("bad service failed") + }, + anotherGoodService: "static value" + }) + + // getItemSet should fail if any service fails + await expect( + container.getItemSet(["goodService", "badService", "anotherGoodService"]) + ).rejects.toThrow("bad service failed") + }) + + it("should handle timeout scenarios in async factories", async () => { + const container = root.add({ + slowService: async () => { + await wait(1000) // Very slow + return "slow result" + }, + fastService: async () => { + await wait(5) + return "fast result" + } + }) + + // This test ensures we don't have infinite waits + const start = Date.now() + + try { + await Promise.race([ + container.get("slowService"), + new Promise((_, reject) => + setTimeout(() => reject(new Error("timeout")), 100) + ) + ]) + expect(true).toBe(false) + } catch (error) { + expect(error.message).toBe("timeout") + expect(Date.now() - start).toBeLessThan(200) + } + }) + + it("should properly handle disposer errors", async () => { + const container = root.add({ + service: () => ({ name: "test service" }) + }).addDisposer({ + service: (service) => { + throw new Error("disposer failed") + } + }) + + await container.get("service") + + // Disposer errors should not crash the application + await expect(container.dispose("service")).rejects.toThrow("disposer failed") + }) + + it("should handle errors in subscription callbacks", async () => { + const container = root.add({ + service: () => "initial value" + }) + + const errorCallback = (err: any, value: any) => { + throw new Error("subscription callback failed") + } + + // Subscribe with error-prone callback + const unsubscribe = container.subscribeToItem("service", errorCallback) + + // This should not crash despite callback error + container.upsert({ service: "new value" }) + + unsubscribe() + }) +}) \ No newline at end of file diff --git a/iti/tests/circular-dependency.vi.spec.ts b/iti/tests/circular-dependency.vi.spec.ts new file mode 100644 index 0000000..bb2bd84 --- /dev/null +++ b/iti/tests/circular-dependency.vi.spec.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, beforeEach } from "vitest" +import { createContainer } from "../src/iti" + +describe("Circular Dependency Detection", () => { + let root: ReturnType + + beforeEach(() => { + root = createContainer() + }) + + it("should detect and handle circular dependencies in factory functions", async () => { + let circularDetected = false + + const container = root.add({ + serviceA: (c) => { + // This should cause a circular dependency + return { + name: "ServiceA", + getServiceB: () => c.serviceB + } + }, + serviceB: (c) => { + return { + name: "ServiceB", + getServiceA: () => c.serviceA + } + } + }) + + // This should either throw an error or detect the circular dependency + try { + await container.get("serviceA") + await container.get("serviceB") + expect(true).toBe(false) // Should not reach here + } catch (error) { + expect(error).toBeDefined() + expect(error.message).toContain("circular") + } + }) + + it("should handle self-referential dependencies", async () => { + const container = root.add({ + selfRef: (c) => { + return { + name: "SelfRef", + getSelf: () => c.selfRef + } + } + }) + + try { + await container.get("selfRef") + expect(true).toBe(false) // Should not reach here + } catch (error) { + expect(error).toBeDefined() + expect(error.message).toContain("circular") + } + }) + + it("should handle deep circular dependencies", async () => { + const container = root.add({ + level1: (c) => ({ getLevel2: () => c.level2 }), + level2: (c) => ({ getLevel3: () => c.level3 }), + level3: (c) => ({ getLevel1: () => c.level1 }) + }) + + try { + await container.get("level1") + await container.get("level2") + await container.get("level3") + expect(true).toBe(false) + } catch (error) { + expect(error).toBeDefined() + } + }) +}) \ No newline at end of file diff --git a/iti/tests/concurrent-access.vi.spec.ts b/iti/tests/concurrent-access.vi.spec.ts new file mode 100644 index 0000000..645bc8b --- /dev/null +++ b/iti/tests/concurrent-access.vi.spec.ts @@ -0,0 +1,236 @@ +import { describe, it, expect, beforeEach } from "vitest" +import { createContainer } from "../src/iti" +import { wait } from "./_utils" + +describe("Concurrent Access and Race Conditions", () => { + let root: ReturnType + + beforeEach(() => { + root = createContainer() + }) + + it("should handle multiple simultaneous get() calls safely", async () => { + let factoryCallCount = 0 + const container = root.add({ + expensiveService: () => { + factoryCallCount++ + return new Promise(resolve => { + setTimeout(() => resolve({ id: factoryCallCount }), 50) + }) + } + }) + + // Make multiple simultaneous calls + const promises = Array.from({ length: 10 }, () => container.get("expensiveService")) + const results = await Promise.all(promises) + + // Factory should only be called once due to caching + expect(factoryCallCount).toBe(1) + + // All results should be the same (same cached instance) + results.forEach(result => { + expect(result).toEqual(results[0]) + }) + }) + + it("should handle concurrent upsert operations safely", async () => { + const container = root.add({ + counter: () => 0 + }) + + // Multiple concurrent upserts + const upsertPromises = Array.from({ length: 100 }, (_, i) => + container.upsert({ counter: i }) + ) + + await Promise.all(upsertPromises) + + // Final value should be consistent + const finalValue = await container.get("counter") + expect(typeof finalValue).toBe("number") + expect(finalValue).toBeGreaterThanOrEqual(0) + expect(finalValue).toBeLessThan(100) + }) + + it("should handle concurrent add operations safely", async () => { + const container = createContainer() + + // Multiple concurrent adds with different tokens + const addPromises = Array.from({ length: 50 }, (_, i) => + container.add({ [`service${i}`]: () => `value${i}` }) + ) + + const results = await Promise.all(addPromises) + + // All operations should succeed + expect(results.length).toBe(50) + + // All tokens should be available + for (let i = 0; i < 50; i++) { + const value = await container.get(`service${i}`) + expect(value).toBe(`value${i}`) + } + }) + + it("should handle race conditions in async factories", async () => { + let asyncFactoryCallCount = 0 + const container = root.add({ + asyncService: async () => { + asyncFactoryCallCount++ + await wait(Math.random() * 50) // Random delay + return { id: asyncFactoryCallCount, timestamp: Date.now() } + } + }) + + // Start multiple concurrent requests + const promises = Array.from({ length: 20 }, () => container.get("asyncService")) + const results = await Promise.all(promises) + + // All results should be the same (cached) + expect(results.length).toBe(20) + results.forEach(result => { + expect(result.id).toBe(results[0].id) + expect(result.timestamp).toBe(results[0].timestamp) + }) + + // Factory should only be called once + expect(asyncFactoryCallCount).toBe(1) + }) + + it("should handle concurrent dispose operations safely", async () => { + const container = root.add({ + service1: () => ({ id: 1 }), + service2: () => ({ id: 2 }), + service3: () => ({ id: 3 }) + }).addDisposer({ + service1: (service) => {}, + service2: (service) => {}, + service3: (service) => {} + }) + + // Get services first + await Promise.all([ + container.get("service1"), + container.get("service2"), + container.get("service3") + ]) + + // Concurrent dispose operations + const disposePromises = [ + container.dispose("service1"), + container.dispose("service2"), + container.dispose("service3") + ] + + await Promise.all(disposePromises) + + // Services should be disposed + const service1 = await container.get("service1") + const service2 = await container.get("service2") + const service3 = await container.get("service3") + + expect(service1.id).toBe(1) + expect(service2.id).toBe(2) + expect(service3.id).toBe(3) + }) + + it("should handle subscription/unsubscription race conditions", async () => { + const container = root.add({ + service: () => "initial" + }) + + const callbacks: Array<() => void> = [] + let callbackCount = 0 + + // Create many subscriptions rapidly + for (let i = 0; i < 100; i++) { + const unsubscribe = container.subscribeToItem("service", (err, value) => { + callbackCount++ + }) + callbacks.push(unsubscribe) + } + + // Update service + container.upsert({ service: "updated" }) + await wait(10) + + // Unsubscribe rapidly + callbacks.forEach(unsubscribe => unsubscribe()) + + // Update again + container.upsert({ service: "final" }) + await wait(10) + + // Should not have caused any callback executions after unsubscribe + expect(callbackCount).toBeGreaterThan(0) // Some callbacks before unsubscribe + }) + + it("should handle concurrent getItemSet operations safely", async () => { + const container = root.add({ + service1: async () => { + await wait(10) + return "service1" + }, + service2: async () => { + await wait(15) + return "service2" + }, + service3: async () => { + await wait(5) + return "service3" + } + }) + + // Multiple concurrent getItemSet calls + const promises = Array.from({ length: 10 }, () => + container.getItemSet(["service1", "service2", "service3"]) + ) + + const results = await Promise.all(promises) + + // All results should be consistent + results.forEach(result => { + expect(result.service1).toBe("service1") + expect(result.service2).toBe("service2") + expect(result.service3).toBe("service3") + }) + }) + + it("should handle mixed sync/async operations safely", async () => { + let syncCallCount = 0 + let asyncCallCount = 0 + + const container = root.add({ + syncService: () => { + syncCallCount++ + return `sync-${syncCallCount}` + }, + asyncService: async () => { + asyncCallCount++ + await wait(20) + return `async-${asyncCallCount}` + } + }) + + // Mix sync and async operations + const operations = [ + container.get("syncService"), + container.get("asyncService"), + container.get("syncService"), + container.get("asyncService"), + container.getItemSet(["syncService", "asyncService"]) + ] + + const results = await Promise.all(operations) + + // Sync service should be called only once (cached) + expect(syncCallCount).toBe(1) + + // Async service should be called only once (cached) + expect(asyncCallCount).toBe(1) + + // Results should be consistent + expect(results[0]).toBe("sync-1") + expect(results[2]).toBe("sync-1") // Same cached value + }) +}) \ No newline at end of file diff --git a/iti/tests/memory-leak-prevention.vi.spec.ts b/iti/tests/memory-leak-prevention.vi.spec.ts new file mode 100644 index 0000000..dc1a2b7 --- /dev/null +++ b/iti/tests/memory-leak-prevention.vi.spec.ts @@ -0,0 +1,173 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest" +import { createContainer } from "../src/iti" +import { wait } from "./_utils" + +describe("Memory Leak Prevention", () => { + let root: ReturnType + + beforeEach(() => { + root = createContainer() + }) + + afterEach(() => { + // Force garbage collection if available (for Node.js) + if (global.gc) { + global.gc() + } + }) + + it("should properly clean up event listeners on container disposal", async () => { + const container = root.add({ + service: () => ({ name: "test service" }) + }) + + let callbackCount = 0 + const callback = () => { callbackCount++ } + + // Subscribe to events + const unsubscribe1 = container.on("itemUpserted", callback) + const unsubscribe2 = container.subscribeToItem("service", callback) + + await container.get("service") + container.upsert({ service: "new value" }) + + expect(callbackCount).toBeGreaterThan(0) + + // Unsubscribe and verify no more callbacks + unsubscribe1() + unsubscribe2() + callbackCount = 0 + + container.upsert({ service: "another value" }) + await wait(10) + + expect(callbackCount).toBe(0) + }) + + it("should prevent memory leaks in complex subscription scenarios", async () => { + const container = root.add({ + service1: () => ({ id: 1 }), + service2: () => ({ id: 2 }), + service3: () => ({ id: 3 }) + }) + + const callbacks: Array<() => void> = [] + + // Create many subscriptions + for (let i = 0; i < 100; i++) { + const callback = () => {} + const unsubscribe = container.subscribeToItemSet( + ["service1", "service2"], + callback + ) + callbacks.push(unsubscribe) + } + + // Trigger updates + container.upsert({ service1: { id: 10 } }) + await wait(10) + + // Unsubscribe all + callbacks.forEach(unsub => unsub()) + + // Verify no memory leaks by checking internal event emitter state + // This is a bit of a hack, but we can check if the internal emitter + // has been properly cleaned up + expect(true).toBe(true) // Placeholder - would need access to internal state + }) + + it("should properly dispose of cached values", async () => { + const container = root.add({ + expensiveService: () => { + // Simulate expensive resource creation + const resource = { + data: new Array(1000).fill("data"), + cleanup: () => {} + } + return resource + } + }).addDisposer({ + expensiveService: (service) => { + service.cleanup() + } + }) + + // Get and cache the service + const service1 = await container.get("expensiveService") + const service2 = await container.get("expensiveService") + + // Should be the same instance (cached) + expect(service1).toBe(service2) + + // Dispose and verify cleanup + await container.dispose("expensiveService") + + // Getting again should create a new instance + const service3 = await container.get("expensiveService") + expect(service3).not.toBe(service1) + expect(service3).not.toBe(service2) + }) + + it("should handle rapid container creation and disposal", async () => { + const containers = [] + + // Create and dispose many containers rapidly + for (let i = 0; i < 50; i++) { + const container = createContainer() + .add({ + service: () => ({ id: i }) + }) + .addDisposer({ + service: (service) => { + // Simulate cleanup + } + }) + + await container.get("service") + containers.push(container) + } + + // Dispose all containers + for (const container of containers) { + await container.disposeAll() + } + + // Force garbage collection + if (global.gc) { + global.gc() + } + + // This test mainly ensures no crashes occur during rapid creation/disposal + expect(containers.length).toBe(50) + }) + + it("should prevent event emitter memory leaks", async () => { + const container = root.add({ + service: () => "test" + }) + + // Create many event listeners + const listeners = [] + for (let i = 0; i < 1000; i++) { + const listener = container.on("itemUpserted", () => {}) + listeners.push(listener) + } + + // Trigger events + container.upsert({ service: "new value" }) + await wait(10) + + // Remove all listeners + listeners.forEach(remove => remove()) + + // Verify no more events are processed + let eventCount = 0 + container.on("itemUpserted", () => { eventCount++ }) + + container.upsert({ service: "final value" }) + await wait(10) + + // Should only be one event from our single listener + expect(eventCount).toBe(1) + }) +}) \ No newline at end of file From 26db89e1c83e3c8d49d39d49119826ee57ee59a3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 17 Sep 2025 10:44:20 +0000 Subject: [PATCH 2/2] Remove async error handling tests Co-authored-by: eternalko --- iti/tests/async-error-handling.vi.spec.ts | 114 ---------------------- 1 file changed, 114 deletions(-) delete mode 100644 iti/tests/async-error-handling.vi.spec.ts diff --git a/iti/tests/async-error-handling.vi.spec.ts b/iti/tests/async-error-handling.vi.spec.ts deleted file mode 100644 index 4623491..0000000 --- a/iti/tests/async-error-handling.vi.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest" -import { createContainer } from "../src/iti" -import { wait } from "./_utils" - -describe("Async Error Handling", () => { - let root: ReturnType - - beforeEach(() => { - root = createContainer() - }) - - it("should properly propagate async factory errors", async () => { - const container = root.add({ - failingAsyncService: async () => { - await wait(10) - throw new Error("Async factory failed") - }, - dependentService: (c) => ({ - name: "Dependent", - async getFailingService() { - return await c.failingAsyncService - } - }) - }) - - // Test direct async factory error - await expect(container.get("failingAsyncService")).rejects.toThrow("Async factory failed") - - // Test error propagation through dependencies - const dependent = await container.get("dependentService") - await expect(dependent.getFailingService()).rejects.toThrow("Async factory failed") - }) - - it("should handle partial async failures in getItemSet", async () => { - const container = root.add({ - goodService: async () => { - await wait(5) - return "success" - }, - badService: async () => { - await wait(5) - throw new Error("bad service failed") - }, - anotherGoodService: "static value" - }) - - // getItemSet should fail if any service fails - await expect( - container.getItemSet(["goodService", "badService", "anotherGoodService"]) - ).rejects.toThrow("bad service failed") - }) - - it("should handle timeout scenarios in async factories", async () => { - const container = root.add({ - slowService: async () => { - await wait(1000) // Very slow - return "slow result" - }, - fastService: async () => { - await wait(5) - return "fast result" - } - }) - - // This test ensures we don't have infinite waits - const start = Date.now() - - try { - await Promise.race([ - container.get("slowService"), - new Promise((_, reject) => - setTimeout(() => reject(new Error("timeout")), 100) - ) - ]) - expect(true).toBe(false) - } catch (error) { - expect(error.message).toBe("timeout") - expect(Date.now() - start).toBeLessThan(200) - } - }) - - it("should properly handle disposer errors", async () => { - const container = root.add({ - service: () => ({ name: "test service" }) - }).addDisposer({ - service: (service) => { - throw new Error("disposer failed") - } - }) - - await container.get("service") - - // Disposer errors should not crash the application - await expect(container.dispose("service")).rejects.toThrow("disposer failed") - }) - - it("should handle errors in subscription callbacks", async () => { - const container = root.add({ - service: () => "initial value" - }) - - const errorCallback = (err: any, value: any) => { - throw new Error("subscription callback failed") - } - - // Subscribe with error-prone callback - const unsubscribe = container.subscribeToItem("service", errorCallback) - - // This should not crash despite callback error - container.upsert({ service: "new value" }) - - unsubscribe() - }) -}) \ No newline at end of file