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.
- package/LICENSE +21 -0
- package/README.md +398 -0
- package/package.json +73 -0
- package/src/adapters/google-auth.js +148 -0
- package/src/adapters/google-drive.js +209 -0
- package/src/app/api/[entity]/[[...path]]/route.js +33 -0
- package/src/app/api/audit/dashboard/route.js +77 -0
- package/src/app/api/audit/logs/route.js +46 -0
- package/src/app/api/audit/permissions/[id]/route.js +37 -0
- package/src/app/api/audit/permissions/route.js +93 -0
- package/src/app/api/audit/permissions/stats/route.js +29 -0
- package/src/app/api/audit/route.js +79 -0
- package/src/app/api/audit/stats/route.js +18 -0
- package/src/app/api/auth/google/callback/route.js +94 -0
- package/src/app/api/auth/google/route.js +58 -0
- package/src/app/api/auth/login/route.js +121 -0
- package/src/app/api/auth/logout/route.js +52 -0
- package/src/app/api/auth/me/route.js +16 -0
- package/src/app/api/auth/mwr-bridge/route.js +77 -0
- package/src/app/api/auth/password-reset/route.js +65 -0
- package/src/app/api/cron/trigger/route.js +58 -0
- package/src/app/api/csrf-token/route.js +8 -0
- package/src/app/api/debug/config/route.js +22 -0
- package/src/app/api/debug/hooks/route.js +16 -0
- package/src/app/api/debug/plugins/route.js +20 -0
- package/src/app/api/debug/sqlite/route.js +21 -0
- package/src/app/api/debug/sync/route.js +29 -0
- package/src/app/api/debug/workflow/route.js +20 -0
- package/src/app/api/domains/[domain]/route.js +26 -0
- package/src/app/api/domains/route.js +21 -0
- package/src/app/api/email/allocate/batch/route.js +106 -0
- package/src/app/api/email/allocate/route.js +141 -0
- package/src/app/api/email/receive/route.js +158 -0
- package/src/app/api/email/route.js +3 -0
- package/src/app/api/email/send/route.js +77 -0
- package/src/app/api/email/unallocated/route.js +47 -0
- package/src/app/api/files/[id]/route.js +38 -0
- package/src/app/api/health/route.js +95 -0
- package/src/app/api/metrics/route.js +73 -0
- package/src/app/api/monitoring/dashboard/route.js +18 -0
- package/src/cli.js +243 -0
- package/src/config/config-loader.js +112 -0
- package/src/config/constants.js +127 -0
- package/src/config/env.js +164 -0
- package/src/config/spec-helpers.js +232 -0
- package/src/engine.server.js +212 -0
- package/src/index.js +368 -0
- package/src/lib/accessibility.js +162 -0
- package/src/lib/action-factory.js +34 -0
- package/src/lib/action-utils.js +21 -0
- package/src/lib/alert-manager.js +189 -0
- package/src/lib/api-error-wrapper.js +125 -0
- package/src/lib/api-helpers.js +53 -0
- package/src/lib/api.js +82 -0
- package/src/lib/audit-logger-enhanced.js +117 -0
- package/src/lib/audit-logger.js +193 -0
- package/src/lib/auth-middleware.js +102 -0
- package/src/lib/auth-route-helpers.js +83 -0
- package/src/lib/business-rules-engine.js +86 -0
- package/src/lib/compression.js +44 -0
- package/src/lib/config-field-helpers.js +91 -0
- package/src/lib/config-generator-engine.js +445 -0
- package/src/lib/config-helpers.js +120 -0
- package/src/lib/connection-guard.js +79 -0
- package/src/lib/crud-action-helpers.js +34 -0
- package/src/lib/crud-factory.js +83 -0
- package/src/lib/crud-handlers.js +244 -0
- package/src/lib/csrf-protection.js +63 -0
- package/src/lib/database-core.js +258 -0
- package/src/lib/database-migrations.js +96 -0
- package/src/lib/date-utils.js +159 -0
- package/src/lib/db-backup.js +97 -0
- package/src/lib/db-monitor.js +127 -0
- package/src/lib/domain-loader.js +82 -0
- package/src/lib/email-sender.js +100 -0
- package/src/lib/error-boundary.js +134 -0
- package/src/lib/error-handler.js +84 -0
- package/src/lib/error-recovery.js +190 -0
- package/src/lib/error-resilience.js +130 -0
- package/src/lib/errors.js +69 -0
- package/src/lib/events-engine.js +182 -0
- package/src/lib/field-iterator.js +50 -0
- package/src/lib/field-registry.js +68 -0
- package/src/lib/field-types.js +154 -0
- package/src/lib/generic-crud-handler.js +32 -0
- package/src/lib/health-monitor.js +134 -0
- package/src/lib/hook-engine.js +169 -0
- package/src/lib/hot-reload/cache-invalidator.js +115 -0
- package/src/lib/hot-reload/checkpoint.js +95 -0
- package/src/lib/hot-reload/debug-exposure.js +67 -0
- package/src/lib/hot-reload/directory-watcher.js +96 -0
- package/src/lib/hot-reload/index.js +50 -0
- package/src/lib/hot-reload/mutex.js +75 -0
- package/src/lib/hot-reload/promise-container.js +66 -0
- package/src/lib/hot-reload/route-wrapper.js +46 -0
- package/src/lib/hot-reload/safe-error.js +51 -0
- package/src/lib/hot-reload/supervisor.js +161 -0
- package/src/lib/hot-reload/timeout-wrapper.js +52 -0
- package/src/lib/http-methods-factory.js +25 -0
- package/src/lib/index-optimizer.js +96 -0
- package/src/lib/index.js +35 -0
- package/src/lib/list-data-transform.js +39 -0
- package/src/lib/log-aggregator.js +116 -0
- package/src/lib/logger.js +55 -0
- package/src/lib/metrics-collector.js +102 -0
- package/src/lib/minifier.js +19 -0
- package/src/lib/monitoring-init.js +67 -0
- package/src/lib/next-compat.js +80 -0
- package/src/lib/next-polyfills.js +135 -0
- package/src/lib/perf-monitor.js +91 -0
- package/src/lib/progress-components.js +181 -0
- package/src/lib/query-cache.js +126 -0
- package/src/lib/query-engine-write.js +221 -0
- package/src/lib/query-engine.js +399 -0
- package/src/lib/query-perf.js +117 -0
- package/src/lib/query-string-adapter.js +75 -0
- package/src/lib/realtime-server.js +67 -0
- package/src/lib/render-cache.js +61 -0
- package/src/lib/request-tracker.js +43 -0
- package/src/lib/resource-hints.js +29 -0
- package/src/lib/resource-monitor.js +117 -0
- package/src/lib/response-formatter.js +80 -0
- package/src/lib/route-helpers.js +33 -0
- package/src/lib/route-resolver.js +142 -0
- package/src/lib/safe-json.js +8 -0
- package/src/lib/server-bootstrap.js +71 -0
- package/src/lib/stage-pipeline.js +153 -0
- package/src/lib/state-protocol.js +171 -0
- package/src/lib/state-transport-client.js +169 -0
- package/src/lib/state-transport-reconnect.js +121 -0
- package/src/lib/state-transport-server.js +181 -0
- package/src/lib/static-server.js +97 -0
- package/src/lib/status-helpers.js +98 -0
- package/src/lib/universal-handler.js +7 -0
- package/src/lib/utils.js +93 -0
- package/src/lib/validate.js +197 -0
- package/src/lib/validation/business-validators.js +61 -0
- package/src/lib/validation/csrf.js +51 -0
- package/src/lib/validation/file-validators.js +34 -0
- package/src/lib/validation/format-validators.js +106 -0
- package/src/lib/validation/index.js +19 -0
- package/src/lib/validation/rate-limit.js +31 -0
- package/src/lib/validation/security-validators.js +78 -0
- package/src/lib/validation-middleware.js +133 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/with-audit-logging.js +63 -0
- package/src/lib/with-error-handler.js +31 -0
- package/src/lib/workflow-engine.js +250 -0
- package/src/server/server.js +305 -0
- package/src/services/collaborator-role.service.js +205 -0
- package/src/services/email-sender.js +105 -0
- package/src/services/notification-engine.js +110 -0
- package/src/services/permission.service.js +181 -0
- package/src/ui/advanced-search-renderer.js +42 -0
- package/src/ui/advanced-widgets.js +47 -0
- package/src/ui/auth-pages.js +114 -0
- package/src/ui/auth-styles.js +53 -0
- package/src/ui/client.js +99 -0
- package/src/ui/collaboration-dialogs.js +31 -0
- package/src/ui/common-handlers.js +156 -0
- package/src/ui/component-engine.js +103 -0
- package/src/ui/dashboard-renderer.js +150 -0
- package/src/ui/dialog-engine.js +147 -0
- package/src/ui/dialog-factory.js +38 -0
- package/src/ui/engagement-cards.js +76 -0
- package/src/ui/engagement-dialogs.js +100 -0
- package/src/ui/engagement-grid-renderer.js +109 -0
- package/src/ui/entity-renderer.js +176 -0
- package/src/ui/event-delegation.js +108 -0
- package/src/ui/fetch-json.js +24 -0
- package/src/ui/file-dialogs.js +81 -0
- package/src/ui/flexup-report-renderer.js +173 -0
- package/src/ui/format-helpers.js +102 -0
- package/src/ui/global-tags.js +144 -0
- package/src/ui/highlight-threading-renderer.js +176 -0
- package/src/ui/idle-logout.js +156 -0
- package/src/ui/job-management-renderer.js +61 -0
- package/src/ui/layout.js +219 -0
- package/src/ui/letter-dialogs.js +37 -0
- package/src/ui/ml-console-renderer.js +108 -0
- package/src/ui/monitoring-dashboard-client.js +136 -0
- package/src/ui/monitoring-dashboard.js +134 -0
- package/src/ui/notifications-renderer.js +51 -0
- package/src/ui/page-handler-admin.js +121 -0
- package/src/ui/page-handler-helpers.js +111 -0
- package/src/ui/page-handler-reviews.js +165 -0
- package/src/ui/page-handler-rfi.js +42 -0
- package/src/ui/page-handler.js +210 -0
- package/src/ui/password-reset-page.js +146 -0
- package/src/ui/perf-helpers.js +96 -0
- package/src/ui/perf-renderer.js +71 -0
- package/src/ui/permissions-ui.js +163 -0
- package/src/ui/picker-dialogs.js +100 -0
- package/src/ui/render-helpers.js +124 -0
- package/src/ui/renderer.js +35 -0
- package/src/ui/review-comparison-renderer.js +58 -0
- package/src/ui/review-detail-panels.js +71 -0
- package/src/ui/review-detail-renderer.js +170 -0
- package/src/ui/review-detail-script.js +95 -0
- package/src/ui/review-mwr-renderer.js +113 -0
- package/src/ui/review-renderer.js +202 -0
- package/src/ui/review-widgets.js +88 -0
- package/src/ui/review-zone-nav.js +12 -0
- package/src/ui/rfi-detail-renderer.js +191 -0
- package/src/ui/rfi-renderer.js +194 -0
- package/src/ui/rfi-report-renderer.js +56 -0
- package/src/ui/rippleui.css +1 -0
- package/src/ui/settings-renderer-advanced.js +195 -0
- package/src/ui/settings-renderer-advanced2.js +158 -0
- package/src/ui/settings-renderer-teams.js +112 -0
- package/src/ui/settings-renderer.js +166 -0
- package/src/ui/spacing-system.js +155 -0
- package/src/ui/standalone-login.js +109 -0
- package/src/ui/styles.css +2530 -0
- package/src/ui/styles2.css +1602 -0
- package/src/ui/test-page.js +23 -0
- package/src/ui/validation-rules.js +73 -0
- package/src/ui/validation-ui.js +147 -0
- package/src/ui/virtual-scroll.js +107 -0
- package/src/ui/webjsx.js +61 -0
- 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();
|