@classytic/arc 2.23.0 → 2.28.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 +2 -0
- package/dist/{BaseController-BQ8KNvWC.mjs → BaseController-B65zbz9m.mjs} +94 -23
- package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
- package/dist/actionPermissions-CAphcmUG.mjs +24 -0
- package/dist/{applyPermissionResult-Tw4iXtlf.mjs → applyPermissionResult-BnyVE9eD.mjs} +10 -0
- package/dist/audit/index.d.mts +32 -3
- package/dist/audit/index.mjs +7 -4
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +263 -259
- package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
- package/dist/cache/index.d.mts +41 -3
- package/dist/cache/index.mjs +33 -4
- package/dist/cleanup/index.d.mts +313 -0
- package/dist/cleanup/index.mjs +1267 -0
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/context/index.mjs +2 -64
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -4
- package/dist/core-BGa8TXJR.mjs +221 -0
- package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-C-N7z2EL.mjs} +3 -2
- package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
- package/dist/{createApp-CjOeWJ9_.mjs → createApp-Du5G54TM.mjs} +187 -96
- package/dist/{core-DQ6rDTyH.mjs → defineResource-2zvVVpxg.mjs} +297 -230
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
- package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
- package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
- package/dist/events/index.d.mts +83 -314
- package/dist/events/index.mjs +183 -95
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +24 -2
- package/dist/factory/index.d.mts +5 -5
- package/dist/factory/index.mjs +30 -8
- package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-OzYGIiU1.d.mts} +26 -2
- package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
- package/dist/headers-BvlQSDMB.mjs +45 -0
- package/dist/health-CB_m4S32.mjs +377 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +27 -3
- package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
- package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
- package/dist/index-CZY4EGga.d.mts +107 -0
- package/dist/{index-CC5ruDGh.d.mts → index-CjVg-Epg.d.mts} +37 -5
- package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +7 -6
- package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/jobs/index.d.mts +2 -0
- package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
- package/dist/integrations/mcp/index.d.mts +4 -4
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +37 -30
- package/dist/integrations/webhooks.mjs +51 -62
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +41 -14
- package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
- package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cs6nRGCt.mjs} +12 -10
- package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
- package/dist/middleware/index.d.mts +17 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/migrations/index.d.mts +94 -44
- package/dist/migrations/index.mjs +196 -44
- package/dist/module-_-M7C5ES.mjs +29 -0
- package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
- package/dist/{openapi-jSDx-VDv.mjs → openapi-BLV4OMXb.mjs} +3 -3
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BGRnV-To.mjs} +4 -27
- package/dist/plugins/index.d.mts +6 -77
- package/dist/plugins/index.mjs +11 -95
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{index-Ba10fdKp.d.mts → preloadResources-CM4uUaFv.d.mts} +51 -50
- package/dist/preloadResources-DSY5HGpS.mjs +45 -0
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +3 -3
- package/dist/presets/index.d.mts +2 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
- package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
- package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
- package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
- package/dist/{realtime-I8OrfnMR.mjs → realtime-DEuDu9io.mjs} +3 -3
- package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
- package/dist/registerRoute-DTp18Ah_.mjs +108 -0
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/{registry-vcOCtPij.mjs → registry-9km_w1aP.mjs} +15 -0
- package/dist/renewingLease-3cuQH7h8.mjs +44 -0
- package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
- package/dist/schedules-CjOd2MG9.mjs +113 -0
- package/dist/schemas/index.d.mts +67 -67
- package/dist/schemas/index.mjs +2 -3
- package/dist/scim/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +4 -4
- package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
- package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
- package/dist/testing/cleanupStoreContract.d.mts +10 -0
- package/dist/testing/cleanupStoreContract.mjs +375 -0
- package/dist/testing/index.d.mts +5 -3
- package/dist/testing/index.mjs +4 -2
- package/dist/testing/outboxStoreContract.d.mts +13 -0
- package/dist/testing/outboxStoreContract.mjs +247 -0
- package/dist/transactionContext-PNdoyLLm.mjs +64 -0
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
- package/dist/types-C_ZZRcD6.d.mts +450 -0
- package/dist/{module-B4u10iOJ.d.mts → types-Cu6vVJmh.d.mts} +196 -60
- package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
- package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
- package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
- package/dist/usage/index.d.mts +3 -52
- package/dist/usage/index.mjs +0 -0
- package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
- package/dist/utils/index.d.mts +58 -85
- package/dist/utils/index.mjs +4 -2
- package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
- package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
- package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
- package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
- package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
- package/package.json +541 -513
- package/skills/arc/references/api-reference.md +3 -1
- package/skills/arc/references/integrations.md +20 -11
- package/dist/QueryCache-Z1B38koh.d.mts +0 -40
- package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
- package/dist/health-ScyjIVo5.mjs +0 -202
- package/dist/requestId-5BhTVzBv.mjs +0 -113
- package/dist/types-XTApa8b6.mjs +0 -24
package/dist/events/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import { a as MemoryEventTransport, i as withRetry, o as createEvent, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-DJG2D7sy.mjs";
|
|
1
|
+
import { a as MemoryEventTransport, i as withRetry, o as createEvent, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-DrQ_FesK.mjs";
|
|
3
2
|
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DY6xq4sM.mjs";
|
|
3
|
+
import { InvalidOutboxEventError, OutboxOwnershipError } from "@classytic/primitives/outbox";
|
|
4
4
|
import { and, anyOf, eq, lte, ne, or } from "@classytic/repo-core/filter";
|
|
5
5
|
import { update } from "@classytic/repo-core/update";
|
|
6
6
|
//#region src/events/defineEvent.ts
|
|
@@ -61,10 +61,6 @@ import { update } from "@classytic/repo-core/update";
|
|
|
61
61
|
* const result = registry.validate('order.created', payload);
|
|
62
62
|
* ```
|
|
63
63
|
*/
|
|
64
|
-
var defineEvent_exports = /* @__PURE__ */ __exportAll({
|
|
65
|
-
createEventRegistry: () => createEventRegistry,
|
|
66
|
-
defineEvent: () => defineEvent
|
|
67
|
-
});
|
|
68
64
|
/**
|
|
69
65
|
* Define a typed event with optional schema validation.
|
|
70
66
|
*
|
|
@@ -243,10 +239,31 @@ const CACHE_EVENTS = Object.freeze({
|
|
|
243
239
|
});
|
|
244
240
|
//#endregion
|
|
245
241
|
//#region src/events/memory-outbox.ts
|
|
242
|
+
/**
|
|
243
|
+
* In-memory outbox store — reference implementation for dev / testing.
|
|
244
|
+
*
|
|
245
|
+
* Supports the full `OutboxStore` capability set (claim/lease, fail/retry,
|
|
246
|
+
* dedupe, visibleAt). Production deployments should pass a `repository` to
|
|
247
|
+
* `EventOutbox` instead of using this; the memory store exists for unit
|
|
248
|
+
* tests and single-process dev where durable persistence is noise.
|
|
249
|
+
*/
|
|
246
250
|
const DEFAULT_LEASE_MS$2 = 3e4;
|
|
247
251
|
var MemoryOutboxStore = class {
|
|
248
252
|
entries = [];
|
|
249
253
|
seenDedupeKeys = /* @__PURE__ */ new Set();
|
|
254
|
+
/**
|
|
255
|
+
* Drop every entry (including delivered / failed / dead-lettered rows) and
|
|
256
|
+
* the dedupe-key memory.
|
|
257
|
+
*
|
|
258
|
+
* For tests only — it is the in-memory analogue of truncating the table, and
|
|
259
|
+
* exists so a suite can isolate cases against ONE store instance (the
|
|
260
|
+
* `reset` hook of `runOutboxStoreContract`). Not part of `OutboxStore`; a
|
|
261
|
+
* production store must never expose this.
|
|
262
|
+
*/
|
|
263
|
+
clear() {
|
|
264
|
+
this.entries.length = 0;
|
|
265
|
+
this.seenDedupeKeys.clear();
|
|
266
|
+
}
|
|
250
267
|
async save(event, options) {
|
|
251
268
|
if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
|
|
252
269
|
if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
|
|
@@ -356,11 +373,117 @@ var MemoryOutboxStore = class {
|
|
|
356
373
|
}
|
|
357
374
|
};
|
|
358
375
|
//#endregion
|
|
376
|
+
//#region src/events/outbox/backoff.ts
|
|
377
|
+
/**
|
|
378
|
+
* Compute a `retryAt` `Date` using exponential backoff with jitter.
|
|
379
|
+
*
|
|
380
|
+
* This is a convenience helper for store authors implementing
|
|
381
|
+
* {@link OutboxStore.fail}: call it to compute the retry visibility window
|
|
382
|
+
* based on the event's current attempt count.
|
|
383
|
+
*
|
|
384
|
+
* Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
|
|
385
|
+
*
|
|
386
|
+
* @example Basic usage inside a store's `fail()` method
|
|
387
|
+
* ```typescript
|
|
388
|
+
* async fail(eventId, error, options) {
|
|
389
|
+
* const entry = await this.findById(eventId);
|
|
390
|
+
* entry.attempts++;
|
|
391
|
+
* if (entry.attempts >= MAX_ATTEMPTS) {
|
|
392
|
+
* return this.deadLetter(eventId, error);
|
|
393
|
+
* }
|
|
394
|
+
* const retryAt = exponentialBackoff({ attempt: entry.attempts });
|
|
395
|
+
* entry.visibleAt = retryAt;
|
|
396
|
+
* await this.update(entry);
|
|
397
|
+
* }
|
|
398
|
+
* ```
|
|
399
|
+
*
|
|
400
|
+
* @example Tuning for a faster transport
|
|
401
|
+
* ```typescript
|
|
402
|
+
* exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
|
|
403
|
+
* // attempt=1 → ~250ms ±30%
|
|
404
|
+
* // attempt=2 → ~500ms ±30%
|
|
405
|
+
* // attempt=3 → ~1000ms ±30%
|
|
406
|
+
* // attempt=10 → capped at 10_000ms
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
function exponentialBackoff(options) {
|
|
410
|
+
const attempt = Math.max(1, Math.floor(options.attempt));
|
|
411
|
+
const baseMs = options.baseMs ?? 1e3;
|
|
412
|
+
const maxMs = options.maxMs ?? 6e4;
|
|
413
|
+
const jitter = Math.max(0, Math.min(1, options.jitter ?? .2));
|
|
414
|
+
const now = options.now ?? Date.now();
|
|
415
|
+
const exp = baseMs * 2 ** (attempt - 1);
|
|
416
|
+
const capped = Math.min(maxMs, exp);
|
|
417
|
+
const jittered = jitter > 0 ? capped * (1 + Math.random() * jitter) : capped;
|
|
418
|
+
return new Date(now + jittered);
|
|
419
|
+
}
|
|
420
|
+
//#endregion
|
|
359
421
|
//#region src/events/repository-outbox-adapter.ts
|
|
422
|
+
/**
|
|
423
|
+
* RepositoryLike → OutboxStore adapter.
|
|
424
|
+
*
|
|
425
|
+
* Maps the `OutboxStore` vocabulary (save / claimPending / acknowledge /
|
|
426
|
+
* fail / getDeadLettered / purge) onto arc's own `RepositoryLike` primitives
|
|
427
|
+
* (create / getOne / findAll / deleteMany / findOneAndUpdate). `EventOutbox`
|
|
428
|
+
* wraps a passed repository with this helper when you use the
|
|
429
|
+
* `{ repository }` option; the function is also re-exported from
|
|
430
|
+
* `@classytic/arc/events` so consumers can build and decorate the store
|
|
431
|
+
* manually (metrics, tracing, multi-transport fan-out).
|
|
432
|
+
*
|
|
433
|
+
* Portability: filters compose via `@classytic/repo-core/filter` and
|
|
434
|
+
* updates via `@classytic/repo-core/update`. The primary-key column name
|
|
435
|
+
* is read from `repository.idField` — mongokit defaults to `_id`,
|
|
436
|
+
* sqlitekit / pgkit / prismakit to the schema's declared PK. The adapter
|
|
437
|
+
* therefore runs on any kit that implements `StandardRepo.findOneAndUpdate`
|
|
438
|
+
* + `getOne` + `getAll` + `deleteMany` + `create`.
|
|
439
|
+
*
|
|
440
|
+
* `fail()` uses a lease-gated read-then-write pair to preserve
|
|
441
|
+
* `firstFailedAt` across retries without relying on Mongo's aggregation-
|
|
442
|
+
* pipeline `$ifNull`. Leases guarantee single-writer during the failure
|
|
443
|
+
* window (`claimPending` filters out non-owned rows), so the two calls are
|
|
444
|
+
* safe under concurrent relayers.
|
|
445
|
+
*
|
|
446
|
+
* Why we DON'T use `StandardRepo.claim()` (repo-core 0.4+) anywhere in this
|
|
447
|
+
* adapter — design note for the next reader who wants to "modernize" us:
|
|
448
|
+
*
|
|
449
|
+
* • `claimPending` (FIFO claim-next loop): `claim(id, ...)` requires the
|
|
450
|
+
* candidate id upfront, but the relayer doesn't have it — that's the
|
|
451
|
+
* whole point of `findOneAndUpdate({ filter, sort: { createdAt: 1 } })`,
|
|
452
|
+
* which finds + claims in a SINGLE round-trip per claimed event.
|
|
453
|
+
* Replacing with `getOne(filter, sort) → claim(id, ...)` doubles the
|
|
454
|
+
* round-trips, opens a TOCTOU window where the candidate state can
|
|
455
|
+
* change between read and CAS (we'd need a retry loop on null-on-race),
|
|
456
|
+
* and gains nothing — the existing call is already atomic and
|
|
457
|
+
* race-free. Throughput regression with no upside; do not adopt.
|
|
458
|
+
*
|
|
459
|
+
* • `acknowledge` (pending → delivered): id is in scope, but the current
|
|
460
|
+
* filter uses `ne('status', 'delivered')` to allow ack from any
|
|
461
|
+
* non-delivered state (defensive; lease invariant means it's pending
|
|
462
|
+
* in practice). `claim` requires an exact `from` value, so adopting
|
|
463
|
+
* would tighten the source-state predicate from "not delivered" to
|
|
464
|
+
* "exactly pending". That's a behavior change — even if the new
|
|
465
|
+
* behavior is arguably tighter/safer, the constraint here is "no
|
|
466
|
+
* semantic regression". Keep `findOneAndUpdate`.
|
|
467
|
+
*
|
|
468
|
+
* • `fail` (any → pending|dead_letter): the current `baseFilter` is
|
|
469
|
+
* id-only (with optional `leaseOwner` guard) — there's NO source-state
|
|
470
|
+
* predicate at all. Adopting `claim` would ADD `from: 'pending'`,
|
|
471
|
+
* blocking `fail()` calls against rows in any other state. Same "no
|
|
472
|
+
* semantic regression" concern; keep `findOneAndUpdate`. The
|
|
473
|
+
* read-then-write pair also exists here to preserve `firstFailedAt`
|
|
474
|
+
* portably (see paragraph above) — `claim` doesn't simplify that.
|
|
475
|
+
*
|
|
476
|
+
* No kit currently ships a filter-based `claimNext`/`claimPending` (Path C
|
|
477
|
+
* in the v2.11.x evaluation) — checked mongokit 3.13.0 + sqlitekit 0.3.0.
|
|
478
|
+
* If/when one appears, the FIFO loop becomes a 1-call adoption candidate.
|
|
479
|
+
*/
|
|
360
480
|
const DEFAULT_LEASE_MS$1 = 3e4;
|
|
361
481
|
const DEFAULT_CLAIM_LIMIT = 100;
|
|
362
482
|
const DEFAULT_PURGE_BATCH = 500;
|
|
363
|
-
|
|
483
|
+
/** Column this adapter reads/writes for the visibility timestamp. */
|
|
484
|
+
const DEFAULT_VISIBLE_AT_FIELD = "visibleAt";
|
|
485
|
+
function repositoryAsOutboxStore(repository, options) {
|
|
486
|
+
const visibleAtField = options?.visibleAtField ?? DEFAULT_VISIBLE_AT_FIELD;
|
|
364
487
|
const missing = [];
|
|
365
488
|
if (typeof repository.create !== "function") missing.push("create");
|
|
366
489
|
if (typeof repository.getOne !== "function") missing.push("getOne");
|
|
@@ -387,7 +510,7 @@ function repositoryAsOutboxStore(repository) {
|
|
|
387
510
|
* lease. Used by `getPending` and `claimPending` — defined once so the
|
|
388
511
|
* two code paths stay in lockstep.
|
|
389
512
|
*/
|
|
390
|
-
const claimableFilter = (now) => and(eq("status", "pending"), lte(
|
|
513
|
+
const claimableFilter = (now) => and(eq("status", "pending"), lte(visibleAtField, now), or(eq("leaseOwner", null), lte("leaseExpiresAt", now)));
|
|
391
514
|
return {
|
|
392
515
|
async save(event, options) {
|
|
393
516
|
if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
|
|
@@ -399,7 +522,7 @@ function repositoryAsOutboxStore(repository) {
|
|
|
399
522
|
type: event.type,
|
|
400
523
|
status: "pending",
|
|
401
524
|
attempts: 0,
|
|
402
|
-
|
|
525
|
+
[visibleAtField]: options?.visibleAt ?? now,
|
|
403
526
|
leaseOwner: null,
|
|
404
527
|
leaseExpiresAt: null,
|
|
405
528
|
deliveredAt: null,
|
|
@@ -433,25 +556,26 @@ function repositoryAsOutboxStore(repository) {
|
|
|
433
556
|
const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS$1;
|
|
434
557
|
const consumerId = options?.consumerId ?? "anonymous";
|
|
435
558
|
const typeFilter = options?.types?.length ? anyOf("type", options.types) : null;
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
559
|
+
const now = /* @__PURE__ */ new Date();
|
|
560
|
+
const candidateFilter = typeFilter ? and(claimableFilter(now), typeFilter) : claimableFilter(now);
|
|
561
|
+
const candidates = unwrapDocs(await r.getAll({
|
|
562
|
+
filters: candidateFilter,
|
|
563
|
+
sort: { createdAt: 1 },
|
|
564
|
+
page: 1,
|
|
565
|
+
limit
|
|
566
|
+
}));
|
|
567
|
+
if (candidates.length === 0) return [];
|
|
568
|
+
return (await Promise.all(candidates.map((candidate) => {
|
|
569
|
+
const claimNow = /* @__PURE__ */ new Date();
|
|
570
|
+
const leaseExpiresAt = new Date(claimNow.getTime() + leaseMs);
|
|
571
|
+
return r.findOneAndUpdate(and(eq(idField, candidate.event.meta.id), claimableFilter(claimNow)), update({
|
|
442
572
|
set: {
|
|
443
573
|
leaseOwner: consumerId,
|
|
444
574
|
leaseExpiresAt
|
|
445
575
|
},
|
|
446
576
|
inc: { attempts: 1 }
|
|
447
|
-
}), {
|
|
448
|
-
|
|
449
|
-
returnDocument: "after"
|
|
450
|
-
});
|
|
451
|
-
if (!doc) break;
|
|
452
|
-
if (isWellFormed(doc.event)) claimed.push(doc.event);
|
|
453
|
-
}
|
|
454
|
-
return claimed;
|
|
577
|
+
}), { returnDocument: "after" });
|
|
578
|
+
}))).filter((doc) => doc !== null).map((doc) => doc.event).filter(isWellFormed);
|
|
455
579
|
},
|
|
456
580
|
async acknowledge(eventId, options) {
|
|
457
581
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -484,7 +608,7 @@ function repositoryAsOutboxStore(repository) {
|
|
|
484
608
|
const firstFailedAt = current.firstFailedAt ?? now;
|
|
485
609
|
if (await r.findOneAndUpdate(filter, update({ set: {
|
|
486
610
|
status: targetStatus,
|
|
487
|
-
visibleAt,
|
|
611
|
+
[visibleAtField]: visibleAt,
|
|
488
612
|
leaseOwner: null,
|
|
489
613
|
leaseExpiresAt: null,
|
|
490
614
|
lastFailedAt: now,
|
|
@@ -537,7 +661,7 @@ function repositoryAsOutboxStore(repository) {
|
|
|
537
661
|
};
|
|
538
662
|
}
|
|
539
663
|
//#endregion
|
|
540
|
-
//#region src/events/outbox.ts
|
|
664
|
+
//#region src/events/outbox/relay.ts
|
|
541
665
|
/** Default outbox retention — delivered events older than this are eligible for purge */
|
|
542
666
|
const DEFAULT_OUTBOX_RETENTION_MS = 10080 * 60 * 1e3;
|
|
543
667
|
const DEFAULT_LEASE_MS = 3e4;
|
|
@@ -552,34 +676,6 @@ const DEFAULT_LEASE_MS = 3e4;
|
|
|
552
676
|
* the store).
|
|
553
677
|
*/
|
|
554
678
|
const MAX_TRACKED_ATTEMPTS = 1e4;
|
|
555
|
-
/**
|
|
556
|
-
* Thrown by a store when `acknowledge` / `fail` is called by a consumer that
|
|
557
|
-
* does not own the event's current lease.
|
|
558
|
-
*
|
|
559
|
-
* Stores that enforce lease ownership MUST throw this (not silently return)
|
|
560
|
-
* so {@link EventOutbox} can detect the mismatch and avoid over-counting
|
|
561
|
-
* successful deliveries. {@link EventOutbox.relay} catches it and reports
|
|
562
|
-
* via {@link EventOutboxOptions.onError} instead of counting as relayed.
|
|
563
|
-
*/
|
|
564
|
-
var OutboxOwnershipError = class extends Error {
|
|
565
|
-
eventId;
|
|
566
|
-
attemptedBy;
|
|
567
|
-
currentOwner;
|
|
568
|
-
constructor(eventId, attemptedBy, currentOwner) {
|
|
569
|
-
super(`Outbox ownership mismatch for event "${eventId}": attempted by "${attemptedBy}", current owner is "${currentOwner ?? "none"}". The lease may have expired and been reclaimed by another worker.`);
|
|
570
|
-
this.name = "OutboxOwnershipError";
|
|
571
|
-
this.eventId = eventId;
|
|
572
|
-
this.attemptedBy = attemptedBy;
|
|
573
|
-
this.currentOwner = currentOwner;
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
/** Thrown by {@link EventOutbox.store} when an event is missing `type` or `meta.id`. */
|
|
577
|
-
var InvalidOutboxEventError = class extends Error {
|
|
578
|
-
constructor(reason) {
|
|
579
|
-
super(`Invalid outbox event: ${reason}`);
|
|
580
|
-
this.name = "InvalidOutboxEventError";
|
|
581
|
-
}
|
|
582
|
-
};
|
|
583
679
|
var EventOutbox = class {
|
|
584
680
|
_store;
|
|
585
681
|
_transport;
|
|
@@ -643,7 +739,7 @@ var EventOutbox = class {
|
|
|
643
739
|
session: autoSession
|
|
644
740
|
};
|
|
645
741
|
}
|
|
646
|
-
if (!effective?.headers?.
|
|
742
|
+
if (!effective?.headers?.traceparent && this._traceContextProvider) {
|
|
647
743
|
const traceHeaders = this._traceContextProvider();
|
|
648
744
|
if (traceHeaders && Object.keys(traceHeaders).length > 0) effective = {
|
|
649
745
|
...effective ?? {},
|
|
@@ -726,11 +822,14 @@ var EventOutbox = class {
|
|
|
726
822
|
usedPublishMany: false
|
|
727
823
|
};
|
|
728
824
|
if (!this._transport) return empty;
|
|
825
|
+
const claimStartedAt = Date.now();
|
|
729
826
|
const pending = this._store.claimPending ? await this._store.claimPending({
|
|
730
827
|
limit: this._batchSize,
|
|
731
828
|
consumerId: this._consumerId,
|
|
732
829
|
leaseMs: this._leaseMs
|
|
733
830
|
}) : await this._store.getPending(this._batchSize);
|
|
831
|
+
const claimMs = Date.now() - claimStartedAt;
|
|
832
|
+
const publishStartedAt = Date.now();
|
|
734
833
|
const valid = [];
|
|
735
834
|
let malformed = 0;
|
|
736
835
|
for (const event of pending) {
|
|
@@ -840,7 +939,9 @@ var EventOutbox = class {
|
|
|
840
939
|
malformed,
|
|
841
940
|
failHookErrors: counts.failHookErrors,
|
|
842
941
|
deadLettered: counts.deadLettered,
|
|
843
|
-
usedPublishMany: canPublishMany && valid.length > 0
|
|
942
|
+
usedPublishMany: canPublishMany && valid.length > 0,
|
|
943
|
+
claimMs,
|
|
944
|
+
publishMs: Date.now() - publishStartedAt
|
|
844
945
|
};
|
|
845
946
|
}
|
|
846
947
|
/**
|
|
@@ -874,51 +975,39 @@ function normalizeError(err) {
|
|
|
874
975
|
};
|
|
875
976
|
return { message: String(err) };
|
|
876
977
|
}
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/events/subscribe-helpers.ts
|
|
877
980
|
/**
|
|
878
|
-
*
|
|
981
|
+
* Subscribe-side helpers — close the symmetry that arc's publish-side
|
|
982
|
+
* already has via [`eventPlugin({ registry, validateMode: 'reject' })`](./eventPlugin.ts).
|
|
879
983
|
*
|
|
880
|
-
*
|
|
881
|
-
* {@link OutboxStore.fail}: call it to compute the retry visibility window
|
|
882
|
-
* based on the event's current attempt count.
|
|
984
|
+
* Two wrapper pairs, both transport-agnostic:
|
|
883
985
|
*
|
|
884
|
-
*
|
|
986
|
+
* - `wrapWithSchema` / `subscribeWithSchema` — read the schema from a
|
|
987
|
+
* typed `EventDefinitionOutput<T>` and validate `event.payload` before
|
|
988
|
+
* the handler runs. Single source of truth for event shape: declare
|
|
989
|
+
* once at `defineEvent`, enforced on BOTH sides without parallel Zod
|
|
990
|
+
* schemas in subscriber files.
|
|
885
991
|
*
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
* if (entry.attempts >= MAX_ATTEMPTS) {
|
|
892
|
-
* return this.deadLetter(eventId, error);
|
|
893
|
-
* }
|
|
894
|
-
* const retryAt = exponentialBackoff({ attempt: entry.attempts });
|
|
895
|
-
* entry.visibleAt = retryAt;
|
|
896
|
-
* await this.update(entry);
|
|
897
|
-
* }
|
|
898
|
-
* ```
|
|
992
|
+
* - `wrapWithBoundary` / `subscribeWithBoundary` — catch + log + swallow
|
|
993
|
+
* handler errors, for projection / cache-invalidation / fire-and-forget
|
|
994
|
+
* handlers where retry would just delay the next-event resync. Lighter
|
|
995
|
+
* than `withRetry` (no DLQ, no exponential backoff) and avoids the
|
|
996
|
+
* "single bad event poisons the bus" hazard.
|
|
899
997
|
*
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
*
|
|
906
|
-
*
|
|
998
|
+
* Both pairs compose with `withRetry` and with each other:
|
|
999
|
+
*
|
|
1000
|
+
* ```ts
|
|
1001
|
+
* await subscribeWithSchema(
|
|
1002
|
+
* fastify,
|
|
1003
|
+
* OrderPaid,
|
|
1004
|
+
* withRetry(handler, { maxRetries: 3 }),
|
|
1005
|
+
* );
|
|
907
1006
|
* ```
|
|
1007
|
+
*
|
|
1008
|
+
* @see [retry.ts](./retry.ts) for the retry + DLQ wrapper.
|
|
1009
|
+
* @see [defineEvent.ts](./defineEvent.ts) for `defineEvent` / `EventDefinitionOutput`.
|
|
908
1010
|
*/
|
|
909
|
-
function exponentialBackoff(options) {
|
|
910
|
-
const attempt = Math.max(1, Math.floor(options.attempt));
|
|
911
|
-
const baseMs = options.baseMs ?? 1e3;
|
|
912
|
-
const maxMs = options.maxMs ?? 6e4;
|
|
913
|
-
const jitter = Math.max(0, Math.min(1, options.jitter ?? .2));
|
|
914
|
-
const now = options.now ?? Date.now();
|
|
915
|
-
const exp = baseMs * 2 ** (attempt - 1);
|
|
916
|
-
const capped = Math.min(maxMs, exp);
|
|
917
|
-
const jittered = jitter > 0 ? capped * (1 + Math.random() * jitter) : capped;
|
|
918
|
-
return new Date(now + jittered);
|
|
919
|
-
}
|
|
920
|
-
//#endregion
|
|
921
|
-
//#region src/events/subscribe-helpers.ts
|
|
922
1011
|
/**
|
|
923
1012
|
* Pure handler wrapper — returns a new `EventHandler` that validates
|
|
924
1013
|
* `event.payload` against the definition's schema before invoking the handler.
|
|
@@ -947,7 +1036,6 @@ function wrapWithSchema(definition, handler, options = {}) {
|
|
|
947
1036
|
if (validate && definition.schema) result = validate(definition.schema, event.payload);
|
|
948
1037
|
else if (registry) result = registry.validate(definition.name, event.payload, eventVersion);
|
|
949
1038
|
else if (definition.schema) {
|
|
950
|
-
const { createEventRegistry } = await Promise.resolve().then(() => defineEvent_exports);
|
|
951
1039
|
const adhoc = createEventRegistry();
|
|
952
1040
|
adhoc.register(definition);
|
|
953
1041
|
result = adhoc.validate(definition.name, event.payload);
|
|
@@ -1041,4 +1129,4 @@ async function subscribeWithBoundary(fastify, pattern, handler, options) {
|
|
|
1041
1129
|
return fastify.events.subscribe(pattern, wrapWithBoundary(handler, options));
|
|
1042
1130
|
}
|
|
1043
1131
|
//#endregion
|
|
1044
|
-
export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox,
|
|
1132
|
+
export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, MemoryEventTransport, MemoryOutboxStore, createDeadLetterPublisher, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-
|
|
1
|
+
import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-uIaauNpZ.mjs";
|
|
2
2
|
export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
|
|
@@ -50,6 +50,7 @@ var RedisStreamTransport = class {
|
|
|
50
50
|
consumer;
|
|
51
51
|
blockTimeMs;
|
|
52
52
|
batchSize;
|
|
53
|
+
processingConcurrency;
|
|
53
54
|
maxRetries;
|
|
54
55
|
claimTimeoutMs;
|
|
55
56
|
deadLetterStream;
|
|
@@ -92,6 +93,7 @@ var RedisStreamTransport = class {
|
|
|
92
93
|
this.consumer = options.consumer ?? `consumer-${crypto.randomUUID().slice(0, 8)}`;
|
|
93
94
|
this.blockTimeMs = options.blockTimeMs ?? 5e3;
|
|
94
95
|
this.batchSize = options.batchSize ?? 10;
|
|
96
|
+
this.processingConcurrency = Math.max(1, Math.floor(options.processingConcurrency ?? 1));
|
|
95
97
|
this.maxRetries = options.maxRetries ?? 5;
|
|
96
98
|
this.claimTimeoutMs = options.claimTimeoutMs ?? 3e4;
|
|
97
99
|
this.deadLetterStream = options.deadLetterStream ?? "arc:events:dlq";
|
|
@@ -218,7 +220,27 @@ var RedisStreamTransport = class {
|
|
|
218
220
|
async readNewMessages() {
|
|
219
221
|
const result = await this.redis.xreadgroup("GROUP", this.group, this.consumer, "COUNT", this.batchSize, "BLOCK", this.blockTimeMs, "STREAMS", this.stream, ">");
|
|
220
222
|
if (!result) return;
|
|
221
|
-
for (const [, entries] of result)
|
|
223
|
+
for (const [, entries] of result) await this.processEntries(entries);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Process a batch's entries with bounded concurrency
|
|
227
|
+
* (`processingConcurrency`, default 1 = strict order). Worker-pool over a
|
|
228
|
+
* shared index — at most N entries in flight, each entry's own handlers
|
|
229
|
+
* still sequential inside `processEntry`.
|
|
230
|
+
*/
|
|
231
|
+
async processEntries(entries) {
|
|
232
|
+
if (this.processingConcurrency <= 1) {
|
|
233
|
+
for (const [messageId, fields] of entries) await this.processEntry(messageId, fields);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
let next = 0;
|
|
237
|
+
const workers = Array.from({ length: Math.min(this.processingConcurrency, entries.length) }, async () => {
|
|
238
|
+
while (next < entries.length) {
|
|
239
|
+
const entry = entries[next++];
|
|
240
|
+
if (entry) await this.processEntry(entry[0], entry[1]);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
await Promise.all(workers);
|
|
222
244
|
}
|
|
223
245
|
async claimPending() {
|
|
224
246
|
try {
|
|
@@ -234,7 +256,7 @@ var RedisStreamTransport = class {
|
|
|
234
256
|
if (overRetryIds.length > 0) await this.moveToDlq(overRetryIds);
|
|
235
257
|
if (staleIds.length > 0) {
|
|
236
258
|
const claimed = await this.redis.xclaim(this.stream, this.group, this.consumer, this.claimTimeoutMs, ...staleIds);
|
|
237
|
-
|
|
259
|
+
await this.processEntries(claimed);
|
|
238
260
|
}
|
|
239
261
|
} catch (err) {
|
|
240
262
|
this.logger.error("[RedisStreamTransport] claimPending error:", err);
|
package/dist/factory/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import "../
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
1
|
+
import { a as ModuleContribution, c as ResourceModule, h as HealthCheck, i as EventHandlerDefinition, l as loadResources, n as ArcModuleInput, o as LoadResourcesOptions, r as ArcModuleRegistry, s as ResourceLike, t as ArcModule } from "../types-Cu6vVJmh.mjs";
|
|
2
|
+
import { a as orderModules, c as collectModuleScheduledJobs, i as resolveModule, l as collectModuleWorkflows, n as getModuleExports, o as subscribeModuleEventHandlers, r as resolveContribution, s as collectModuleHealthChecks, t as defineModule } from "../index-CZY4EGga.mjs";
|
|
3
|
+
import "../fastify-augmentation-OzYGIiU1.mjs";
|
|
4
|
+
import { a as RawBodyOptions, c as BetterAuthOption, d as JwtAuthOption, i as MultipartOptions, l as CustomAuthenticatorOption, n as preloadResourcesAsync, o as UnderPressureOptions, r as CreateAppOptions, s as AuthOption, t as preloadResources, u as CustomPluginAuthOption } from "../preloadResources-CM4uUaFv.mjs";
|
|
5
5
|
import { FastifyInstance, FastifyServerOptions } from "fastify";
|
|
6
6
|
//#region src/factory/createApp.d.ts
|
|
7
7
|
/**
|
|
@@ -133,4 +133,4 @@ declare const testingPreset: Partial<CreateAppOptions>;
|
|
|
133
133
|
declare const edgePreset: Partial<CreateAppOptions>;
|
|
134
134
|
declare function getPreset(name: "production" | "development" | "testing" | "edge" | "worker"): Partial<CreateAppOptions>;
|
|
135
135
|
//#endregion
|
|
136
|
-
export { ArcFactory, type ArcModule, type ArcModuleInput, type ArcModuleRegistry, type ArcWorker, type AuthOption, type BetterAuthOption, type CreateAppOptions, type CreateWorkerOptions, type CustomAuthenticatorOption, type CustomPluginAuthOption, type FetchHandlerOptions, type JwtAuthOption, type LoadResourcesOptions, type MultipartOptions, type RawBodyOptions, type ResourceLike, type ResourceModule, type UnderPressureOptions, type WorkerHealthOptions, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
|
|
136
|
+
export { ArcFactory, type ArcModule, type ArcModuleInput, type ArcModuleRegistry, type ArcWorker, type AuthOption, type BetterAuthOption, type CreateAppOptions, type CreateWorkerOptions, type CustomAuthenticatorOption, type CustomPluginAuthOption, type EventHandlerDefinition, type FetchHandlerOptions, type JwtAuthOption, type LoadResourcesOptions, type ModuleContribution, type MultipartOptions, type RawBodyOptions, type ResourceLike, type ResourceModule, type UnderPressureOptions, type WorkerHealthOptions, collectModuleHealthChecks, collectModuleScheduledJobs, collectModuleWorkflows, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveContribution, resolveModule, subscribeModuleEventHandlers, testingPreset, toFetchHandler };
|
package/dist/factory/index.mjs
CHANGED
|
@@ -1,20 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as
|
|
1
|
+
import { r as mergeHealthChecks, t as healthPlugin } from "../health-CB_m4S32.mjs";
|
|
2
|
+
import { a as edgePreset, c as testingPreset, d as subscribeModuleEventHandlers, f as collectModuleHealthChecks, g as resolveModule, h as resolveContribution, i as developmentPreset, l as workerPreset, m as collectModuleWorkflows, n as createApp, o as getPreset, p as collectModuleScheduledJobs, s as productionPreset, t as ArcFactory, u as orderModules } from "../createApp-Du5G54TM.mjs";
|
|
3
|
+
import { n as getModuleExports, t as defineModule } from "../module-_-M7C5ES.mjs";
|
|
3
4
|
import { t as loadResources } from "../loadResources-DpU8pfjV.mjs";
|
|
5
|
+
import { n as preloadResourcesAsync, t as preloadResources } from "../preloadResources-DSY5HGpS.mjs";
|
|
4
6
|
//#region src/factory/createWorker.ts
|
|
5
7
|
async function createWorker(options, workerOptions = {}) {
|
|
6
8
|
const { auth: _auth, helmet: _helmet, cors: _cors, rateLimit: _rateLimit, multipart: _multipart, rawBody: _rawBody, sensible: _sensible, underPressure: _underPressure, replyHelpers: _replyHelpers, ...runtimeOptions } = options;
|
|
9
|
+
const sharedHealth = typeof runtimeOptions.arcPlugins?.health === "object" && runtimeOptions.arcPlugins.health !== null ? runtimeOptions.arcPlugins.health : {};
|
|
10
|
+
const workerArcPlugins = {
|
|
11
|
+
...workerPreset.arcPlugins,
|
|
12
|
+
...runtimeOptions.arcPlugins,
|
|
13
|
+
...workerOptions.health ? { health: false } : {}
|
|
14
|
+
};
|
|
7
15
|
const app = await createApp({
|
|
8
16
|
...runtimeOptions,
|
|
9
17
|
preset: "worker",
|
|
10
|
-
|
|
11
|
-
...workerPreset.arcPlugins,
|
|
12
|
-
...runtimeOptions.arcPlugins
|
|
13
|
-
} } : {}
|
|
18
|
+
arcPlugins: workerArcPlugins
|
|
14
19
|
});
|
|
15
20
|
if (workerOptions.health) {
|
|
16
21
|
const { port, host = "0.0.0.0", checks = [] } = workerOptions.health;
|
|
17
|
-
|
|
22
|
+
const mergedChecks = mergeHealthChecks([
|
|
23
|
+
{
|
|
24
|
+
owner: "the module graph",
|
|
25
|
+
checks: app.arc?.healthChecks ?? []
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
owner: "shared arcPlugins.health",
|
|
29
|
+
checks: sharedHealth.checks
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
owner: "the worker probe",
|
|
33
|
+
checks
|
|
34
|
+
}
|
|
35
|
+
]);
|
|
36
|
+
await app.register(healthPlugin, {
|
|
37
|
+
...sharedHealth,
|
|
38
|
+
checks: mergedChecks
|
|
39
|
+
});
|
|
18
40
|
await app.listen({
|
|
19
41
|
port,
|
|
20
42
|
host
|
|
@@ -98,4 +120,4 @@ function payloadTooLarge(limit) {
|
|
|
98
120
|
});
|
|
99
121
|
}
|
|
100
122
|
//#endregion
|
|
101
|
-
export { ArcFactory, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
|
|
123
|
+
export { ArcFactory, collectModuleHealthChecks, collectModuleScheduledJobs, collectModuleWorkflows, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveContribution, resolveModule, subscribeModuleEventHandlers, testingPreset, toFetchHandler };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mn as HookSystem, z as ResourceRegistry } from "./index-DPRH0Cme.mjs";
|
|
2
2
|
import { t as ExternalOpenApiPaths } from "./externalPaths-BE6wFpYv.mjs";
|
|
3
|
-
import { r as ArcModuleRegistry } from "./
|
|
3
|
+
import { h as HealthCheck, r as ArcModuleRegistry, t as ArcModule, u as ScheduleDefinition } from "./types-Cu6vVJmh.mjs";
|
|
4
|
+
import "./index-CZY4EGga.mjs";
|
|
4
5
|
import { FastifyPluginAsync } from "fastify";
|
|
5
6
|
//#region src/core/arcCorePlugin.d.ts
|
|
6
7
|
interface ArcCorePluginOptions {
|
|
@@ -38,6 +39,29 @@ interface ArcCore {
|
|
|
38
39
|
* Populated lazily by `registerResources` — absent until a module exports.
|
|
39
40
|
*/
|
|
40
41
|
modules?: Partial<ArcModuleRegistry> & Record<string, unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* Module-contributed readiness checks, collected in dependency order and
|
|
44
|
+
* frozen by `createApp`. The API role merges these into its `healthPlugin`;
|
|
45
|
+
* `createWorker` reads this to give its probe listener the identical union
|
|
46
|
+
* (the worker preset disables the main health plugin, so this is how module
|
|
47
|
+
* checks reach the probe without a double registration). Empty until
|
|
48
|
+
* `createApp` populates it.
|
|
49
|
+
*/
|
|
50
|
+
healthChecks?: readonly HealthCheck[];
|
|
51
|
+
/**
|
|
52
|
+
* The resolved, dependency-ordered module definitions (frozen by createApp).
|
|
53
|
+
* Integrations that collect a module arm at their OWN init time — e.g. the
|
|
54
|
+
* streamline integration calling `collectModuleWorkflows(fastify, defs)` after
|
|
55
|
+
* it has created + decorated its container — read the list from here. Empty
|
|
56
|
+
* until createApp populates it.
|
|
57
|
+
*/
|
|
58
|
+
moduleDefinitions?: readonly ArcModule[];
|
|
59
|
+
/**
|
|
60
|
+
* Module-contributed recurring schedules, resolved once in dependency order
|
|
61
|
+
* and frozen after bootstraps. The same definitions are executed by Arc's
|
|
62
|
+
* schedules plugin and exposed here for introspection. Empty until collection.
|
|
63
|
+
*/
|
|
64
|
+
scheduledJobs?: readonly Readonly<ScheduleDefinition>[];
|
|
41
65
|
}
|
|
42
66
|
declare const arcCorePlugin: FastifyPluginAsync<ArcCorePluginOptions>;
|
|
43
67
|
declare const _default: FastifyPluginAsync<ArcCorePluginOptions>;
|
|
@@ -334,7 +334,7 @@ export default crudSchemas;
|
|
|
334
334
|
const camel = toCamelCase(name);
|
|
335
335
|
const useMongoKit = config.adapter === "mongokit" || !config.adapter;
|
|
336
336
|
const schemaGeneratorImport = useMongoKit ? `import { buildCrudSchemasFromModel } from '@classytic/mongokit';\n` : "";
|
|
337
|
-
const queryParserImport = useMongoKit ? `\nimport { QueryParser } from '@classytic/mongokit';\n\nconst queryParser = new QueryParser({\n // Whitelist the fields this resource accepts in URL filters.\n // Empty by default — only \`createdAt\` is implicit; add yours.\n allowedFilterFields: [],\n});\n` : "";
|
|
337
|
+
const queryParserImport = useMongoKit ? `\nimport { QueryParser } from '@classytic/mongokit';\n\n// Fail-closed by default (mongokit >=3.25): invalid/blocked query input →\n// HTTP 400 instead of silently broadening the result set.\nconst queryParser = new QueryParser({\n // Whitelist the fields this resource accepts in URL filters.\n // Empty by default — only \`createdAt\` is implicit; add yours.\n allowedFilterFields: [],\n});\n` : "";
|
|
338
338
|
const adapterCall = useMongoKit ? `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository, schemaGenerator: buildCrudSchemasFromModel })` : `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository })`;
|
|
339
339
|
const queryParserConfig = useMongoKit ? `\n queryParser,` : "";
|
|
340
340
|
return isMultiTenant ? `/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { u as ValidationError } from "./errors-DkO8Nk6-.mjs";
|
|
2
|
+
//#region src/utils/headers.ts
|
|
3
|
+
/**
|
|
4
|
+
* Canonical header typing + accessor.
|
|
5
|
+
*
|
|
6
|
+
* Node/Fastify header values are `string | string[] | undefined` — repeated
|
|
7
|
+
* headers arrive as arrays. `IRequestContext.headers` is DECLARED as
|
|
8
|
+
* `Record<string, string | undefined>` for ergonomic host code, but the HTTP
|
|
9
|
+
* adapter passes Fastify's headers object through unchanged, so `string[]`
|
|
10
|
+
* CAN appear at runtime behind the narrower type (the declared type is
|
|
11
|
+
* scheduled to widen to {@link ArcHeaders} in v3). Until then, read headers
|
|
12
|
+
* through {@link getHeader} instead of indexing — it's honest about arrays
|
|
13
|
+
* and deterministic about duplicates.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Read a single header value. Lowercases the name (Node normalizes incoming
|
|
17
|
+
* header names to lowercase) and returns the FIRST value when the header
|
|
18
|
+
* was repeated.
|
|
19
|
+
*
|
|
20
|
+
* Duplicate policy: first-value is deterministic, but for security-sensitive
|
|
21
|
+
* headers (authorization material, forwarded-for chains, content-length) a
|
|
22
|
+
* duplicate usually signals smuggling or a broken proxy — consumers that
|
|
23
|
+
* care should check `Array.isArray(headers[name])` and REJECT instead of
|
|
24
|
+
* picking a winner.
|
|
25
|
+
*/
|
|
26
|
+
function getHeader(headers, name) {
|
|
27
|
+
if (!headers) return void 0;
|
|
28
|
+
const value = headers[name.toLowerCase()];
|
|
29
|
+
if (Array.isArray(value)) return value[0];
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read an identity-bearing or protocol header and reject repeated values.
|
|
34
|
+
*
|
|
35
|
+
* Repeated security-sensitive headers are ambiguous at proxy boundaries and
|
|
36
|
+
* may indicate request smuggling. Callers must not silently choose a winner.
|
|
37
|
+
*/
|
|
38
|
+
function requireSingleHeaderValue(headers, name) {
|
|
39
|
+
if (!headers) return void 0;
|
|
40
|
+
const value = headers[name.toLowerCase()];
|
|
41
|
+
if (Array.isArray(value)) throw new ValidationError(`Duplicate '${name}' header`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { requireSingleHeaderValue as n, getHeader as t };
|