hot-reload-singleton.test.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
  2. /**
  3. * Tests for hot-reload singleton pattern (globalThis caching)
  4. * Verifies that EventEmitter and RequestFilterEngine use the same instance
  5. * across multiple dynamic imports (simulating different worker contexts)
  6. */
  7. describe("globalThis singleton pattern", () => {
  8. beforeEach(() => {
  9. vi.resetModules();
  10. });
  11. afterEach(() => {
  12. // Clean up globalThis
  13. const g = globalThis as Record<string, unknown>;
  14. delete g.__CCH_EVENT_EMITTER__;
  15. delete g.__CCH_REQUEST_FILTER_ENGINE__;
  16. delete g.__CCH_SENSITIVE_WORD_DETECTOR__;
  17. });
  18. test("eventEmitter: multiple imports return same instance", async () => {
  19. // First import
  20. const { eventEmitter: emitter1 } = await import("@/lib/event-emitter");
  21. // Reset module cache to simulate different worker context
  22. vi.resetModules();
  23. // Second import
  24. const { eventEmitter: emitter2 } = await import("@/lib/event-emitter");
  25. // Should be the exact same instance due to globalThis caching
  26. expect(emitter1).toBe(emitter2);
  27. });
  28. test("eventEmitter: globalThis stores the singleton", async () => {
  29. const g = globalThis as Record<string, unknown>;
  30. // Before import, should not exist
  31. expect(g.__CCH_EVENT_EMITTER__).toBeUndefined();
  32. // After import, should exist
  33. const { eventEmitter } = await import("@/lib/event-emitter");
  34. expect(g.__CCH_EVENT_EMITTER__).toBe(eventEmitter);
  35. });
  36. test("requestFilterEngine: multiple imports return same instance", async () => {
  37. // First import
  38. const { requestFilterEngine: engine1 } = await import("@/lib/request-filter-engine");
  39. // Reset module cache
  40. vi.resetModules();
  41. // Second import
  42. const { requestFilterEngine: engine2 } = await import("@/lib/request-filter-engine");
  43. // Should be the exact same instance
  44. expect(engine1).toBe(engine2);
  45. });
  46. test("requestFilterEngine: globalThis stores the singleton", async () => {
  47. const g = globalThis as Record<string, unknown>;
  48. // Before import, should not exist
  49. expect(g.__CCH_REQUEST_FILTER_ENGINE__).toBeUndefined();
  50. // After import, should exist
  51. const { requestFilterEngine } = await import("@/lib/request-filter-engine");
  52. expect(g.__CCH_REQUEST_FILTER_ENGINE__).toBe(requestFilterEngine);
  53. });
  54. test("sensitiveWordDetector: multiple imports return same instance", async () => {
  55. // First import
  56. const { sensitiveWordDetector: detector1 } = await import("@/lib/sensitive-word-detector");
  57. // Reset module cache
  58. vi.resetModules();
  59. // Second import
  60. const { sensitiveWordDetector: detector2 } = await import("@/lib/sensitive-word-detector");
  61. // Should be the exact same instance
  62. expect(detector1).toBe(detector2);
  63. });
  64. test("sensitiveWordDetector: globalThis stores the singleton", async () => {
  65. const g = globalThis as Record<string, unknown>;
  66. // Before import, should not exist
  67. expect(g.__CCH_SENSITIVE_WORD_DETECTOR__).toBeUndefined();
  68. // After import, should exist
  69. const { sensitiveWordDetector } = await import("@/lib/sensitive-word-detector");
  70. expect(g.__CCH_SENSITIVE_WORD_DETECTOR__).toBe(sensitiveWordDetector);
  71. });
  72. });
  73. describe("event propagation between singleton instances", () => {
  74. const prevRuntime = process.env.NEXT_RUNTIME;
  75. beforeEach(() => {
  76. vi.resetModules();
  77. process.env.NEXT_RUNTIME = "nodejs";
  78. // Clean globalThis
  79. const g = globalThis as Record<string, unknown>;
  80. delete g.__CCH_EVENT_EMITTER__;
  81. delete g.__CCH_REQUEST_FILTER_ENGINE__;
  82. delete g.__CCH_SENSITIVE_WORD_DETECTOR__;
  83. });
  84. afterEach(() => {
  85. process.env.NEXT_RUNTIME = prevRuntime;
  86. const g = globalThis as Record<string, unknown>;
  87. delete g.__CCH_EVENT_EMITTER__;
  88. delete g.__CCH_REQUEST_FILTER_ENGINE__;
  89. delete g.__CCH_SENSITIVE_WORD_DETECTOR__;
  90. });
  91. test("events emitted in one context should be received in another", async () => {
  92. const handler = vi.fn();
  93. // Context A: subscribe to event
  94. const { eventEmitter: emitterA } = await import("@/lib/event-emitter");
  95. emitterA.on("requestFiltersUpdated", handler);
  96. // Reset modules to simulate different worker context
  97. vi.resetModules();
  98. // Context B: emit event
  99. const { eventEmitter: emitterB } = await import("@/lib/event-emitter");
  100. emitterB.emitRequestFiltersUpdated();
  101. // Handler should be called because both contexts share the same globalThis instance
  102. expect(handler).toHaveBeenCalledTimes(1);
  103. });
  104. test("all event types should work with singleton pattern", async () => {
  105. const handlers = {
  106. errorRules: vi.fn(),
  107. sensitiveWords: vi.fn(),
  108. requestFilters: vi.fn(),
  109. };
  110. // Subscribe in context A
  111. const { eventEmitter: emitterA } = await import("@/lib/event-emitter");
  112. emitterA.on("errorRulesUpdated", handlers.errorRules);
  113. emitterA.on("sensitiveWordsUpdated", handlers.sensitiveWords);
  114. emitterA.on("requestFiltersUpdated", handlers.requestFilters);
  115. vi.resetModules();
  116. // Emit in context B
  117. const { eventEmitter: emitterB } = await import("@/lib/event-emitter");
  118. emitterB.emitErrorRulesUpdated();
  119. emitterB.emitSensitiveWordsUpdated();
  120. emitterB.emitRequestFiltersUpdated();
  121. expect(handlers.errorRules).toHaveBeenCalledTimes(1);
  122. expect(handlers.sensitiveWords).toHaveBeenCalledTimes(1);
  123. expect(handlers.requestFilters).toHaveBeenCalledTimes(1);
  124. });
  125. });