thatcher 1.0.4

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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/package.json +73 -0
  4. package/src/adapters/google-auth.js +148 -0
  5. package/src/adapters/google-drive.js +209 -0
  6. package/src/app/api/[entity]/[[...path]]/route.js +33 -0
  7. package/src/app/api/audit/dashboard/route.js +77 -0
  8. package/src/app/api/audit/logs/route.js +46 -0
  9. package/src/app/api/audit/permissions/[id]/route.js +37 -0
  10. package/src/app/api/audit/permissions/route.js +93 -0
  11. package/src/app/api/audit/permissions/stats/route.js +29 -0
  12. package/src/app/api/audit/route.js +79 -0
  13. package/src/app/api/audit/stats/route.js +18 -0
  14. package/src/app/api/auth/google/callback/route.js +94 -0
  15. package/src/app/api/auth/google/route.js +58 -0
  16. package/src/app/api/auth/login/route.js +121 -0
  17. package/src/app/api/auth/logout/route.js +52 -0
  18. package/src/app/api/auth/me/route.js +16 -0
  19. package/src/app/api/auth/mwr-bridge/route.js +77 -0
  20. package/src/app/api/auth/password-reset/route.js +65 -0
  21. package/src/app/api/cron/trigger/route.js +58 -0
  22. package/src/app/api/csrf-token/route.js +8 -0
  23. package/src/app/api/debug/config/route.js +22 -0
  24. package/src/app/api/debug/hooks/route.js +16 -0
  25. package/src/app/api/debug/plugins/route.js +20 -0
  26. package/src/app/api/debug/sqlite/route.js +21 -0
  27. package/src/app/api/debug/sync/route.js +29 -0
  28. package/src/app/api/debug/workflow/route.js +20 -0
  29. package/src/app/api/domains/[domain]/route.js +26 -0
  30. package/src/app/api/domains/route.js +21 -0
  31. package/src/app/api/email/allocate/batch/route.js +106 -0
  32. package/src/app/api/email/allocate/route.js +141 -0
  33. package/src/app/api/email/receive/route.js +158 -0
  34. package/src/app/api/email/route.js +3 -0
  35. package/src/app/api/email/send/route.js +77 -0
  36. package/src/app/api/email/unallocated/route.js +47 -0
  37. package/src/app/api/files/[id]/route.js +38 -0
  38. package/src/app/api/health/route.js +95 -0
  39. package/src/app/api/metrics/route.js +73 -0
  40. package/src/app/api/monitoring/dashboard/route.js +18 -0
  41. package/src/cli.js +243 -0
  42. package/src/config/config-loader.js +112 -0
  43. package/src/config/constants.js +127 -0
  44. package/src/config/env.js +164 -0
  45. package/src/config/spec-helpers.js +232 -0
  46. package/src/engine.server.js +212 -0
  47. package/src/index.js +368 -0
  48. package/src/lib/accessibility.js +162 -0
  49. package/src/lib/action-factory.js +34 -0
  50. package/src/lib/action-utils.js +21 -0
  51. package/src/lib/alert-manager.js +189 -0
  52. package/src/lib/api-error-wrapper.js +125 -0
  53. package/src/lib/api-helpers.js +53 -0
  54. package/src/lib/api.js +82 -0
  55. package/src/lib/audit-logger-enhanced.js +117 -0
  56. package/src/lib/audit-logger.js +193 -0
  57. package/src/lib/auth-middleware.js +102 -0
  58. package/src/lib/auth-route-helpers.js +83 -0
  59. package/src/lib/business-rules-engine.js +86 -0
  60. package/src/lib/compression.js +44 -0
  61. package/src/lib/config-field-helpers.js +91 -0
  62. package/src/lib/config-generator-engine.js +445 -0
  63. package/src/lib/config-helpers.js +120 -0
  64. package/src/lib/connection-guard.js +79 -0
  65. package/src/lib/crud-action-helpers.js +34 -0
  66. package/src/lib/crud-factory.js +83 -0
  67. package/src/lib/crud-handlers.js +244 -0
  68. package/src/lib/csrf-protection.js +63 -0
  69. package/src/lib/database-core.js +258 -0
  70. package/src/lib/database-migrations.js +96 -0
  71. package/src/lib/date-utils.js +159 -0
  72. package/src/lib/db-backup.js +97 -0
  73. package/src/lib/db-monitor.js +127 -0
  74. package/src/lib/domain-loader.js +82 -0
  75. package/src/lib/email-sender.js +100 -0
  76. package/src/lib/error-boundary.js +134 -0
  77. package/src/lib/error-handler.js +84 -0
  78. package/src/lib/error-recovery.js +190 -0
  79. package/src/lib/error-resilience.js +130 -0
  80. package/src/lib/errors.js +69 -0
  81. package/src/lib/events-engine.js +182 -0
  82. package/src/lib/field-iterator.js +50 -0
  83. package/src/lib/field-registry.js +68 -0
  84. package/src/lib/field-types.js +154 -0
  85. package/src/lib/generic-crud-handler.js +32 -0
  86. package/src/lib/health-monitor.js +134 -0
  87. package/src/lib/hook-engine.js +169 -0
  88. package/src/lib/hot-reload/cache-invalidator.js +115 -0
  89. package/src/lib/hot-reload/checkpoint.js +95 -0
  90. package/src/lib/hot-reload/debug-exposure.js +67 -0
  91. package/src/lib/hot-reload/directory-watcher.js +96 -0
  92. package/src/lib/hot-reload/index.js +50 -0
  93. package/src/lib/hot-reload/mutex.js +75 -0
  94. package/src/lib/hot-reload/promise-container.js +66 -0
  95. package/src/lib/hot-reload/route-wrapper.js +46 -0
  96. package/src/lib/hot-reload/safe-error.js +51 -0
  97. package/src/lib/hot-reload/supervisor.js +161 -0
  98. package/src/lib/hot-reload/timeout-wrapper.js +52 -0
  99. package/src/lib/http-methods-factory.js +25 -0
  100. package/src/lib/index-optimizer.js +96 -0
  101. package/src/lib/index.js +35 -0
  102. package/src/lib/list-data-transform.js +39 -0
  103. package/src/lib/log-aggregator.js +116 -0
  104. package/src/lib/logger.js +55 -0
  105. package/src/lib/metrics-collector.js +102 -0
  106. package/src/lib/minifier.js +19 -0
  107. package/src/lib/monitoring-init.js +67 -0
  108. package/src/lib/next-compat.js +80 -0
  109. package/src/lib/next-polyfills.js +135 -0
  110. package/src/lib/perf-monitor.js +91 -0
  111. package/src/lib/progress-components.js +181 -0
  112. package/src/lib/query-cache.js +126 -0
  113. package/src/lib/query-engine-write.js +221 -0
  114. package/src/lib/query-engine.js +399 -0
  115. package/src/lib/query-perf.js +117 -0
  116. package/src/lib/query-string-adapter.js +75 -0
  117. package/src/lib/realtime-server.js +67 -0
  118. package/src/lib/render-cache.js +61 -0
  119. package/src/lib/request-tracker.js +43 -0
  120. package/src/lib/resource-hints.js +29 -0
  121. package/src/lib/resource-monitor.js +117 -0
  122. package/src/lib/response-formatter.js +80 -0
  123. package/src/lib/route-helpers.js +33 -0
  124. package/src/lib/route-resolver.js +142 -0
  125. package/src/lib/safe-json.js +8 -0
  126. package/src/lib/server-bootstrap.js +71 -0
  127. package/src/lib/stage-pipeline.js +153 -0
  128. package/src/lib/state-protocol.js +171 -0
  129. package/src/lib/state-transport-client.js +169 -0
  130. package/src/lib/state-transport-reconnect.js +121 -0
  131. package/src/lib/state-transport-server.js +181 -0
  132. package/src/lib/static-server.js +97 -0
  133. package/src/lib/status-helpers.js +98 -0
  134. package/src/lib/universal-handler.js +7 -0
  135. package/src/lib/utils.js +93 -0
  136. package/src/lib/validate.js +197 -0
  137. package/src/lib/validation/business-validators.js +61 -0
  138. package/src/lib/validation/csrf.js +51 -0
  139. package/src/lib/validation/file-validators.js +34 -0
  140. package/src/lib/validation/format-validators.js +106 -0
  141. package/src/lib/validation/index.js +19 -0
  142. package/src/lib/validation/rate-limit.js +31 -0
  143. package/src/lib/validation/security-validators.js +78 -0
  144. package/src/lib/validation-middleware.js +133 -0
  145. package/src/lib/validators.js +105 -0
  146. package/src/lib/with-audit-logging.js +63 -0
  147. package/src/lib/with-error-handler.js +31 -0
  148. package/src/lib/workflow-engine.js +250 -0
  149. package/src/server/server.js +305 -0
  150. package/src/services/collaborator-role.service.js +205 -0
  151. package/src/services/email-sender.js +105 -0
  152. package/src/services/notification-engine.js +110 -0
  153. package/src/services/permission.service.js +181 -0
  154. package/src/ui/advanced-search-renderer.js +42 -0
  155. package/src/ui/advanced-widgets.js +47 -0
  156. package/src/ui/auth-pages.js +114 -0
  157. package/src/ui/auth-styles.js +53 -0
  158. package/src/ui/client.js +99 -0
  159. package/src/ui/collaboration-dialogs.js +31 -0
  160. package/src/ui/common-handlers.js +156 -0
  161. package/src/ui/component-engine.js +103 -0
  162. package/src/ui/dashboard-renderer.js +150 -0
  163. package/src/ui/dialog-engine.js +147 -0
  164. package/src/ui/dialog-factory.js +38 -0
  165. package/src/ui/engagement-cards.js +76 -0
  166. package/src/ui/engagement-dialogs.js +100 -0
  167. package/src/ui/engagement-grid-renderer.js +109 -0
  168. package/src/ui/entity-renderer.js +176 -0
  169. package/src/ui/event-delegation.js +108 -0
  170. package/src/ui/fetch-json.js +24 -0
  171. package/src/ui/file-dialogs.js +81 -0
  172. package/src/ui/flexup-report-renderer.js +173 -0
  173. package/src/ui/format-helpers.js +102 -0
  174. package/src/ui/global-tags.js +144 -0
  175. package/src/ui/highlight-threading-renderer.js +176 -0
  176. package/src/ui/idle-logout.js +156 -0
  177. package/src/ui/job-management-renderer.js +61 -0
  178. package/src/ui/layout.js +219 -0
  179. package/src/ui/letter-dialogs.js +37 -0
  180. package/src/ui/ml-console-renderer.js +108 -0
  181. package/src/ui/monitoring-dashboard-client.js +136 -0
  182. package/src/ui/monitoring-dashboard.js +134 -0
  183. package/src/ui/notifications-renderer.js +51 -0
  184. package/src/ui/page-handler-admin.js +121 -0
  185. package/src/ui/page-handler-helpers.js +111 -0
  186. package/src/ui/page-handler-reviews.js +165 -0
  187. package/src/ui/page-handler-rfi.js +42 -0
  188. package/src/ui/page-handler.js +210 -0
  189. package/src/ui/password-reset-page.js +146 -0
  190. package/src/ui/perf-helpers.js +96 -0
  191. package/src/ui/perf-renderer.js +71 -0
  192. package/src/ui/permissions-ui.js +163 -0
  193. package/src/ui/picker-dialogs.js +100 -0
  194. package/src/ui/render-helpers.js +124 -0
  195. package/src/ui/renderer.js +35 -0
  196. package/src/ui/review-comparison-renderer.js +58 -0
  197. package/src/ui/review-detail-panels.js +71 -0
  198. package/src/ui/review-detail-renderer.js +170 -0
  199. package/src/ui/review-detail-script.js +95 -0
  200. package/src/ui/review-mwr-renderer.js +113 -0
  201. package/src/ui/review-renderer.js +202 -0
  202. package/src/ui/review-widgets.js +88 -0
  203. package/src/ui/review-zone-nav.js +12 -0
  204. package/src/ui/rfi-detail-renderer.js +191 -0
  205. package/src/ui/rfi-renderer.js +194 -0
  206. package/src/ui/rfi-report-renderer.js +56 -0
  207. package/src/ui/rippleui.css +1 -0
  208. package/src/ui/settings-renderer-advanced.js +195 -0
  209. package/src/ui/settings-renderer-advanced2.js +158 -0
  210. package/src/ui/settings-renderer-teams.js +112 -0
  211. package/src/ui/settings-renderer.js +166 -0
  212. package/src/ui/spacing-system.js +155 -0
  213. package/src/ui/standalone-login.js +109 -0
  214. package/src/ui/styles.css +2530 -0
  215. package/src/ui/styles2.css +1602 -0
  216. package/src/ui/test-page.js +23 -0
  217. package/src/ui/validation-rules.js +73 -0
  218. package/src/ui/validation-ui.js +147 -0
  219. package/src/ui/virtual-scroll.js +107 -0
  220. package/src/ui/webjsx.js +61 -0
  221. package/src/ui/widgets.js +152 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Hook Engine - Event system for plugins and lifecycle hooks
