@rotorsoft/act 0.38.0 → 0.39.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/README.md +21 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +13 -0
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/test/index.d.ts +2 -0
- package/dist/@types/test/index.d.ts.map +1 -0
- package/dist/@types/test/sandbox.d.ts +113 -0
- package/dist/@types/test/sandbox.d.ts.map +1 -0
- package/dist/chunk-5WRI5ZAA.js +31 -0
- package/dist/chunk-5WRI5ZAA.js.map +1 -0
- package/dist/chunk-RHS57BUR.js +877 -0
- package/dist/chunk-RHS57BUR.js.map +1 -0
- package/dist/dist-INXH4GUE.js +546 -0
- package/dist/dist-INXH4GUE.js.map +1 -0
- package/dist/index.cjs +25 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +74 -882
- package/dist/index.js.map +1 -1
- package/dist/magic-string.es-ZGO77SPS.js +1309 -0
- package/dist/magic-string.es-ZGO77SPS.js.map +1 -0
- package/dist/test/index.cjs +4833 -0
- package/dist/test/index.cjs.map +1 -0
- package/dist/test/index.js +3912 -0
- package/dist/test/index.js.map +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +6 -1
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConcurrencyError,
|
|
3
|
+
Environments,
|
|
4
|
+
LogLevels,
|
|
5
|
+
ValidationError
|
|
6
|
+
} from "./chunk-AGWZY6YT.js";
|
|
7
|
+
|
|
8
|
+
// src/adapters/console-logger.ts
|
|
9
|
+
var LEVEL_VALUES = {
|
|
10
|
+
fatal: 60,
|
|
11
|
+
error: 50,
|
|
12
|
+
warn: 40,
|
|
13
|
+
info: 30,
|
|
14
|
+
debug: 20,
|
|
15
|
+
trace: 10
|
|
16
|
+
};
|
|
17
|
+
var LEVEL_COLORS = {
|
|
18
|
+
fatal: "\x1B[41m\x1B[37m",
|
|
19
|
+
// white on red bg
|
|
20
|
+
error: "\x1B[31m",
|
|
21
|
+
// red
|
|
22
|
+
warn: "\x1B[33m",
|
|
23
|
+
// yellow
|
|
24
|
+
info: "\x1B[32m",
|
|
25
|
+
// green
|
|
26
|
+
debug: "\x1B[36m",
|
|
27
|
+
// cyan
|
|
28
|
+
trace: "\x1B[90m"
|
|
29
|
+
// gray
|
|
30
|
+
};
|
|
31
|
+
var RESET = "\x1B[0m";
|
|
32
|
+
var noop = () => {
|
|
33
|
+
};
|
|
34
|
+
var ConsoleLogger = class _ConsoleLogger {
|
|
35
|
+
level;
|
|
36
|
+
_pretty;
|
|
37
|
+
fatal;
|
|
38
|
+
error;
|
|
39
|
+
warn;
|
|
40
|
+
info;
|
|
41
|
+
debug;
|
|
42
|
+
trace;
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
const {
|
|
45
|
+
level = "info",
|
|
46
|
+
pretty = process.env.NODE_ENV !== "production",
|
|
47
|
+
bindings
|
|
48
|
+
} = options;
|
|
49
|
+
this._pretty = pretty;
|
|
50
|
+
this.level = level;
|
|
51
|
+
const threshold = LEVEL_VALUES[level] ?? 30;
|
|
52
|
+
const write = pretty ? this._prettyWrite.bind(this, bindings) : this._jsonWrite.bind(this, bindings);
|
|
53
|
+
this.fatal = write.bind(this, "fatal", 60);
|
|
54
|
+
this.error = threshold <= 50 ? write.bind(this, "error", 50) : noop;
|
|
55
|
+
this.warn = threshold <= 40 ? write.bind(this, "warn", 40) : noop;
|
|
56
|
+
this.info = threshold <= 30 ? write.bind(this, "info", 30) : noop;
|
|
57
|
+
this.debug = threshold <= 20 ? write.bind(this, "debug", 20) : noop;
|
|
58
|
+
this.trace = threshold <= 10 ? write.bind(this, "trace", 10) : noop;
|
|
59
|
+
}
|
|
60
|
+
/** No-op — `console.log` has no resources to release. */
|
|
61
|
+
async dispose() {
|
|
62
|
+
}
|
|
63
|
+
/** @inheritDoc */
|
|
64
|
+
child(bindings) {
|
|
65
|
+
return new _ConsoleLogger({
|
|
66
|
+
level: this.level,
|
|
67
|
+
pretty: this._pretty,
|
|
68
|
+
bindings
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
_jsonWrite(bindings, level, _num, objOrMsg, msg) {
|
|
72
|
+
let obj;
|
|
73
|
+
let message;
|
|
74
|
+
if (typeof objOrMsg === "string") {
|
|
75
|
+
message = objOrMsg;
|
|
76
|
+
obj = {};
|
|
77
|
+
} else if (objOrMsg !== null && typeof objOrMsg === "object") {
|
|
78
|
+
message = msg;
|
|
79
|
+
obj = { ...objOrMsg };
|
|
80
|
+
} else {
|
|
81
|
+
message = msg;
|
|
82
|
+
obj = { value: objOrMsg };
|
|
83
|
+
}
|
|
84
|
+
const entry = Object.assign({ level, time: Date.now() }, bindings, obj);
|
|
85
|
+
if (message) entry.msg = message;
|
|
86
|
+
let line;
|
|
87
|
+
try {
|
|
88
|
+
line = JSON.stringify(entry);
|
|
89
|
+
} catch {
|
|
90
|
+
line = JSON.stringify({
|
|
91
|
+
level,
|
|
92
|
+
time: entry.time,
|
|
93
|
+
msg: message ?? "[unserializable]",
|
|
94
|
+
unserializable: true
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
process.stdout.write(line + "\n");
|
|
98
|
+
}
|
|
99
|
+
_prettyWrite(bindings, level, _num, objOrMsg, msg) {
|
|
100
|
+
const color = LEVEL_COLORS[level];
|
|
101
|
+
const tag = `${color}${level.toUpperCase().padEnd(5)}${RESET}`;
|
|
102
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
103
|
+
let message;
|
|
104
|
+
let data;
|
|
105
|
+
if (typeof objOrMsg === "string") {
|
|
106
|
+
message = objOrMsg;
|
|
107
|
+
} else {
|
|
108
|
+
message = msg ?? "";
|
|
109
|
+
if (objOrMsg !== void 0 && objOrMsg !== null) {
|
|
110
|
+
try {
|
|
111
|
+
data = JSON.stringify(objOrMsg);
|
|
112
|
+
} catch {
|
|
113
|
+
data = "[unserializable]";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const bindStr = bindings && Object.keys(bindings).length ? ` ${JSON.stringify(bindings)}` : "";
|
|
118
|
+
const parts = [ts, tag, message, data, bindStr].filter(Boolean);
|
|
119
|
+
process.stdout.write(parts.join(" ") + "\n");
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// src/lru-map.ts
|
|
124
|
+
var LruMap = class {
|
|
125
|
+
constructor(_maxSize) {
|
|
126
|
+
this._maxSize = _maxSize;
|
|
127
|
+
}
|
|
128
|
+
_entries = /* @__PURE__ */ new Map();
|
|
129
|
+
get(key) {
|
|
130
|
+
const v = this._entries.get(key);
|
|
131
|
+
if (v === void 0) return void 0;
|
|
132
|
+
this._entries.delete(key);
|
|
133
|
+
this._entries.set(key, v);
|
|
134
|
+
return v;
|
|
135
|
+
}
|
|
136
|
+
has(key) {
|
|
137
|
+
return this._entries.has(key);
|
|
138
|
+
}
|
|
139
|
+
set(key, value) {
|
|
140
|
+
this._entries.delete(key);
|
|
141
|
+
if (this._entries.size >= this._maxSize) {
|
|
142
|
+
const oldest = this._entries.keys().next().value;
|
|
143
|
+
this._entries.delete(oldest);
|
|
144
|
+
}
|
|
145
|
+
this._entries.set(key, value);
|
|
146
|
+
}
|
|
147
|
+
delete(key) {
|
|
148
|
+
return this._entries.delete(key);
|
|
149
|
+
}
|
|
150
|
+
clear() {
|
|
151
|
+
this._entries.clear();
|
|
152
|
+
}
|
|
153
|
+
get size() {
|
|
154
|
+
return this._entries.size;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var LruSet = class {
|
|
158
|
+
_map;
|
|
159
|
+
constructor(maxSize) {
|
|
160
|
+
this._map = new LruMap(maxSize);
|
|
161
|
+
}
|
|
162
|
+
has(value) {
|
|
163
|
+
return this._map.has(value);
|
|
164
|
+
}
|
|
165
|
+
add(value) {
|
|
166
|
+
this._map.set(value, true);
|
|
167
|
+
}
|
|
168
|
+
delete(value) {
|
|
169
|
+
return this._map.delete(value);
|
|
170
|
+
}
|
|
171
|
+
clear() {
|
|
172
|
+
this._map.clear();
|
|
173
|
+
}
|
|
174
|
+
get size() {
|
|
175
|
+
return this._map.size;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/adapters/in-memory-cache.ts
|
|
180
|
+
var InMemoryCache = class {
|
|
181
|
+
// CacheEntry<any> lets `get<TState>` and `set<TState>` flow without casts:
|
|
182
|
+
// any is bidirectionally compatible with the per-call TState binding, while
|
|
183
|
+
// the public Cache interface still presents a typed surface to callers.
|
|
184
|
+
_entries;
|
|
185
|
+
constructor(options) {
|
|
186
|
+
this._entries = new LruMap(options?.maxSize ?? 1e3);
|
|
187
|
+
}
|
|
188
|
+
/** @inheritDoc */
|
|
189
|
+
async get(stream) {
|
|
190
|
+
return this._entries.get(stream);
|
|
191
|
+
}
|
|
192
|
+
/** @inheritDoc */
|
|
193
|
+
async set(stream, entry) {
|
|
194
|
+
this._entries.set(stream, entry);
|
|
195
|
+
}
|
|
196
|
+
/** @inheritDoc */
|
|
197
|
+
async invalidate(stream) {
|
|
198
|
+
this._entries.delete(stream);
|
|
199
|
+
}
|
|
200
|
+
/** @inheritDoc */
|
|
201
|
+
async clear() {
|
|
202
|
+
this._entries.clear();
|
|
203
|
+
}
|
|
204
|
+
/** @inheritDoc */
|
|
205
|
+
async dispose() {
|
|
206
|
+
this._entries.clear();
|
|
207
|
+
}
|
|
208
|
+
/** Current number of entries held by the LRU. */
|
|
209
|
+
get size() {
|
|
210
|
+
return this._entries.size;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/config.ts
|
|
215
|
+
import * as fs from "fs";
|
|
216
|
+
import { z } from "zod";
|
|
217
|
+
|
|
218
|
+
// src/ports.ts
|
|
219
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
220
|
+
|
|
221
|
+
// src/utils.ts
|
|
222
|
+
import { prettifyError, ZodError } from "zod";
|
|
223
|
+
var validate = (target, payload, schema) => {
|
|
224
|
+
try {
|
|
225
|
+
return schema ? schema.parse(payload) : payload;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof ZodError) {
|
|
228
|
+
throw new ValidationError(target, payload, prettifyError(error));
|
|
229
|
+
}
|
|
230
|
+
throw new ValidationError(target, payload, error);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
var extend = (source, schema, target) => {
|
|
234
|
+
const value = validate("config", source, schema);
|
|
235
|
+
return { ...target, ...value };
|
|
236
|
+
};
|
|
237
|
+
async function sleep(ms) {
|
|
238
|
+
return new Promise((resolve) => setTimeout(resolve, ms ?? config().sleepMs));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/adapters/in-memory-store.ts
|
|
242
|
+
var InMemoryStream = class {
|
|
243
|
+
constructor(stream, source, priority = 0) {
|
|
244
|
+
this.stream = stream;
|
|
245
|
+
this.source = source;
|
|
246
|
+
this._priority = priority;
|
|
247
|
+
}
|
|
248
|
+
_at = -1;
|
|
249
|
+
_retry = -1;
|
|
250
|
+
_blocked = false;
|
|
251
|
+
_error = "";
|
|
252
|
+
_leased_by = void 0;
|
|
253
|
+
_leased_until = void 0;
|
|
254
|
+
_priority = 0;
|
|
255
|
+
get priority() {
|
|
256
|
+
return this._priority;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Bump the priority via {@link subscribe}: keeps the maximum across
|
|
260
|
+
* reactions so the highest-priority registrant wins.
|
|
261
|
+
*/
|
|
262
|
+
bumpPriority(priority) {
|
|
263
|
+
if (priority > this._priority) this._priority = priority;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Set the priority outright via {@link prioritize}: operator
|
|
267
|
+
* runtime override that ignores the build-time `max()` invariant.
|
|
268
|
+
*/
|
|
269
|
+
setPriority(priority) {
|
|
270
|
+
this._priority = priority;
|
|
271
|
+
}
|
|
272
|
+
get is_available() {
|
|
273
|
+
return !this._blocked && (!this._leased_until || this._leased_until <= /* @__PURE__ */ new Date());
|
|
274
|
+
}
|
|
275
|
+
get at() {
|
|
276
|
+
return this._at;
|
|
277
|
+
}
|
|
278
|
+
get retry() {
|
|
279
|
+
return this._retry;
|
|
280
|
+
}
|
|
281
|
+
get blocked() {
|
|
282
|
+
return this._blocked;
|
|
283
|
+
}
|
|
284
|
+
get error() {
|
|
285
|
+
return this._error;
|
|
286
|
+
}
|
|
287
|
+
get leased_by() {
|
|
288
|
+
return this._leased_by;
|
|
289
|
+
}
|
|
290
|
+
get leased_until() {
|
|
291
|
+
return this._leased_until;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Attempt to lease this stream for processing.
|
|
295
|
+
* @param lease - The lease request.
|
|
296
|
+
* @param millis - Lease duration in milliseconds.
|
|
297
|
+
* @returns The granted lease or undefined if blocked.
|
|
298
|
+
*/
|
|
299
|
+
lease(lease, millis) {
|
|
300
|
+
if (millis > 0) {
|
|
301
|
+
this._leased_by = lease.by;
|
|
302
|
+
this._leased_until = new Date(Date.now() + millis);
|
|
303
|
+
}
|
|
304
|
+
this._retry = this._retry + 1;
|
|
305
|
+
return {
|
|
306
|
+
stream: this.stream,
|
|
307
|
+
source: this.source,
|
|
308
|
+
at: lease.at,
|
|
309
|
+
by: lease.by,
|
|
310
|
+
retry: this._retry,
|
|
311
|
+
lagging: lease.lagging
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Acknowledge completion of processing for this stream.
|
|
316
|
+
* @param lease - The lease request.
|
|
317
|
+
*/
|
|
318
|
+
ack(lease) {
|
|
319
|
+
if (this._leased_by === lease.by) {
|
|
320
|
+
this._leased_by = void 0;
|
|
321
|
+
this._leased_until = void 0;
|
|
322
|
+
this._at = lease.at;
|
|
323
|
+
this._retry = -1;
|
|
324
|
+
return {
|
|
325
|
+
stream: this.stream,
|
|
326
|
+
source: this.source,
|
|
327
|
+
at: this._at,
|
|
328
|
+
by: lease.by,
|
|
329
|
+
retry: this._retry,
|
|
330
|
+
lagging: lease.lagging
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
336
|
+
* @param lease - The lease request.
|
|
337
|
+
* @param error Blocked error message.
|
|
338
|
+
*/
|
|
339
|
+
block(lease, error) {
|
|
340
|
+
if (this._leased_by === lease.by) {
|
|
341
|
+
this._blocked = true;
|
|
342
|
+
this._error = error;
|
|
343
|
+
return {
|
|
344
|
+
stream: this.stream,
|
|
345
|
+
source: this.source,
|
|
346
|
+
at: this._at,
|
|
347
|
+
by: this._leased_by,
|
|
348
|
+
retry: this._retry,
|
|
349
|
+
error: this._error,
|
|
350
|
+
lagging: lease.lagging
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Reset this stream's watermark and state for replay. The retry counter
|
|
356
|
+
* resets to -1 to match the constructor + ack() invariant ("released
|
|
357
|
+
* stream"); the next claim() bumps it to 0 (first attempt).
|
|
358
|
+
*/
|
|
359
|
+
reset() {
|
|
360
|
+
this._at = -1;
|
|
361
|
+
this._retry = -1;
|
|
362
|
+
this._blocked = false;
|
|
363
|
+
this._error = "";
|
|
364
|
+
this._leased_by = void 0;
|
|
365
|
+
this._leased_until = void 0;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
var InMemoryStore = class {
|
|
369
|
+
// stored events
|
|
370
|
+
_events = [];
|
|
371
|
+
// stored stream positions and other metadata
|
|
372
|
+
_streams = /* @__PURE__ */ new Map();
|
|
373
|
+
// last committed version per stream — O(1) replacement for filter-on-commit
|
|
374
|
+
_streamVersions = /* @__PURE__ */ new Map();
|
|
375
|
+
// max non-snapshot event id per stream — drives the source-pattern probe in claim()
|
|
376
|
+
// without scanning the full event log.
|
|
377
|
+
_maxEventIdByStream = /* @__PURE__ */ new Map();
|
|
378
|
+
// global max non-snapshot event id — fast pre-check for source-less streams in claim()
|
|
379
|
+
_maxNonSnapEventId = -1;
|
|
380
|
+
_resetIndexes() {
|
|
381
|
+
this._events.length = 0;
|
|
382
|
+
this._streamVersions.clear();
|
|
383
|
+
this._maxEventIdByStream.clear();
|
|
384
|
+
this._maxNonSnapEventId = -1;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Dispose of the store and clear all events.
|
|
388
|
+
* @returns Promise that resolves when disposal is complete.
|
|
389
|
+
*/
|
|
390
|
+
async dispose() {
|
|
391
|
+
await sleep();
|
|
392
|
+
this._resetIndexes();
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Seed the store with initial data (no-op for in-memory).
|
|
396
|
+
* @returns Promise that resolves when seeding is complete.
|
|
397
|
+
*/
|
|
398
|
+
async seed() {
|
|
399
|
+
await sleep();
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Drop all data from the store.
|
|
403
|
+
* @returns Promise that resolves when the store is cleared.
|
|
404
|
+
*/
|
|
405
|
+
async drop() {
|
|
406
|
+
await sleep();
|
|
407
|
+
this._resetIndexes();
|
|
408
|
+
this._streams = /* @__PURE__ */ new Map();
|
|
409
|
+
}
|
|
410
|
+
in_query(query, e) {
|
|
411
|
+
if (query.stream) {
|
|
412
|
+
if (query.stream_exact) {
|
|
413
|
+
if (e.stream !== query.stream) return false;
|
|
414
|
+
} else if (!RegExp(`^${query.stream}$`).test(e.stream)) return false;
|
|
415
|
+
}
|
|
416
|
+
if (query.names && !query.names.includes(e.name)) return false;
|
|
417
|
+
if (query.correlation && e.meta?.correlation !== query.correlation)
|
|
418
|
+
return false;
|
|
419
|
+
if (e.name === SNAP_EVENT && !query.with_snaps) return false;
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Query events in the store, optionally filtered by query options.
|
|
424
|
+
* @param callback - Function to call for each event.
|
|
425
|
+
* @param query - Optional query options.
|
|
426
|
+
* @returns The number of events processed.
|
|
427
|
+
*/
|
|
428
|
+
async query(callback, query) {
|
|
429
|
+
await sleep();
|
|
430
|
+
let count = 0;
|
|
431
|
+
if (query?.backward) {
|
|
432
|
+
let i = (query?.before || this._events.length) - 1;
|
|
433
|
+
while (i >= 0) {
|
|
434
|
+
const e = this._events[i--];
|
|
435
|
+
if (query && !this.in_query(query, e)) continue;
|
|
436
|
+
if (query?.created_before && e.created >= query.created_before)
|
|
437
|
+
continue;
|
|
438
|
+
if (query.after && e.id <= query.after) break;
|
|
439
|
+
if (query.created_after && e.created <= query.created_after) break;
|
|
440
|
+
callback(e);
|
|
441
|
+
count++;
|
|
442
|
+
if (query?.limit && count >= query.limit) break;
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
let i = (query?.after ?? -1) + 1;
|
|
446
|
+
while (i < this._events.length) {
|
|
447
|
+
const e = this._events[i++];
|
|
448
|
+
if (query && !this.in_query(query, e)) continue;
|
|
449
|
+
if (query?.created_after && e.created <= query.created_after) continue;
|
|
450
|
+
if (query?.before && e.id >= query.before) break;
|
|
451
|
+
if (query?.created_before && e.created >= query.created_before) break;
|
|
452
|
+
callback(e);
|
|
453
|
+
count++;
|
|
454
|
+
if (query?.limit && count >= query.limit) break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return count;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Commit one or more events to a stream.
|
|
461
|
+
* @param stream - The stream name.
|
|
462
|
+
* @param msgs - The events/messages to commit.
|
|
463
|
+
* @param meta - Event metadata.
|
|
464
|
+
* @param expectedVersion - Optional optimistic concurrency check.
|
|
465
|
+
* @returns The committed events with metadata.
|
|
466
|
+
* @throws ConcurrencyError if expectedVersion does not match.
|
|
467
|
+
*/
|
|
468
|
+
async commit(stream, msgs, meta, expectedVersion) {
|
|
469
|
+
await sleep();
|
|
470
|
+
const currentVersion = this._streamVersions.get(stream) ?? -1;
|
|
471
|
+
if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
|
|
472
|
+
throw new ConcurrencyError(
|
|
473
|
+
stream,
|
|
474
|
+
currentVersion,
|
|
475
|
+
msgs,
|
|
476
|
+
expectedVersion
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
let version = currentVersion + 1;
|
|
480
|
+
let lastNonSnapId = -1;
|
|
481
|
+
const committed = msgs.map(({ name, data }) => {
|
|
482
|
+
const c = {
|
|
483
|
+
id: this._events.length,
|
|
484
|
+
stream,
|
|
485
|
+
version,
|
|
486
|
+
created: /* @__PURE__ */ new Date(),
|
|
487
|
+
name,
|
|
488
|
+
data,
|
|
489
|
+
meta
|
|
490
|
+
};
|
|
491
|
+
this._events.push(c);
|
|
492
|
+
if (name !== SNAP_EVENT) lastNonSnapId = c.id;
|
|
493
|
+
version++;
|
|
494
|
+
return c;
|
|
495
|
+
});
|
|
496
|
+
this._streamVersions.set(stream, version - 1);
|
|
497
|
+
if (lastNonSnapId >= 0) {
|
|
498
|
+
this._maxEventIdByStream.set(stream, lastNonSnapId);
|
|
499
|
+
this._maxNonSnapEventId = lastNonSnapId;
|
|
500
|
+
}
|
|
501
|
+
return committed;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Atomically discovers and leases streams for processing.
|
|
505
|
+
* Fuses poll + lease into a single operation.
|
|
506
|
+
* @param lagging - Max streams from lagging frontier.
|
|
507
|
+
* @param leading - Max streams from leading frontier.
|
|
508
|
+
* @param by - Lease holder identifier.
|
|
509
|
+
* @param millis - Lease duration in milliseconds.
|
|
510
|
+
* @returns Granted leases.
|
|
511
|
+
*/
|
|
512
|
+
async claim(lagging, leading, by, millis) {
|
|
513
|
+
await sleep();
|
|
514
|
+
const sourceRegex = /* @__PURE__ */ new Map();
|
|
515
|
+
const getRegex = (source) => {
|
|
516
|
+
let re = sourceRegex.get(source);
|
|
517
|
+
if (!re) {
|
|
518
|
+
re = new RegExp(source);
|
|
519
|
+
sourceRegex.set(source, re);
|
|
520
|
+
}
|
|
521
|
+
return re;
|
|
522
|
+
};
|
|
523
|
+
const hasWork = (s) => {
|
|
524
|
+
if (s.at < 0) return true;
|
|
525
|
+
if (!s.source) return s.at < this._maxNonSnapEventId;
|
|
526
|
+
const re = getRegex(s.source);
|
|
527
|
+
for (const [streamName, maxId] of this._maxEventIdByStream) {
|
|
528
|
+
if (maxId > s.at && re.test(streamName)) return true;
|
|
529
|
+
}
|
|
530
|
+
return false;
|
|
531
|
+
};
|
|
532
|
+
const available = [...this._streams.values()].filter(
|
|
533
|
+
(s) => s.is_available && hasWork(s)
|
|
534
|
+
);
|
|
535
|
+
const lag = available.sort((a, b) => b.priority - a.priority || a.at - b.at).slice(0, lagging).map((s) => ({
|
|
536
|
+
stream: s.stream,
|
|
537
|
+
source: s.source,
|
|
538
|
+
at: s.at,
|
|
539
|
+
lagging: true
|
|
540
|
+
}));
|
|
541
|
+
const lead = available.sort((a, b) => b.at - a.at).slice(0, leading).map((s) => ({
|
|
542
|
+
stream: s.stream,
|
|
543
|
+
source: s.source,
|
|
544
|
+
at: s.at,
|
|
545
|
+
lagging: false
|
|
546
|
+
}));
|
|
547
|
+
const seen = /* @__PURE__ */ new Set();
|
|
548
|
+
const combined = [...lag, ...lead].filter((p) => {
|
|
549
|
+
if (seen.has(p.stream)) return false;
|
|
550
|
+
seen.add(p.stream);
|
|
551
|
+
return true;
|
|
552
|
+
});
|
|
553
|
+
return combined.map(
|
|
554
|
+
(p) => this._streams.get(p.stream)?.lease({ ...p, by, retry: 0 }, millis)
|
|
555
|
+
).filter((l) => !!l);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Registers streams for event processing. When the same stream is
|
|
559
|
+
* resubscribed with a different priority, the **maximum** wins — so
|
|
560
|
+
* the highest-priority registered reaction sets the scheduling lane.
|
|
561
|
+
* Use {@link prioritize} for operator runtime overrides.
|
|
562
|
+
*
|
|
563
|
+
* @param streams - Streams to register with optional source + priority.
|
|
564
|
+
* @returns subscribed count and current max watermark.
|
|
565
|
+
*/
|
|
566
|
+
async subscribe(streams) {
|
|
567
|
+
await sleep();
|
|
568
|
+
let subscribed = 0;
|
|
569
|
+
for (const { stream, source, priority = 0 } of streams) {
|
|
570
|
+
const existing = this._streams.get(stream);
|
|
571
|
+
if (existing) {
|
|
572
|
+
existing.bumpPriority(priority);
|
|
573
|
+
} else {
|
|
574
|
+
this._streams.set(stream, new InMemoryStream(stream, source, priority));
|
|
575
|
+
subscribed++;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
let watermark = -1;
|
|
579
|
+
for (const s of this._streams.values()) {
|
|
580
|
+
if (s.at > watermark) watermark = s.at;
|
|
581
|
+
}
|
|
582
|
+
return { subscribed, watermark };
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Acknowledge completion of processing for leased streams.
|
|
586
|
+
* @param leases - Leases to acknowledge, including last processed watermark and lease holder.
|
|
587
|
+
*/
|
|
588
|
+
async ack(leases) {
|
|
589
|
+
await sleep();
|
|
590
|
+
return leases.map((l) => this._streams.get(l.stream)?.ack(l)).filter((l) => !!l);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
594
|
+
* @param leases - Leases to block, including lease holder and last error message.
|
|
595
|
+
* @returns Blocked leases.
|
|
596
|
+
*/
|
|
597
|
+
async block(leases) {
|
|
598
|
+
await sleep();
|
|
599
|
+
return leases.map((l) => this._streams.get(l.stream)?.block(l, l.error)).filter((l) => !!l);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Reset watermarks for the given streams to -1, clearing retry, blocked,
|
|
603
|
+
* error, and lease state so they can be replayed from the beginning.
|
|
604
|
+
* @param streams - Stream names to reset.
|
|
605
|
+
* @returns Count of streams that were actually reset.
|
|
606
|
+
*/
|
|
607
|
+
async reset(streams) {
|
|
608
|
+
await sleep();
|
|
609
|
+
let count = 0;
|
|
610
|
+
for (const name of streams) {
|
|
611
|
+
const s = this._streams.get(name);
|
|
612
|
+
if (s) {
|
|
613
|
+
s.reset();
|
|
614
|
+
count++;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return count;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Bulk-update priority of streams matching `filter`. Mirrors
|
|
621
|
+
* {@link query_streams}'s filter semantics — see {@link Store.prioritize}.
|
|
622
|
+
* Unlike {@link subscribe} (which keeps `max()` of registered
|
|
623
|
+
* priorities), this sets the priority outright — operator override
|
|
624
|
+
* for the build-time scheduling policy.
|
|
625
|
+
*
|
|
626
|
+
* @returns Count of streams whose priority changed.
|
|
627
|
+
*/
|
|
628
|
+
async prioritize(filter, priority) {
|
|
629
|
+
await sleep();
|
|
630
|
+
const streamRe = filter.stream && !filter.stream_exact ? new RegExp(`^${filter.stream}$`) : void 0;
|
|
631
|
+
const sourceRe = filter.source && !filter.source_exact ? new RegExp(`^${filter.source}$`) : void 0;
|
|
632
|
+
let count = 0;
|
|
633
|
+
for (const s of this._streams.values()) {
|
|
634
|
+
if (filter.stream !== void 0) {
|
|
635
|
+
if (filter.stream_exact ? s.stream !== filter.stream : !streamRe.test(s.stream))
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (filter.source !== void 0) {
|
|
639
|
+
if (s.source === void 0) continue;
|
|
640
|
+
if (filter.source_exact ? s.source !== filter.source : !sourceRe.test(s.source))
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (filter.blocked !== void 0 && s.blocked !== filter.blocked)
|
|
644
|
+
continue;
|
|
645
|
+
if (s.priority !== priority) {
|
|
646
|
+
s.setPriority(priority);
|
|
647
|
+
count++;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return count;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Streams registered subscription positions to the callback, ordered by
|
|
654
|
+
* stream name. Returns the highest event id in the store and the count
|
|
655
|
+
* of positions emitted.
|
|
656
|
+
*/
|
|
657
|
+
async query_streams(callback, query) {
|
|
658
|
+
await sleep();
|
|
659
|
+
const limit = query?.limit ?? 100;
|
|
660
|
+
const after = query?.after;
|
|
661
|
+
const blocked = query?.blocked;
|
|
662
|
+
const streamRe = query?.stream && !query.stream_exact ? new RegExp(`^${query.stream}$`) : void 0;
|
|
663
|
+
const sourceRe = query?.source && !query.source_exact ? new RegExp(`^${query.source}$`) : void 0;
|
|
664
|
+
const sorted = [...this._streams.values()].sort(
|
|
665
|
+
(a, b) => a.stream.localeCompare(b.stream)
|
|
666
|
+
);
|
|
667
|
+
let count = 0;
|
|
668
|
+
for (const s of sorted) {
|
|
669
|
+
if (after !== void 0 && s.stream <= after) continue;
|
|
670
|
+
if (query?.stream !== void 0) {
|
|
671
|
+
if (query.stream_exact ? s.stream !== query.stream : !streamRe.test(s.stream))
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (query?.source !== void 0) {
|
|
675
|
+
if (s.source === void 0) continue;
|
|
676
|
+
if (query.source_exact ? s.source !== query.source : !sourceRe.test(s.source))
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (blocked !== void 0 && s.blocked !== blocked) continue;
|
|
680
|
+
callback({
|
|
681
|
+
stream: s.stream,
|
|
682
|
+
source: s.source,
|
|
683
|
+
at: s.at,
|
|
684
|
+
retry: s.retry,
|
|
685
|
+
blocked: s.blocked,
|
|
686
|
+
error: s.error,
|
|
687
|
+
priority: s.priority,
|
|
688
|
+
leased_by: s.leased_by,
|
|
689
|
+
leased_until: s.leased_until
|
|
690
|
+
});
|
|
691
|
+
count++;
|
|
692
|
+
if (count >= limit) break;
|
|
693
|
+
}
|
|
694
|
+
return { maxEventId: this._events.length - 1, count };
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Atomically truncates streams and seeds each with a snapshot or tombstone.
|
|
698
|
+
* @param targets - Streams to truncate with optional snapshot state and meta.
|
|
699
|
+
* @returns Map keyed by stream name, each entry with `deleted` count and `committed` event.
|
|
700
|
+
*/
|
|
701
|
+
async truncate(targets) {
|
|
702
|
+
await sleep();
|
|
703
|
+
const deletedCounts = /* @__PURE__ */ new Map();
|
|
704
|
+
const streamSet = new Set(targets.map((t) => t.stream));
|
|
705
|
+
for (const e of this._events) {
|
|
706
|
+
if (streamSet.has(e.stream)) {
|
|
707
|
+
deletedCounts.set(e.stream, (deletedCounts.get(e.stream) ?? 0) + 1);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
this._events = this._events.filter((e) => !streamSet.has(e.stream));
|
|
711
|
+
for (const stream of streamSet) {
|
|
712
|
+
this._streams.delete(stream);
|
|
713
|
+
this._streamVersions.delete(stream);
|
|
714
|
+
this._maxEventIdByStream.delete(stream);
|
|
715
|
+
}
|
|
716
|
+
const result = /* @__PURE__ */ new Map();
|
|
717
|
+
for (const { stream, snapshot, meta } of targets) {
|
|
718
|
+
const event = {
|
|
719
|
+
id: this._events.length,
|
|
720
|
+
stream,
|
|
721
|
+
version: 0,
|
|
722
|
+
created: /* @__PURE__ */ new Date(),
|
|
723
|
+
name: snapshot !== void 0 ? SNAP_EVENT : TOMBSTONE_EVENT,
|
|
724
|
+
data: snapshot ?? {},
|
|
725
|
+
meta: meta ?? { correlation: "", causation: {} }
|
|
726
|
+
};
|
|
727
|
+
this._events.push(event);
|
|
728
|
+
this._streamVersions.set(stream, 0);
|
|
729
|
+
if (event.name !== SNAP_EVENT) {
|
|
730
|
+
this._maxEventIdByStream.set(stream, event.id);
|
|
731
|
+
}
|
|
732
|
+
result.set(stream, {
|
|
733
|
+
deleted: deletedCounts.get(stream) ?? 0,
|
|
734
|
+
committed: event
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
let max = -1;
|
|
738
|
+
for (const id of this._maxEventIdByStream.values()) if (id > max) max = id;
|
|
739
|
+
this._maxNonSnapEventId = max;
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// src/ports.ts
|
|
745
|
+
var scoped = new AsyncLocalStorage();
|
|
746
|
+
var ExitCodes = ["ERROR", "EXIT"];
|
|
747
|
+
var adapters = /* @__PURE__ */ new Map();
|
|
748
|
+
function port(injector) {
|
|
749
|
+
return (adapter) => {
|
|
750
|
+
if (!adapters.has(injector.name)) {
|
|
751
|
+
const injected = injector(adapter);
|
|
752
|
+
adapters.set(injector.name, injected);
|
|
753
|
+
log().info(`[act] + ${injector.name}:${injected.constructor.name}`);
|
|
754
|
+
}
|
|
755
|
+
return adapters.get(injector.name);
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
var log = port(function log2(adapter) {
|
|
759
|
+
const cfg = config();
|
|
760
|
+
return adapter || new ConsoleLogger({
|
|
761
|
+
level: cfg.logLevel,
|
|
762
|
+
pretty: cfg.env !== "production"
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
var _store = port(function store(adapter) {
|
|
766
|
+
return adapter ?? new InMemoryStore();
|
|
767
|
+
});
|
|
768
|
+
var store2 = ((adapter) => {
|
|
769
|
+
return scoped.getStore()?.store ?? _store(adapter);
|
|
770
|
+
});
|
|
771
|
+
var _cache = port(function cache(adapter) {
|
|
772
|
+
return adapter ?? new InMemoryCache();
|
|
773
|
+
});
|
|
774
|
+
var cache2 = ((adapter) => {
|
|
775
|
+
return scoped.getStore()?.cache ?? _cache(adapter);
|
|
776
|
+
});
|
|
777
|
+
var disposers = [];
|
|
778
|
+
async function disposeAndExit(code = "EXIT") {
|
|
779
|
+
if (code === "ERROR" && config().env === "production") {
|
|
780
|
+
log().warn(
|
|
781
|
+
"disposeAndExit('ERROR') ignored in production \u2014 process kept alive"
|
|
782
|
+
);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
for (const disposer of [...disposers].reverse()) {
|
|
786
|
+
await disposer();
|
|
787
|
+
}
|
|
788
|
+
for (const adapter of [...adapters.values()].reverse()) {
|
|
789
|
+
await adapter.dispose();
|
|
790
|
+
log().info(`[act] - ${adapter.constructor.name}`);
|
|
791
|
+
}
|
|
792
|
+
adapters.clear();
|
|
793
|
+
config().env !== "test" && process.exit(code === "ERROR" ? 1 : 0);
|
|
794
|
+
}
|
|
795
|
+
function dispose(disposer) {
|
|
796
|
+
disposer && disposers.push(disposer);
|
|
797
|
+
return disposeAndExit;
|
|
798
|
+
}
|
|
799
|
+
var SNAP_EVENT = "__snapshot__";
|
|
800
|
+
var TOMBSTONE_EVENT = "__tombstone__";
|
|
801
|
+
|
|
802
|
+
// src/config.ts
|
|
803
|
+
var PackageSchema = z.object({
|
|
804
|
+
name: z.string().min(1),
|
|
805
|
+
version: z.string().min(1),
|
|
806
|
+
description: z.string().min(1).optional(),
|
|
807
|
+
author: z.object({ name: z.string().min(1), email: z.string().optional() }).optional().or(z.string().min(1)).optional(),
|
|
808
|
+
license: z.string().min(1).optional(),
|
|
809
|
+
dependencies: z.record(z.string(), z.string()).optional()
|
|
810
|
+
});
|
|
811
|
+
var FALLBACK_PACKAGE = {
|
|
812
|
+
name: "act-fallback",
|
|
813
|
+
version: "0.0.0-fallback",
|
|
814
|
+
description: "Synthetic fallback \u2014 package.json could not be loaded"
|
|
815
|
+
};
|
|
816
|
+
var getPackage = () => {
|
|
817
|
+
try {
|
|
818
|
+
const raw = fs.readFileSync("package.json");
|
|
819
|
+
return JSON.parse(raw.toString());
|
|
820
|
+
} catch (err) {
|
|
821
|
+
pkgLoadError = err;
|
|
822
|
+
return FALLBACK_PACKAGE;
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
var pkgLoadError;
|
|
826
|
+
var BaseSchema = PackageSchema.extend({
|
|
827
|
+
env: z.enum(Environments),
|
|
828
|
+
logLevel: z.enum(LogLevels),
|
|
829
|
+
logSingleLine: z.boolean(),
|
|
830
|
+
sleepMs: z.number().int().min(0).max(5e3)
|
|
831
|
+
});
|
|
832
|
+
var { NODE_ENV, LOG_LEVEL, LOG_SINGLE_LINE, SLEEP_MS } = process.env;
|
|
833
|
+
var env = NODE_ENV || "development";
|
|
834
|
+
var logLevel = LOG_LEVEL || (NODE_ENV === "test" ? "fatal" : NODE_ENV === "production" ? "info" : "trace");
|
|
835
|
+
var logSingleLine = (LOG_SINGLE_LINE || "true") === "true";
|
|
836
|
+
var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100", 10);
|
|
837
|
+
var pkg = getPackage();
|
|
838
|
+
var _validated;
|
|
839
|
+
var config = () => {
|
|
840
|
+
if (!_validated) {
|
|
841
|
+
_validated = extend(
|
|
842
|
+
{ ...pkg, env, logLevel, logSingleLine, sleepMs },
|
|
843
|
+
BaseSchema
|
|
844
|
+
);
|
|
845
|
+
if (pkgLoadError) {
|
|
846
|
+
const msg = pkgLoadError instanceof Error ? pkgLoadError.message : typeof pkgLoadError === "string" ? pkgLoadError : "unknown error";
|
|
847
|
+
log().warn(
|
|
848
|
+
`[act] Could not read package.json (${msg}); using synthetic name="${FALLBACK_PACKAGE.name}" version="${FALLBACK_PACKAGE.version}".`
|
|
849
|
+
);
|
|
850
|
+
pkgLoadError = void 0;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return _validated;
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
export {
|
|
857
|
+
ConsoleLogger,
|
|
858
|
+
LruSet,
|
|
859
|
+
InMemoryCache,
|
|
860
|
+
PackageSchema,
|
|
861
|
+
config,
|
|
862
|
+
validate,
|
|
863
|
+
extend,
|
|
864
|
+
sleep,
|
|
865
|
+
InMemoryStore,
|
|
866
|
+
scoped,
|
|
867
|
+
ExitCodes,
|
|
868
|
+
port,
|
|
869
|
+
log,
|
|
870
|
+
store2 as store,
|
|
871
|
+
cache2 as cache,
|
|
872
|
+
disposeAndExit,
|
|
873
|
+
dispose,
|
|
874
|
+
SNAP_EVENT,
|
|
875
|
+
TOMBSTONE_EVENT
|
|
876
|
+
};
|
|
877
|
+
//# sourceMappingURL=chunk-RHS57BUR.js.map
|