@flowdot.ai/daemon 1.0.0
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 +45 -0
- package/README.md +51 -0
- package/dist/goals/DependencyResolver.d.ts +54 -0
- package/dist/goals/DependencyResolver.js +329 -0
- package/dist/goals/ErrorRecovery.d.ts +133 -0
- package/dist/goals/ErrorRecovery.js +489 -0
- package/dist/goals/GoalApiClient.d.ts +81 -0
- package/dist/goals/GoalApiClient.js +743 -0
- package/dist/goals/GoalCache.d.ts +65 -0
- package/dist/goals/GoalCache.js +243 -0
- package/dist/goals/GoalCommsHandler.d.ts +150 -0
- package/dist/goals/GoalCommsHandler.js +378 -0
- package/dist/goals/GoalExporter.d.ts +164 -0
- package/dist/goals/GoalExporter.js +318 -0
- package/dist/goals/GoalImporter.d.ts +107 -0
- package/dist/goals/GoalImporter.js +345 -0
- package/dist/goals/GoalManager.d.ts +110 -0
- package/dist/goals/GoalManager.js +535 -0
- package/dist/goals/GoalReporter.d.ts +105 -0
- package/dist/goals/GoalReporter.js +534 -0
- package/dist/goals/GoalScheduler.d.ts +102 -0
- package/dist/goals/GoalScheduler.js +209 -0
- package/dist/goals/GoalValidator.d.ts +72 -0
- package/dist/goals/GoalValidator.js +657 -0
- package/dist/goals/MetaGoalEnforcer.d.ts +111 -0
- package/dist/goals/MetaGoalEnforcer.js +536 -0
- package/dist/goals/MilestoneBreaker.d.ts +74 -0
- package/dist/goals/MilestoneBreaker.js +348 -0
- package/dist/goals/PermissionBridge.d.ts +109 -0
- package/dist/goals/PermissionBridge.js +326 -0
- package/dist/goals/ProgressTracker.d.ts +113 -0
- package/dist/goals/ProgressTracker.js +324 -0
- package/dist/goals/ReviewScheduler.d.ts +106 -0
- package/dist/goals/ReviewScheduler.js +360 -0
- package/dist/goals/TaskExecutor.d.ts +116 -0
- package/dist/goals/TaskExecutor.js +370 -0
- package/dist/goals/TaskFeedback.d.ts +126 -0
- package/dist/goals/TaskFeedback.js +402 -0
- package/dist/goals/TaskGenerator.d.ts +75 -0
- package/dist/goals/TaskGenerator.js +329 -0
- package/dist/goals/TaskQueue.d.ts +84 -0
- package/dist/goals/TaskQueue.js +331 -0
- package/dist/goals/TaskSanitizer.d.ts +61 -0
- package/dist/goals/TaskSanitizer.js +464 -0
- package/dist/goals/errors.d.ts +116 -0
- package/dist/goals/errors.js +299 -0
- package/dist/goals/index.d.ts +24 -0
- package/dist/goals/index.js +23 -0
- package/dist/goals/types.d.ts +395 -0
- package/dist/goals/types.js +230 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/loop/DaemonIPC.d.ts +67 -0
- package/dist/loop/DaemonIPC.js +358 -0
- package/dist/loop/IntervalParser.d.ts +39 -0
- package/dist/loop/IntervalParser.js +217 -0
- package/dist/loop/LoopDaemon.d.ts +123 -0
- package/dist/loop/LoopDaemon.js +1821 -0
- package/dist/loop/LoopExecutor.d.ts +93 -0
- package/dist/loop/LoopExecutor.js +326 -0
- package/dist/loop/LoopManager.d.ts +79 -0
- package/dist/loop/LoopManager.js +476 -0
- package/dist/loop/LoopScheduler.d.ts +69 -0
- package/dist/loop/LoopScheduler.js +329 -0
- package/dist/loop/LoopStore.d.ts +57 -0
- package/dist/loop/LoopStore.js +406 -0
- package/dist/loop/LoopValidator.d.ts +55 -0
- package/dist/loop/LoopValidator.js +603 -0
- package/dist/loop/errors.d.ts +115 -0
- package/dist/loop/errors.js +312 -0
- package/dist/loop/index.d.ts +11 -0
- package/dist/loop/index.js +10 -0
- package/dist/loop/notifications/Notifier.d.ts +28 -0
- package/dist/loop/notifications/Notifier.js +78 -0
- package/dist/loop/notifications/SlackNotifier.d.ts +28 -0
- package/dist/loop/notifications/SlackNotifier.js +203 -0
- package/dist/loop/notifications/TerminalNotifier.d.ts +18 -0
- package/dist/loop/notifications/TerminalNotifier.js +72 -0
- package/dist/loop/notifications/WebhookNotifier.d.ts +24 -0
- package/dist/loop/notifications/WebhookNotifier.js +123 -0
- package/dist/loop/notifications/index.d.ts +24 -0
- package/dist/loop/notifications/index.js +109 -0
- package/dist/loop/types.d.ts +280 -0
- package/dist/loop/types.js +222 -0
- package/package.json +92 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { Logger } from './types.js';
|
|
3
|
+
export type RequestHandler = (method: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
4
|
+
export interface IPCServerOptions {
|
|
5
|
+
port?: number;
|
|
6
|
+
host?: string;
|
|
7
|
+
onRequest?: RequestHandler;
|
|
8
|
+
logger?: Logger;
|
|
9
|
+
}
|
|
10
|
+
export interface IPCClientOptions {
|
|
11
|
+
port?: number;
|
|
12
|
+
host?: string;
|
|
13
|
+
connectionTimeout?: number;
|
|
14
|
+
responseTimeout?: number;
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
}
|
|
17
|
+
export declare class IPCServer extends EventEmitter {
|
|
18
|
+
private server;
|
|
19
|
+
private clients;
|
|
20
|
+
private readonly port;
|
|
21
|
+
private readonly host;
|
|
22
|
+
private readonly onRequest;
|
|
23
|
+
private readonly logger;
|
|
24
|
+
constructor(options?: IPCServerOptions);
|
|
25
|
+
start(): Promise<void>;
|
|
26
|
+
stop(): Promise<void>;
|
|
27
|
+
broadcast(event: string, data: Record<string, unknown>): void;
|
|
28
|
+
getClientCount(): number;
|
|
29
|
+
isRunning(): boolean;
|
|
30
|
+
private handleConnection;
|
|
31
|
+
private handleMessage;
|
|
32
|
+
private defaultHandler;
|
|
33
|
+
private encodeMessage;
|
|
34
|
+
}
|
|
35
|
+
export declare class IPCClient extends EventEmitter {
|
|
36
|
+
private socket;
|
|
37
|
+
private readonly port;
|
|
38
|
+
private readonly host;
|
|
39
|
+
private readonly connectionTimeout;
|
|
40
|
+
private readonly responseTimeout;
|
|
41
|
+
private readonly logger;
|
|
42
|
+
private pendingRequests;
|
|
43
|
+
private requestIdCounter;
|
|
44
|
+
private buffer;
|
|
45
|
+
constructor(options?: IPCClientOptions);
|
|
46
|
+
connect(): Promise<void>;
|
|
47
|
+
disconnect(): void;
|
|
48
|
+
isConnected(): boolean;
|
|
49
|
+
request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
50
|
+
private handleData;
|
|
51
|
+
private handleMessage;
|
|
52
|
+
private handleResponse;
|
|
53
|
+
private handleEvent;
|
|
54
|
+
private handleDisconnect;
|
|
55
|
+
private generateRequestId;
|
|
56
|
+
}
|
|
57
|
+
export declare function isDaemonRunning(options?: {
|
|
58
|
+
port?: number;
|
|
59
|
+
host?: string;
|
|
60
|
+
}): Promise<boolean>;
|
|
61
|
+
export declare function waitForDaemon(options?: {
|
|
62
|
+
port?: number;
|
|
63
|
+
host?: string;
|
|
64
|
+
timeout?: number;
|
|
65
|
+
interval?: number;
|
|
66
|
+
}): Promise<void>;
|
|
67
|
+
export declare function daemonRequest<T = unknown>(method: string, params?: Record<string, unknown>, options?: IPCClientOptions): Promise<T>;
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { createServer, createConnection } from 'node:net';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { DaemonNotRunningError, DaemonCommunicationError, DaemonConnectionError, DaemonAlreadyRunningError, } from './errors.js';
|
|
4
|
+
const MESSAGE_DELIMITER = '\n';
|
|
5
|
+
const DEFAULT_PORT = 47691;
|
|
6
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
7
|
+
const CONNECTION_TIMEOUT_MS = 5000;
|
|
8
|
+
const RESPONSE_TIMEOUT_MS = 30000;
|
|
9
|
+
const noopLogger = {
|
|
10
|
+
debug: () => { },
|
|
11
|
+
info: () => { },
|
|
12
|
+
warn: () => { },
|
|
13
|
+
error: () => { },
|
|
14
|
+
};
|
|
15
|
+
export class IPCServer extends EventEmitter {
|
|
16
|
+
server = null;
|
|
17
|
+
clients = new Set();
|
|
18
|
+
port;
|
|
19
|
+
host;
|
|
20
|
+
onRequest;
|
|
21
|
+
logger;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
super();
|
|
24
|
+
this.port = options.port ?? DEFAULT_PORT;
|
|
25
|
+
this.host = options.host ?? DEFAULT_HOST;
|
|
26
|
+
this.onRequest = options.onRequest ?? this.defaultHandler.bind(this);
|
|
27
|
+
this.logger = options.logger ?? noopLogger;
|
|
28
|
+
}
|
|
29
|
+
async start() {
|
|
30
|
+
if (this.server) {
|
|
31
|
+
throw new DaemonAlreadyRunningError(this.port);
|
|
32
|
+
}
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
this.server = createServer((socket) => this.handleConnection(socket));
|
|
35
|
+
this.server.on('error', (error) => {
|
|
36
|
+
if (error.code === 'EADDRINUSE') {
|
|
37
|
+
reject(new DaemonAlreadyRunningError(this.port));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
reject(new DaemonCommunicationError('server', error.message));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
this.server.listen(this.port, this.host, () => {
|
|
44
|
+
this.logger.info('LOOP', `IPC server listening on ${this.host}:${this.port}`);
|
|
45
|
+
resolve();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async stop() {
|
|
50
|
+
if (!this.server) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (const client of this.clients) {
|
|
54
|
+
client.destroy();
|
|
55
|
+
}
|
|
56
|
+
this.clients.clear();
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
this.server.close(() => {
|
|
59
|
+
this.server = null;
|
|
60
|
+
this.logger.info('LOOP', 'IPC server stopped');
|
|
61
|
+
resolve();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
broadcast(event, data) {
|
|
66
|
+
const message = {
|
|
67
|
+
type: 'event',
|
|
68
|
+
id: `evt_${Date.now()}`,
|
|
69
|
+
event,
|
|
70
|
+
data,
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
const encoded = this.encodeMessage(message);
|
|
74
|
+
for (const client of this.clients) {
|
|
75
|
+
if (!client.destroyed) {
|
|
76
|
+
client.write(encoded);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
getClientCount() {
|
|
81
|
+
return this.clients.size;
|
|
82
|
+
}
|
|
83
|
+
isRunning() {
|
|
84
|
+
return this.server !== null;
|
|
85
|
+
}
|
|
86
|
+
handleConnection(socket) {
|
|
87
|
+
this.logger.debug('LOOP', 'IPC client connected');
|
|
88
|
+
this.clients.add(socket);
|
|
89
|
+
let buffer = '';
|
|
90
|
+
socket.on('data', async (data) => {
|
|
91
|
+
buffer += data.toString();
|
|
92
|
+
const lines = buffer.split(MESSAGE_DELIMITER);
|
|
93
|
+
buffer = lines.pop() ?? '';
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
if (line.trim()) {
|
|
96
|
+
try {
|
|
97
|
+
await this.handleMessage(socket, line);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
this.logger.error('LOOP', 'Error handling IPC message', {
|
|
101
|
+
error: error instanceof Error ? error.message : String(error),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
socket.on('close', () => {
|
|
108
|
+
this.logger.debug('LOOP', 'IPC client disconnected');
|
|
109
|
+
this.clients.delete(socket);
|
|
110
|
+
});
|
|
111
|
+
socket.on('error', (error) => {
|
|
112
|
+
this.logger.error('LOOP', 'IPC socket error', { error: error.message });
|
|
113
|
+
this.clients.delete(socket);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async handleMessage(socket, data) {
|
|
117
|
+
let message;
|
|
118
|
+
try {
|
|
119
|
+
message = JSON.parse(data);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
this.logger.error('LOOP', 'Invalid IPC message (not JSON)', { data: data.substring(0, 100) });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (message.type !== 'request') {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const request = message;
|
|
129
|
+
this.logger.debug('LOOP', `IPC request: ${request.method}`, { params: request.params });
|
|
130
|
+
try {
|
|
131
|
+
const result = await this.onRequest(request.method, request.params ?? {});
|
|
132
|
+
const response = {
|
|
133
|
+
type: 'response',
|
|
134
|
+
id: request.id,
|
|
135
|
+
requestId: request.id,
|
|
136
|
+
success: true,
|
|
137
|
+
payload: result,
|
|
138
|
+
timestamp: new Date().toISOString(),
|
|
139
|
+
};
|
|
140
|
+
socket.write(this.encodeMessage(response));
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const response = {
|
|
144
|
+
type: 'response',
|
|
145
|
+
id: request.id,
|
|
146
|
+
requestId: request.id,
|
|
147
|
+
success: false,
|
|
148
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
149
|
+
errorCode: error instanceof Error && 'code' in error ? error.code : undefined,
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
};
|
|
152
|
+
socket.write(this.encodeMessage(response));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async defaultHandler(method, _params) {
|
|
156
|
+
throw new Error(`Unknown method: ${method}`);
|
|
157
|
+
}
|
|
158
|
+
encodeMessage(message) {
|
|
159
|
+
return JSON.stringify(message) + MESSAGE_DELIMITER;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export class IPCClient extends EventEmitter {
|
|
163
|
+
socket = null;
|
|
164
|
+
port;
|
|
165
|
+
host;
|
|
166
|
+
connectionTimeout;
|
|
167
|
+
responseTimeout;
|
|
168
|
+
logger;
|
|
169
|
+
pendingRequests = new Map();
|
|
170
|
+
requestIdCounter = 0;
|
|
171
|
+
buffer = '';
|
|
172
|
+
constructor(options = {}) {
|
|
173
|
+
super();
|
|
174
|
+
this.port = options.port ?? DEFAULT_PORT;
|
|
175
|
+
this.host = options.host ?? DEFAULT_HOST;
|
|
176
|
+
this.connectionTimeout = options.connectionTimeout ?? CONNECTION_TIMEOUT_MS;
|
|
177
|
+
this.responseTimeout = options.responseTimeout ?? RESPONSE_TIMEOUT_MS;
|
|
178
|
+
this.logger = options.logger ?? noopLogger;
|
|
179
|
+
}
|
|
180
|
+
async connect() {
|
|
181
|
+
if (this.socket) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
const timeout = setTimeout(() => {
|
|
186
|
+
if (this.socket) {
|
|
187
|
+
this.socket.destroy();
|
|
188
|
+
this.socket = null;
|
|
189
|
+
}
|
|
190
|
+
reject(new DaemonConnectionError(this.host, this.port, 'Connection timeout'));
|
|
191
|
+
}, this.connectionTimeout);
|
|
192
|
+
this.socket = createConnection({ port: this.port, host: this.host }, () => {
|
|
193
|
+
clearTimeout(timeout);
|
|
194
|
+
this.logger.debug('LOOP', 'IPC client connected to daemon');
|
|
195
|
+
resolve();
|
|
196
|
+
});
|
|
197
|
+
this.socket.on('data', (data) => this.handleData(data));
|
|
198
|
+
this.socket.on('close', () => {
|
|
199
|
+
this.handleDisconnect();
|
|
200
|
+
});
|
|
201
|
+
this.socket.on('error', (error) => {
|
|
202
|
+
clearTimeout(timeout);
|
|
203
|
+
this.socket = null;
|
|
204
|
+
if (error.code === 'ECONNREFUSED') {
|
|
205
|
+
reject(new DaemonNotRunningError());
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
reject(new DaemonConnectionError(this.host, this.port, error.message));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
disconnect() {
|
|
214
|
+
if (this.socket) {
|
|
215
|
+
this.socket.destroy();
|
|
216
|
+
this.socket = null;
|
|
217
|
+
}
|
|
218
|
+
for (const [, pending] of this.pendingRequests) {
|
|
219
|
+
clearTimeout(pending.timeout);
|
|
220
|
+
pending.reject(new DaemonCommunicationError('client', 'Connection closed'));
|
|
221
|
+
}
|
|
222
|
+
this.pendingRequests.clear();
|
|
223
|
+
}
|
|
224
|
+
isConnected() {
|
|
225
|
+
return this.socket !== null && !this.socket.destroyed;
|
|
226
|
+
}
|
|
227
|
+
async request(method, params = {}) {
|
|
228
|
+
if (!this.socket || this.socket.destroyed) {
|
|
229
|
+
throw new DaemonNotRunningError();
|
|
230
|
+
}
|
|
231
|
+
const id = this.generateRequestId();
|
|
232
|
+
const request = {
|
|
233
|
+
type: 'request',
|
|
234
|
+
id,
|
|
235
|
+
method,
|
|
236
|
+
params,
|
|
237
|
+
timestamp: new Date().toISOString(),
|
|
238
|
+
};
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
const timeout = setTimeout(() => {
|
|
241
|
+
this.pendingRequests.delete(id);
|
|
242
|
+
reject(new DaemonCommunicationError('client', `Request timeout: ${method}`));
|
|
243
|
+
}, this.responseTimeout);
|
|
244
|
+
this.pendingRequests.set(id, {
|
|
245
|
+
resolve: resolve,
|
|
246
|
+
reject,
|
|
247
|
+
timeout,
|
|
248
|
+
});
|
|
249
|
+
const encoded = JSON.stringify(request) + MESSAGE_DELIMITER;
|
|
250
|
+
this.socket.write(encoded);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
handleData(data) {
|
|
254
|
+
this.buffer += data.toString();
|
|
255
|
+
const lines = this.buffer.split(MESSAGE_DELIMITER);
|
|
256
|
+
this.buffer = lines.pop() ?? '';
|
|
257
|
+
for (const line of lines) {
|
|
258
|
+
if (line.trim()) {
|
|
259
|
+
try {
|
|
260
|
+
this.handleMessage(line);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
this.logger.error('LOOP', 'Error handling IPC message', {
|
|
264
|
+
error: error instanceof Error ? error.message : String(error),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
handleMessage(data) {
|
|
271
|
+
let message;
|
|
272
|
+
try {
|
|
273
|
+
message = JSON.parse(data);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
this.logger.error('LOOP', 'Invalid IPC message (not JSON)');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (message.type === 'response') {
|
|
280
|
+
this.handleResponse(message);
|
|
281
|
+
}
|
|
282
|
+
else if (message.type === 'event') {
|
|
283
|
+
this.handleEvent(message);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
handleResponse(response) {
|
|
287
|
+
const pending = this.pendingRequests.get(response.requestId ?? response.id);
|
|
288
|
+
if (!pending) {
|
|
289
|
+
this.logger.warn('LOOP', `Received response for unknown request: ${response.id}`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.pendingRequests.delete(response.requestId ?? response.id);
|
|
293
|
+
clearTimeout(pending.timeout);
|
|
294
|
+
if (response.success) {
|
|
295
|
+
pending.resolve(response.payload);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const error = new DaemonCommunicationError('client', response.errorMessage ?? 'Unknown error');
|
|
299
|
+
if (response.errorCode) {
|
|
300
|
+
error.code = response.errorCode;
|
|
301
|
+
}
|
|
302
|
+
pending.reject(error);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
handleEvent(event) {
|
|
306
|
+
this.emit(event.event, event.data);
|
|
307
|
+
}
|
|
308
|
+
handleDisconnect() {
|
|
309
|
+
this.socket = null;
|
|
310
|
+
for (const [, pending] of this.pendingRequests) {
|
|
311
|
+
clearTimeout(pending.timeout);
|
|
312
|
+
pending.reject(new DaemonCommunicationError('client', 'Connection closed'));
|
|
313
|
+
}
|
|
314
|
+
this.pendingRequests.clear();
|
|
315
|
+
this.emit('disconnect');
|
|
316
|
+
}
|
|
317
|
+
generateRequestId() {
|
|
318
|
+
return `req_${Date.now()}_${this.requestIdCounter++}`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
export async function isDaemonRunning(options) {
|
|
322
|
+
const client = new IPCClient({
|
|
323
|
+
port: options?.port ?? DEFAULT_PORT,
|
|
324
|
+
host: options?.host ?? DEFAULT_HOST,
|
|
325
|
+
connectionTimeout: 1000,
|
|
326
|
+
});
|
|
327
|
+
try {
|
|
328
|
+
await client.connect();
|
|
329
|
+
client.disconnect();
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
export async function waitForDaemon(options) {
|
|
337
|
+
const timeout = options?.timeout ?? 10000;
|
|
338
|
+
const interval = options?.interval ?? 500;
|
|
339
|
+
const startTime = Date.now();
|
|
340
|
+
while (Date.now() - startTime < timeout) {
|
|
341
|
+
if (await isDaemonRunning(options)) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
345
|
+
}
|
|
346
|
+
throw new DaemonNotRunningError();
|
|
347
|
+
}
|
|
348
|
+
export async function daemonRequest(method, params = {}, options) {
|
|
349
|
+
const client = new IPCClient(options);
|
|
350
|
+
try {
|
|
351
|
+
await client.connect();
|
|
352
|
+
const result = await client.request(method, params);
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
client.disconnect();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LoopInterval } from './types.js';
|
|
2
|
+
export interface IntervalParserOptions {
|
|
3
|
+
minIntervalMs?: number;
|
|
4
|
+
timezone?: string;
|
|
5
|
+
currentDate?: Date;
|
|
6
|
+
}
|
|
7
|
+
export interface ParseResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
interval?: LoopInterval;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class IntervalParser {
|
|
13
|
+
private readonly minIntervalMs;
|
|
14
|
+
private readonly timezone;
|
|
15
|
+
constructor(options?: IntervalParserOptions);
|
|
16
|
+
parse(input: string, currentDate?: Date): LoopInterval;
|
|
17
|
+
safeParse(input: string, currentDate?: Date): ParseResult;
|
|
18
|
+
validate(input: string): {
|
|
19
|
+
valid: boolean;
|
|
20
|
+
error?: string;
|
|
21
|
+
};
|
|
22
|
+
calculateNextRun(interval: LoopInterval, fromDate?: Date): Date;
|
|
23
|
+
describe(interval: LoopInterval): string;
|
|
24
|
+
private detectIntervalType;
|
|
25
|
+
private parseDuration;
|
|
26
|
+
private parseCron;
|
|
27
|
+
private getNextCronRun;
|
|
28
|
+
private estimateCronInterval;
|
|
29
|
+
private describeCron;
|
|
30
|
+
private describeDuration;
|
|
31
|
+
}
|
|
32
|
+
export declare function createIntervalParser(options?: IntervalParserOptions): IntervalParser;
|
|
33
|
+
export declare function parseInterval(input: string, currentDate?: Date): LoopInterval;
|
|
34
|
+
export declare function validateInterval(input: string): {
|
|
35
|
+
valid: boolean;
|
|
36
|
+
error?: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function isValidInterval(input: string): boolean;
|
|
39
|
+
export declare function calculateNextRun(interval: LoopInterval, fromDate?: Date): Date;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import msPkg from 'ms';
|
|
2
|
+
import cronParser from 'cron-parser';
|
|
3
|
+
import { InvalidIntervalError, IntervalTooShortError, InvalidCronExpressionError, } from './errors.js';
|
|
4
|
+
function parseDurationString(value) {
|
|
5
|
+
return msPkg(value);
|
|
6
|
+
}
|
|
7
|
+
const DEFAULT_MIN_INTERVAL_MS = 60 * 1000;
|
|
8
|
+
const CRON_PATTERN = /^[\d*,\-/]+(\s+[\d*,\-/]+){4,5}$/;
|
|
9
|
+
export class IntervalParser {
|
|
10
|
+
minIntervalMs;
|
|
11
|
+
timezone;
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
14
|
+
this.timezone = options.timezone;
|
|
15
|
+
}
|
|
16
|
+
parse(input, currentDate = new Date()) {
|
|
17
|
+
const trimmed = input.trim();
|
|
18
|
+
if (!trimmed) {
|
|
19
|
+
throw new InvalidIntervalError(input, 'Interval cannot be empty');
|
|
20
|
+
}
|
|
21
|
+
const type = this.detectIntervalType(trimmed);
|
|
22
|
+
if (type === 'cron') {
|
|
23
|
+
return this.parseCron(trimmed, currentDate);
|
|
24
|
+
}
|
|
25
|
+
return this.parseDuration(trimmed, currentDate);
|
|
26
|
+
}
|
|
27
|
+
safeParse(input, currentDate = new Date()) {
|
|
28
|
+
try {
|
|
29
|
+
const interval = this.parse(input, currentDate);
|
|
30
|
+
return { success: true, interval };
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return { success: false, error: message };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
validate(input) {
|
|
38
|
+
const result = this.safeParse(input);
|
|
39
|
+
if (result.success) {
|
|
40
|
+
return { valid: true };
|
|
41
|
+
}
|
|
42
|
+
return { valid: false, error: result.error };
|
|
43
|
+
}
|
|
44
|
+
calculateNextRun(interval, fromDate = new Date()) {
|
|
45
|
+
if (interval.type === 'cron' && interval.cronExpression) {
|
|
46
|
+
return this.getNextCronRun(interval.cronExpression, fromDate);
|
|
47
|
+
}
|
|
48
|
+
return new Date(fromDate.getTime() + interval.milliseconds);
|
|
49
|
+
}
|
|
50
|
+
describe(interval) {
|
|
51
|
+
if (interval.type === 'cron') {
|
|
52
|
+
return this.describeCron(interval.cronExpression);
|
|
53
|
+
}
|
|
54
|
+
return this.describeDuration(interval.milliseconds);
|
|
55
|
+
}
|
|
56
|
+
detectIntervalType(input) {
|
|
57
|
+
if (CRON_PATTERN.test(input)) {
|
|
58
|
+
return 'cron';
|
|
59
|
+
}
|
|
60
|
+
if (input.includes(' ') && input.split(/\s+/).length >= 5) {
|
|
61
|
+
return 'cron';
|
|
62
|
+
}
|
|
63
|
+
return 'duration';
|
|
64
|
+
}
|
|
65
|
+
parseDuration(input, currentDate) {
|
|
66
|
+
const milliseconds = parseDurationString(input);
|
|
67
|
+
if (milliseconds === undefined) {
|
|
68
|
+
throw new InvalidIntervalError(input, 'Could not parse duration. Use formats like "5m", "1h", "3d", "1w"');
|
|
69
|
+
}
|
|
70
|
+
if (typeof milliseconds !== 'number' || isNaN(milliseconds)) {
|
|
71
|
+
throw new InvalidIntervalError(input, 'Duration parsing resulted in invalid value');
|
|
72
|
+
}
|
|
73
|
+
if (milliseconds <= 0) {
|
|
74
|
+
throw new InvalidIntervalError(input, 'Duration must be positive');
|
|
75
|
+
}
|
|
76
|
+
if (milliseconds < this.minIntervalMs) {
|
|
77
|
+
throw new IntervalTooShortError(input, milliseconds, this.minIntervalMs);
|
|
78
|
+
}
|
|
79
|
+
const nextRunAt = new Date(currentDate.getTime() + milliseconds);
|
|
80
|
+
return {
|
|
81
|
+
type: 'duration',
|
|
82
|
+
raw: input,
|
|
83
|
+
milliseconds,
|
|
84
|
+
cronExpression: null,
|
|
85
|
+
nextRunAt,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
parseCron(input, currentDate) {
|
|
89
|
+
try {
|
|
90
|
+
const options = {
|
|
91
|
+
currentDate,
|
|
92
|
+
};
|
|
93
|
+
if (this.timezone) {
|
|
94
|
+
options.tz = this.timezone;
|
|
95
|
+
}
|
|
96
|
+
const parsed = cronParser.parseExpression(input, options);
|
|
97
|
+
const nextRunAt = parsed.next().toDate();
|
|
98
|
+
const milliseconds = this.estimateCronInterval(input, currentDate);
|
|
99
|
+
if (milliseconds < this.minIntervalMs) {
|
|
100
|
+
throw new IntervalTooShortError(input, milliseconds, this.minIntervalMs);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
type: 'cron',
|
|
104
|
+
raw: input,
|
|
105
|
+
milliseconds,
|
|
106
|
+
cronExpression: input,
|
|
107
|
+
nextRunAt,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (error instanceof IntervalTooShortError) {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
+
throw new InvalidCronExpressionError(input, message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
getNextCronRun(expression, fromDate) {
|
|
119
|
+
const options = {
|
|
120
|
+
currentDate: fromDate,
|
|
121
|
+
};
|
|
122
|
+
if (this.timezone) {
|
|
123
|
+
options.tz = this.timezone;
|
|
124
|
+
}
|
|
125
|
+
const parsed = cronParser.parseExpression(expression, options);
|
|
126
|
+
return parsed.next().toDate();
|
|
127
|
+
}
|
|
128
|
+
estimateCronInterval(expression, currentDate) {
|
|
129
|
+
try {
|
|
130
|
+
const options = {
|
|
131
|
+
currentDate,
|
|
132
|
+
};
|
|
133
|
+
if (this.timezone) {
|
|
134
|
+
options.tz = this.timezone;
|
|
135
|
+
}
|
|
136
|
+
const parsed = cronParser.parseExpression(expression, options);
|
|
137
|
+
const runs = [];
|
|
138
|
+
for (let i = 0; i < 5; i++) {
|
|
139
|
+
runs.push(parsed.next().toDate());
|
|
140
|
+
}
|
|
141
|
+
let totalInterval = 0;
|
|
142
|
+
for (let i = 1; i < runs.length; i++) {
|
|
143
|
+
totalInterval += runs[i].getTime() - runs[i - 1].getTime();
|
|
144
|
+
}
|
|
145
|
+
return Math.round(totalInterval / (runs.length - 1));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return 60 * 1000;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
describeCron(expression) {
|
|
152
|
+
const parts = expression.split(/\s+/);
|
|
153
|
+
if (parts.length < 5) {
|
|
154
|
+
return `cron: ${expression}`;
|
|
155
|
+
}
|
|
156
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
157
|
+
if (minute === '0' && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
|
|
158
|
+
return 'Every hour at minute 0';
|
|
159
|
+
}
|
|
160
|
+
if (minute !== '*' && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
|
|
161
|
+
return `Every hour at minute ${minute}`;
|
|
162
|
+
}
|
|
163
|
+
if (minute !== '*' && hour !== '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
|
|
164
|
+
return `Daily at ${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`;
|
|
165
|
+
}
|
|
166
|
+
if (dayOfWeek !== '*' && dayOfMonth === '*') {
|
|
167
|
+
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
168
|
+
const dayNum = parseInt(dayOfWeek, 10);
|
|
169
|
+
if (dayNum >= 0 && dayNum <= 6) {
|
|
170
|
+
return `Every ${days[dayNum]} at ${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return `cron: ${expression}`;
|
|
174
|
+
}
|
|
175
|
+
describeDuration(milliseconds) {
|
|
176
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
177
|
+
const minutes = Math.floor(seconds / 60);
|
|
178
|
+
const hours = Math.floor(minutes / 60);
|
|
179
|
+
const days = Math.floor(hours / 24);
|
|
180
|
+
const weeks = Math.floor(days / 7);
|
|
181
|
+
if (weeks > 0 && days % 7 === 0) {
|
|
182
|
+
return weeks === 1 ? 'Every week' : `Every ${weeks} weeks`;
|
|
183
|
+
}
|
|
184
|
+
if (days > 0 && hours % 24 === 0) {
|
|
185
|
+
return days === 1 ? 'Every day' : `Every ${days} days`;
|
|
186
|
+
}
|
|
187
|
+
if (hours > 0 && minutes % 60 === 0) {
|
|
188
|
+
return hours === 1 ? 'Every hour' : `Every ${hours} hours`;
|
|
189
|
+
}
|
|
190
|
+
if (minutes > 0 && seconds % 60 === 0) {
|
|
191
|
+
return minutes === 1 ? 'Every minute' : `Every ${minutes} minutes`;
|
|
192
|
+
}
|
|
193
|
+
if (seconds > 0) {
|
|
194
|
+
return seconds === 1 ? 'Every second' : `Every ${seconds} seconds`;
|
|
195
|
+
}
|
|
196
|
+
return `Every ${milliseconds}ms`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export function createIntervalParser(options) {
|
|
200
|
+
return new IntervalParser(options);
|
|
201
|
+
}
|
|
202
|
+
export function parseInterval(input, currentDate) {
|
|
203
|
+
const parser = new IntervalParser();
|
|
204
|
+
return parser.parse(input, currentDate);
|
|
205
|
+
}
|
|
206
|
+
export function validateInterval(input) {
|
|
207
|
+
const parser = new IntervalParser();
|
|
208
|
+
return parser.validate(input);
|
|
209
|
+
}
|
|
210
|
+
export function isValidInterval(input) {
|
|
211
|
+
const result = validateInterval(input);
|
|
212
|
+
return result.valid;
|
|
213
|
+
}
|
|
214
|
+
export function calculateNextRun(interval, fromDate = new Date()) {
|
|
215
|
+
const parser = new IntervalParser();
|
|
216
|
+
return parser.calculateNextRun(interval, fromDate);
|
|
217
|
+
}
|