3
+ * Fires events like 'create:entity:after', 'update:entity:before', etc.
4
+ */
5
+
6
+ export class HookEngine {
7
+ constructor() {
8
+ this.hooks = new Map();
9
+ }
10
+
11
+ /**
12
+ * Register a hook handler
13
+ * @param {string} name - Event name
14
+ * @param {Function} callback - Handler function
15
+ * @param {object} options - { priority, once }
16
+ */
17
+ register(name, callback, options = {}) {
18
+ const { priority = 0, once = false } = options;
19
+ if (!this.hooks.has(name)) this.hooks.set(name, []);
20
+ const list = this.hooks.get(name);
21
+ list.push({ callback, priority, once });
22
+ list.sort((a, b) => b.priority - a.priority);
23
+ return this;
24
+ }
25
+
26
+ /**
27
+ * Alias for register
28
+ */
29
+ on(name, callback, options = {}) {
30
+ return this.register(name, callback, options);
31
+ }
32
+
33
+ /**
34
+ * Unregister a hook handler
35
+ */
36
+ off(name, callback) {
37
+ const list = this.hooks.get(name);
38
+ if (!list) return this;
39
+ const idx = list.findIndex(h => h.callback === callback);
40
+ if (idx !== -1) list.splice(idx, 1);
41
+ if (list.length === 0) this.hooks.delete(name);
42
+ return this;
43
+ }
44
+
45
+ /**
46
+ * Execute all handlers for an event (fire-and-forget style)
47
+ * @param {string} name
48
+ * @param {object} data
49
+ * @param {object} options - { fallthrough: true to continue on error }
50
+ * @returns {Promise<{success: boolean, data, errors}>}
51
+ */
52
+ async execute(name, data = {}, options = {}) {
53
+ const { fallthrough = true } = options;
54
+ const hooks = this.hooks.get(name);
55
+ if (!hooks || hooks.length === 0) return { success: true, data };
56
+ const errors = [];
57
+
58
+ for (const hook of [...hooks]) {
59
+ try {
60
+ await hook.callback(data);
61
+ if (hook.once) this.off(name, hook.callback);
62
+ } catch (error) {
63
+ console.error(`[HookEngine] Hook "${name}" error:`, error.message);
64
+ errors.push(error);
65
+ if (!fallthrough) throw error;
66
+ }
67
+ }
68
+
69
+ return { success: errors.length === 0, data, errors };
70
+ }
71
+
72
+ /**
73
+ * Pipe data through transformation hooks
74
+ * @param {string} name
75
+ * @param {any} data
76
+ * @returns {Promise<any>}
77
+ */
78
+ async pipe(name, data = {}) {
79
+ const hooks = this.hooks.get(name);
80
+ if (!hooks || hooks.length === 0) return data;
81
+
82
+ let current = data;
83
+ for (const hook of [...hooks]) {
84
+ try {
85
+ const result = await hook.callback(current);
86
+ if (result !== undefined) current = result;
87
+ if (hook.once) this.off(name, hook.callback);
88
+ } catch (error) {
89
+ console.error(`[HookEngine] Pipe hook "${name}" error:`, error.message);
90
+ }
91
+ }
92
+ return current;
93
+ }
94
+
95
+ /**
96
+ * Get all listeners for an event
97
+ * @param {string} name
98
+ * @returns {Array}
99
+ */
100
+ listeners(name) {
101
+ return this.hooks.has(name) ? Array.from(this.hooks.get(name)) : [];
102
+ }
103
+
104
+ /**
105
+ * Get statistics
106
+ * @returns {object} Map of event -> count
107
+ */
108
+ stats() {
109
+ const result = {};
110
+ for (const [name, list] of this.hooks.entries()) {
111
+ result[name] = list.length;
112
+ }
113
+ return result;
114
+ }
115
+
116
+ /**
117
+ * Clear all hooks
118
+ */
119
+ clear() {
120
+ this.hooks.clear();
121
+ }
122
+ }
123
+
124
+ // Global singleton
125
+ export const hookEngine = new HookEngine();
126
+
127
+ /**
128
+ * Execute hook (convenience function)
129
+ */
130
+ export async function executeHook(name, data = {}, options = {}) {
131
+ return hookEngine.execute(name, data, options);
132
+ }
133
+
134
+ /**
135
+ * Pipe through hooks
136
+ */
137
+ export async function pipeHook(name, data = {}) {
138
+ return hookEngine.pipe(name, data);
139
+ }
140
+
141
+ /**
142
+ * Register hook debug exposure
143
+ */
144
+ function registerHookDebug() {
145
+ if (globalThis.__debug__) {
146
+ globalThis.__debug__.expose('hooks', {
147
+ stats: () => hookEngine.stats(),
148
+ engine: hookEngine,
149
+ }, 'HookEngine registry');
150
+ }
151
+ }
152
+
153
+ // Auto-register when __debug__ becomes available
154
+ if (globalThis.__debug__) {
155
+ registerHookDebug();
156
+ } else {
157
+ const origDesc = Object.getOwnPropertyDescriptor(globalThis, '__debug__');
158
+ Object.defineProperty(globalThis, '__debug__', {
159
+ configurable: true,
160
+ set(v) {
161
+ Object.defineProperty(globalThis, '__debug__', { value: v, configurable: true, writable: true });
162
+ registerHookDebug();
163
+ if (origDesc?.set) origDesc.set.call(this, v);
164
+ },
165
+ get() { return undefined; }
166
+ });
167
+ }
168
+
169
+ export default hookEngine;
@@ -0,0 +1,115 @@
1
+ import path from 'path';
2
+ import { createRequire } from 'module';
3
+ import { DirectoryWatcher } from './directory-watcher.js';
4
+
5
+ const _require = createRequire(import.meta.url);
6
+
7
+ export class CacheInvalidator {
8
+ constructor() {
9
+ this.invalidated = new Set();
10
+ this.watcher = null;
11
+ this.onInvalidate = null;
12
+ }
13
+
14
+ startWatching(rootDir) {
15
+ if (this.watcher) return this;
16
+ this.watcher = new DirectoryWatcher({ rootDir });
17
+ this.watcher.on('change', (event) => {
18
+ const count = this.invalidateByFilePath(event.path);
19
+ if (count > 0 && this.onInvalidate) {
20
+ this.onInvalidate(event.path, count);
21
+ }
22
+ });
23
+ this.watcher.start();
24
+ return this;
25
+ }
26
+
27
+ stopWatching() {
28
+ if (this.watcher) {
29
+ this.watcher.stop();
30
+ this.watcher = null;
31
+ }
32
+ return this;
33
+ }
34
+
35
+ invalidateByFilePath(filePath) {
36
+ const normalized = path.resolve(filePath);
37
+ let count = 0;
38
+ if (typeof _require.cache === 'undefined') return count;
39
+ for (const modulePath of Object.keys(_require.cache)) {
40
+ if (modulePath === normalized || modulePath.startsWith(normalized)) {
41
+ this._invalidateModule(modulePath);
42
+ this.invalidated.add(modulePath);
43
+ count++;
44
+ }
45
+ }
46
+ return count;
47
+ }
48
+
49
+ invalidate(modulePath) {
50
+ try {
51
+ const resolved = _require.resolve(modulePath);
52
+ if (_require.cache[resolved]) {
53
+ this._invalidateModule(resolved);
54
+ this.invalidated.add(resolved);
55
+ return true;
56
+ }
57
+ } catch (_) {}
58
+ return false;
59
+ }
60
+
61
+ invalidatePattern(pattern) {
62
+ const regex = new RegExp(pattern);
63
+ let count = 0;
64
+ if (typeof _require.cache === 'undefined') return count;
65
+ for (const modulePath of Object.keys(_require.cache)) {
66
+ if (regex.test(modulePath)) {
67
+ this._invalidateModule(modulePath);
68
+ this.invalidated.add(modulePath);
69
+ count++;
70
+ }
71
+ }
72
+ return count;
73
+ }
74
+
75
+ invalidateAll(excludePattern = /node_modules/) {
76
+ let count = 0;
77
+ if (typeof _require.cache === 'undefined') return count;
78
+ for (const modulePath of Object.keys(_require.cache)) {
79
+ if (excludePattern && excludePattern.test(modulePath)) continue;
80
+ this._invalidateModule(modulePath);
81
+ this.invalidated.add(modulePath);
82
+ count++;
83
+ }
84
+ return count;
85
+ }
86
+
87
+ _invalidateModule(modulePath) {
88
+ if (typeof _require.cache === 'undefined') return;
89
+ const mod = _require.cache[modulePath];
90
+ if (!mod) return;
91
+ if (mod.parent) {
92
+ const index = mod.parent.children.indexOf(mod);
93
+ if (index !== -1) mod.parent.children.splice(index, 1);
94
+ }
95
+ for (const child of mod.children || []) {
96
+ this._invalidateModule(child.id);
97
+ }
98
+ delete _require.cache[modulePath];
99
+ }
100
+
101
+ getStats() {
102
+ return {
103
+ totalCached: typeof _require.cache !== 'undefined' ? Object.keys(_require.cache).length : 0,
104
+ invalidated: this.invalidated.size,
105
+ watching: !!this.watcher,
106
+ watcherStats: this.watcher ? this.watcher.getStats() : null
107
+ };
108
+ }
109
+
110
+ clearStats() {
111
+ this.invalidated.clear();
112
+ }
113
+ }
114
+
115
+ export const globalInvalidator = new CacheInvalidator();
@@ -0,0 +1,95 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export class CheckpointManager {
5
+ constructor(options = {}) {
6
+ this.checkpointDir = options.checkpointDir || path.join(process.cwd(), 'data', 'checkpoints');
7
+ this.maxCheckpoints = options.maxCheckpoints || 10;
8
+ this.checkpoints = new Map();
9
+
10
+ if (!fs.existsSync(this.checkpointDir)) {
11
+ fs.mkdirSync(this.checkpointDir, { recursive: true });
12
+ }
13
+ }
14
+
15
+ async save(key, state) {
16
+ const checkpoint = { key, state, timestamp: Date.now(), version: 1 };
17
+ this.checkpoints.set(key, checkpoint);
18
+ const filename = this._getFilename(key);
19
+ const filepath = path.join(this.checkpointDir, filename);
20
+
21
+ try {
22
+ await fs.promises.writeFile(filepath, JSON.stringify(checkpoint, null, 2), 'utf-8');
23
+ await this._pruneOldCheckpoints(key);
24
+ return true;
25
+ } catch (err) {
26
+ console.error(`[Checkpoint] Failed to save ${key}:`, err);
27
+ return false;
28
+ }
29
+ }
30
+
31
+ async load(key) {
32
+ if (this.checkpoints.has(key)) {
33
+ return this.checkpoints.get(key).state;
34
+ }
35
+ const files = await this._getCheckpointFiles(key);
36
+ if (files.length === 0) return null;
37
+ const latest = files[files.length - 1];
38
+ const filepath = path.join(this.checkpointDir, latest);
39
+ try {
40
+ const data = await fs.promises.readFile(filepath, 'utf-8');
41
+ const checkpoint = JSON.parse(data);
42
+ this.checkpoints.set(key, checkpoint);
43
+ return checkpoint.state;
44
+ } catch (err) {
45
+ console.error(`[Checkpoint] Failed to load ${key}:`, err);
46
+ return null;
47
+ }
48
+ }
49
+
50
+ async restore(key, defaultState = null) {
51
+ const state = await this.load(key);
52
+ return state !== null ? state : defaultState;
53
+ }
54
+
55
+ async clear(key) {
56
+ this.checkpoints.delete(key);
57
+ const files = await this._getCheckpointFiles(key);
58
+ await Promise.all(files.map(f => fs.promises.unlink(path.join(this.checkpointDir, f))));
59
+ }
60
+
61
+ async clearAll() {
62
+ this.checkpoints.clear();
63
+ const files = await fs.promises.readdir(this.checkpointDir);
64
+ await Promise.all(files.map(f => fs.promises.unlink(path.join(this.checkpointDir, f))));
65
+ }
66
+
67
+ _getFilename(key) {
68
+ const safe = key.replace(/[^a-z0-9-_]/gi, '_');
69
+ return `${safe}_${Date.now()}.json`;
70
+ }
71
+
72
+ async _getCheckpointFiles(key) {
73
+ const safe = key.replace(/[^a-z0-9-_]/gi, '_');
74
+ const prefix = `${safe}_`;
75
+ const files = await fs.promises.readdir(this.checkpointDir);
76
+ return files.filter(f => f.startsWith(prefix) && f.endsWith('.json')).sort();
77
+ }
78
+
79
+ async _pruneOldCheckpoints(key) {
80
+ const files = await this._getCheckpointFiles(key);
81
+ if (files.length <= this.maxCheckpoints) return;
82
+ const toDelete = files.slice(0, files.length - this.maxCheckpoints);
83
+ await Promise.all(toDelete.map(f => fs.promises.unlink(path.join(this.checkpointDir, f))));
84
+ }
85
+
86
+ getStats() {
87
+ return {
88
+ inMemory: this.checkpoints.size,
89
+ checkpointDir: this.checkpointDir,
90
+ maxCheckpoints: this.maxCheckpoints
91
+ };
92
+ }
93
+ }
94
+
95
+ export const globalCheckpoint = new CheckpointManager();
@@ -0,0 +1,67 @@
1
+ export class DebugExposure {
2
+ constructor() {
3
+ this.exposed = new Map();
4
+ globalThis.__debug__ = this;
5
+ }
6
+
7
+ expose(key, value, description = '') {
8
+ this.exposed.set(key, {
9
+ value,
10
+ description,
11
+ exposedAt: new Date().toISOString()
12
+ });
13
+ if (!globalThis[key]) globalThis[key] = value;
14
+ return value;
15
+ }
16
+
17
+ get(key) {
18
+ const entry = this.exposed.get(key);
19
+ return entry ? entry.value : undefined;
20
+ }
21
+
22
+ list() {
23
+ const list = [];
24
+ for (const [key, entry] of this.exposed.entries()) {
25
+ list.push({
26
+ key,
27
+ description: entry.description,
28
+ exposedAt: entry.exposedAt,
29
+ type: typeof entry.value
30
+ });
31
+ }
32
+ return list;
33
+ }
34
+
35
+ inspect(key) {
36
+ const entry = this.exposed.get(key);
37
+ if (!entry) return null;
38
+ const value = entry.value;
39
+ const info = {
40
+ key,
41
+ description: entry.description,
42
+ exposedAt: entry.exposedAt,
43
+ type: typeof value,
44
+ constructor: value?.constructor?.name
45
+ };
46
+ if (typeof value === 'object' && value !== null) {
47
+ if (typeof value.getStats === 'function') info.stats = value.getStats();
48
+ if (typeof value.getState === 'function') info.state = value.getState();
49
+ info.keys = Object.keys(value);
50
+ }
51
+ return info;
52
+ }
53
+
54
+ remove(key) {
55
+ this.exposed.delete(key);
56
+ if (globalThis[key]) delete globalThis[key];
57
+ }
58
+
59
+ clear() {
60
+ for (const key of this.exposed.keys()) {
61
+ this.remove(key);
62
+ }
63
+ }
64
+ }
65
+
66
+ export const globalDebug = new DebugExposure();
67
+ export const expose = globalDebug.expose.bind(globalDebug);
@@ -0,0 +1,96 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { EventEmitter } from 'events';
4
+
5
+ export class DirectoryWatcher extends EventEmitter {
6
+ constructor(options = {}) {
7
+ super();
8
+ this.rootDir = options.rootDir || path.join(process.cwd(), 'src');
9
+ this.debounceMs = options.debounceMs || 100;
10
+ this.extensions = options.extensions || ['.js', '.jsx', '.ts', '.tsx'];
11
+ this.exclude = options.exclude || ['node_modules', '.git', 'migration'];
12
+ this.watchers = new Map();
13
+ this.debounceTimers = new Map();
14
+ this.running = false;
15
+ this.stats = { totalWatched: 0, eventsReceived: 0, invalidations: 0 };
16
+ }
17
+
18
+ start() {
19
+ if (this.running) return this;
20
+ this.running = true;
21
+ this._watchRecursive(this.rootDir);
22
+ this.emit('started', { directories: this.watchers.size });
23
+ return this;
24
+ }
25
+
26
+ stop() {
27
+ this.running = false;
28
+ for (const [, watcher] of this.watchers.entries()) {
29
+ try { watcher.close(); } catch (_) {}
30
+ }
31
+ this.watchers.clear();
32
+ for (const timer of this.debounceTimers.values()) {
33
+ clearTimeout(timer);
34
+ }
35
+ this.debounceTimers.clear();
36
+ this.emit('stopped');
37
+ return this;
38
+ }
39
+
40
+ _watchRecursive(dir) {
41
+ if (!this.running) return;
42
+ if (!fs.existsSync(dir)) return;
43
+
44
+ try {
45
+ const watcher = fs.watch(dir, { persistent: false }, (eventType, filename) => {
46
+ if (!filename) return;
47
+ this._handleChange(dir, filename, eventType);
48
+ });
49
+ watcher.on('error', (err) => {
50
+ this.emit('watchError', { dir, error: err });
51
+ this.watchers.delete(dir);
52
+ });
53
+ this.watchers.set(dir, watcher);
54
+ this.stats.totalWatched++;
55
+ } catch (err) {
56
+ this.emit('watchError', { dir, error: err });
57
+ return;
58
+ }
59
+
60
+ try {
61
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
62
+ for (const entry of entries) {
63
+ if (!entry.isDirectory()) continue;
64
+ if (this.exclude.includes(entry.name)) continue;
65
+ if (entry.name.startsWith('.')) continue;
66
+ this._watchRecursive(path.join(dir, entry.name));
67
+ }
68
+ } catch (_) {}
69
+ }
70
+
71
+ _handleChange(dir, filename, eventType) {
72
+ const ext = path.extname(filename);
73
+ if (!this.extensions.includes(ext)) return;
74
+ this.stats.eventsReceived++;
75
+ const fullPath = path.join(dir, filename);
76
+ if (this.debounceTimers.has(fullPath)) {
77
+ clearTimeout(this.debounceTimers.get(fullPath));
78
+ }
79
+ this.debounceTimers.set(fullPath, setTimeout(() => {
80
+ this.debounceTimers.delete(fullPath);
81
+ this.stats.invalidations++;
82
+ this.emit('change', { path: fullPath, dir, filename, eventType, timestamp: Date.now() });
83
+ }, this.debounceMs));
84
+ }
85
+
86
+ getStats() {
87
+ return {
88
+ running: this.running,
89
+ directoriesWatched: this.watchers.size,
90
+ pendingDebounce: this.debounceTimers.size,
91
+ ...this.stats
92
+ };
93
+ }
94
+ }
95
+
96
+ export const globalWatcher = new DirectoryWatcher();
@@ -0,0 +1,50 @@
1
+ import { createRequire } from 'module';
2
+ import { fileURLToPath } from 'url';
3
+ import path from 'path';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const require = createRequire(import.meta.url);
8
+
9
+ const promiseContainerMod = require('./promise-container.js');
10
+ export { Mutex, MutexManager, globalManager } from './mutex.js';
11
+ import { Supervisor, SupervisorTree, globalTree } from './supervisor.js';
12
+ const checkpointMod = require('./checkpoint.js');
13
+ const timeoutMod = require('./timeout-wrapper.js');
14
+ const safeErrorMod = require('./safe-error.js');
15
+ const cacheInvalidatorMod = require('./cache-invalidator.js');
16
+ const directoryWatcherMod = require('./directory-watcher.js');
17
+ const routeWrapperMod = require('./route-wrapper.js');
18
+ const debugExposureMod = require('./debug-exposure.js');
19
+
20
+ const { PromiseContainer, globalContainer, contain } = promiseContainerMod;
21
+
22
+ const { CheckpointManager, globalCheckpoint } = checkpointMod;
23
+ const { TimeoutError, withTimeout, withAbortableTimeout, retry } = timeoutMod;
24
+ const { safeError, safeStringify } = safeErrorMod;
25
+ const { CacheInvalidator, globalInvalidator } = cacheInvalidatorMod;
26
+ const { DirectoryWatcher, globalWatcher } = directoryWatcherMod;
27
+ const { wrapRouteHandler, wrapRouteHandlers } = routeWrapperMod;
28
+ const { DebugExposure, globalDebug, expose } = debugExposureMod;
29
+
30
+ expose('hotReload', {
31
+ promises: globalContainer,
32
+ mutexes: globalManager,
33
+ supervisors: globalTree,
34
+ checkpoints: globalCheckpoint,
35
+ cache: globalInvalidator,
36
+ watcher: globalWatcher,
37
+ debug: globalDebug
38
+ }, 'Hot reload infrastructure');
39
+
40
+ export {
41
+ PromiseContainer, globalContainer, contain,
42
+ Supervisor, SupervisorTree, globalTree,
43
+ CheckpointManager, globalCheckpoint,
44
+ TimeoutError, withTimeout, withAbortableTimeout, retry,
45
+ safeError, safeStringify,
46
+ CacheInvalidator, globalInvalidator,
47
+ DirectoryWatcher, globalWatcher,
48
+ wrapRouteHandler, wrapRouteHandlers,
49
+ DebugExposure, globalDebug, expose
50
+ };
@@ -0,0 +1,75 @@
1
+ export class Mutex {
2
+ constructor(name = 'anonymous') {
3
+ this.name = name;
4
+ this.locked = false;
5
+ this.queue = [];
6
+ }
7
+
8
+ async acquire() {
9
+ if (!this.locked) {
10
+ this.locked = true;
11
+ return () => this.release();
12
+ }
13
+
14
+ return new Promise(resolve => {
15
+ this.queue.push(resolve);
16
+ });
17
+ }
18
+
19
+ release() {
20
+ if (this.queue.length > 0) {
21
+ const next = this.queue.shift();
22
+ next(() => this.release());
23
+ } else {
24
+ this.locked = false;
25
+ }
26
+ }
27
+
28
+ async runExclusive(fn) {
29
+ const release = await this.acquire();
30
+ try {
31
+ return await fn();
32
+ } finally {
33
+ release();
34
+ }
35
+ }
36
+
37
+ isLocked() {
38
+ return this.locked;
39
+ }
40
+
41
+ getQueueSize() {
42
+ return this.queue.length;
43
+ }
44
+ }
45
+
46
+ export class MutexManager {
47
+ constructor() {
48
+ this.mutexes = new Map();
49
+ }
50
+
51
+ get(key) {
52
+ if (!this.mutexes.has(key)) {
53
+ this.mutexes.set(key, new Mutex(key));
54
+ }
55
+ return this.mutexes.get(key);
56
+ }
57
+
58
+ async lock(key, fn) {
59
+ const mutex = this.get(key);
60
+ return mutex.runExclusive(fn);
61
+ }
62
+
63
+ getStats() {
64
+ const stats = {};
65
+ for (const [key, mutex] of this.mutexes.entries()) {
66
+ stats[key] = {
67
+ locked: mutex.isLocked(),
68
+ queueSize: mutex.getQueueSize()
69
+ };
70
+ }
71
+ return stats;
72
+ }
73
+ }
74
+
75
+ export const globalManager = new MutexManager();