@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
|
@@ -1,291 +1,8 @@
|
|
|
1
|
-
import { m as RESERVED_QUERY_PARAMS
|
|
1
|
+
import { m as RESERVED_QUERY_PARAMS } from "./constants-DNre0wRg.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { t as ArcError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
-
import { r as getAvailablePresets } from "./presets-BOe4Ugkl.mjs";
|
|
5
4
|
import { errorContractSchema as errorContractSchema$1, errorDetailSchema as errorDetailSchema$1 } from "@classytic/repo-core/errors";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Resource Configuration Validator
|
|
9
|
-
*
|
|
10
|
-
* Fail-fast validation at definition time.
|
|
11
|
-
* Invalid configs throw immediately with clear, actionable errors.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* const result = validateResourceConfig(config);
|
|
15
|
-
* if (!result.valid) {
|
|
16
|
-
* console.error(formatValidationErrors(result.errors));
|
|
17
|
-
* }
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* Validate a resource configuration
|
|
21
|
-
*/
|
|
22
|
-
function validateResourceConfig(config, options = {}) {
|
|
23
|
-
const errors = [];
|
|
24
|
-
const warnings = [];
|
|
25
|
-
if (!config.name) errors.push({
|
|
26
|
-
field: "name",
|
|
27
|
-
message: "Resource name is required",
|
|
28
|
-
suggestion: "Add a unique resource name (e.g., \"product\", \"user\")"
|
|
29
|
-
});
|
|
30
|
-
else if (!/^[a-z][a-z0-9-]*$/i.test(config.name)) errors.push({
|
|
31
|
-
field: "name",
|
|
32
|
-
message: `Invalid resource name "${config.name}"`,
|
|
33
|
-
suggestion: "Use alphanumeric characters and hyphens, starting with a letter"
|
|
34
|
-
});
|
|
35
|
-
const crudRoutes = CRUD_OPERATIONS;
|
|
36
|
-
const disabledRoutes = new Set(config.disabledRoutes ?? []);
|
|
37
|
-
const enabledCrudRoutes = crudRoutes.filter((route) => !disabledRoutes.has(route));
|
|
38
|
-
if (!config.disableDefaultRoutes && enabledCrudRoutes.length > 0) {
|
|
39
|
-
if (!config.adapter) errors.push({
|
|
40
|
-
field: "adapter",
|
|
41
|
-
message: "Data adapter is required when CRUD routes are enabled",
|
|
42
|
-
suggestion: "Provide an adapter: createMongooseAdapter({ model, repository })"
|
|
43
|
-
});
|
|
44
|
-
else if (!config.adapter.repository) errors.push({
|
|
45
|
-
field: "adapter.repository",
|
|
46
|
-
message: "Adapter must provide a repository",
|
|
47
|
-
suggestion: "Ensure your adapter returns a valid StandardRepo (see @classytic/repo-core)"
|
|
48
|
-
});
|
|
49
|
-
} else if (!config.adapter && !config.routes?.length) warnings.push({
|
|
50
|
-
field: "config",
|
|
51
|
-
message: "Resource has no adapter and no routes",
|
|
52
|
-
suggestion: "Provide either adapter for CRUD or routes for custom logic"
|
|
53
|
-
});
|
|
54
|
-
if (config.controller && !options.skipControllerCheck && !config.disableDefaultRoutes) {
|
|
55
|
-
const ctrl = config.controller;
|
|
56
|
-
const requiredMethods = CRUD_OPERATIONS;
|
|
57
|
-
for (const method of requiredMethods) if (typeof ctrl[method] !== "function") errors.push({
|
|
58
|
-
field: `controller.${method}`,
|
|
59
|
-
message: `Missing required CRUD method "${method}"`,
|
|
60
|
-
suggestion: "Extend BaseController which implements IController interface"
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
if (config.controller && config.routes) validateRouteHandlers(config.controller, config.routes, errors);
|
|
64
|
-
if (config.permissions) validatePermissionKeys(config, options, errors, warnings);
|
|
65
|
-
if (config.presets && !options.allowUnknownPresets) validatePresets(config.presets, errors, warnings);
|
|
66
|
-
if (config.prefix) {
|
|
67
|
-
if (!config.prefix.startsWith("/")) errors.push({
|
|
68
|
-
field: "prefix",
|
|
69
|
-
message: `Prefix must start with "/" (got "${config.prefix}")`,
|
|
70
|
-
suggestion: `Change to "/${config.prefix}"`
|
|
71
|
-
});
|
|
72
|
-
if (config.prefix.endsWith("/") && config.prefix !== "/") warnings.push({
|
|
73
|
-
field: "prefix",
|
|
74
|
-
message: `Prefix should not end with "/" (got "${config.prefix}")`,
|
|
75
|
-
suggestion: `Change to "${config.prefix.slice(0, -1)}"`
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
if (config.routes) {
|
|
79
|
-
validateRoutes(config.routes, errors);
|
|
80
|
-
validateRouteCrudCollisions(config, errors);
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
valid: errors.length === 0,
|
|
84
|
-
errors,
|
|
85
|
-
warnings
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
function validateRouteHandlers(controller, routes, errors) {
|
|
89
|
-
const ctrl = controller;
|
|
90
|
-
for (const route of routes) if (typeof route.handler === "string") {
|
|
91
|
-
if (typeof ctrl[route.handler] !== "function") errors.push({
|
|
92
|
-
field: `routes[${route.method} ${route.path}]`,
|
|
93
|
-
message: `Handler "${route.handler}" not found on controller`,
|
|
94
|
-
suggestion: `Add method "${route.handler}" to controller or use a function handler`
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
function validatePermissionKeys(config, options, _errors, warnings) {
|
|
99
|
-
const validKeys = /* @__PURE__ */ new Set([...CRUD_OPERATIONS, ...options.additionalPermissionKeys ?? []]);
|
|
100
|
-
for (const route of config.routes ?? []) if (typeof route.handler === "string") validKeys.add(route.handler);
|
|
101
|
-
for (const preset of config.presets ?? []) {
|
|
102
|
-
const presetName = typeof preset === "string" ? preset : preset.name;
|
|
103
|
-
if (presetName === "softDelete") {
|
|
104
|
-
validKeys.add("deleted");
|
|
105
|
-
validKeys.add("restore");
|
|
106
|
-
}
|
|
107
|
-
if (presetName === "slugLookup") validKeys.add("getBySlug");
|
|
108
|
-
if (presetName === "tree") {
|
|
109
|
-
validKeys.add("tree");
|
|
110
|
-
validKeys.add("children");
|
|
111
|
-
validKeys.add("getTree");
|
|
112
|
-
validKeys.add("getChildren");
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
for (const key of Object.keys(config.permissions ?? {})) if (!validKeys.has(key)) warnings.push({
|
|
116
|
-
field: `permissions.${key}`,
|
|
117
|
-
message: `Unknown permission key "${key}"`,
|
|
118
|
-
suggestion: `Valid keys: ${Array.from(validKeys).join(", ")}`
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
function validatePresets(presets, errors, warnings) {
|
|
122
|
-
const availablePresets = getAvailablePresets();
|
|
123
|
-
for (const preset of presets) {
|
|
124
|
-
if (typeof preset === "object" && ("middlewares" in preset || "routes" in preset)) continue;
|
|
125
|
-
const presetName = typeof preset === "string" ? preset : preset.name;
|
|
126
|
-
if (!availablePresets.includes(presetName)) errors.push({
|
|
127
|
-
field: "presets",
|
|
128
|
-
message: `Unknown preset "${presetName}"`,
|
|
129
|
-
suggestion: `Available presets: ${availablePresets.join(", ")}`
|
|
130
|
-
});
|
|
131
|
-
if (typeof preset === "object") validatePresetOptions(preset, warnings);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
function validatePresetOptions(preset, warnings) {
|
|
135
|
-
const validOptions = {
|
|
136
|
-
slugLookup: ["slugField"],
|
|
137
|
-
tree: ["parentField"],
|
|
138
|
-
softDelete: ["deletedField"],
|
|
139
|
-
ownedByUser: ["ownerField"],
|
|
140
|
-
multiTenant: ["tenantField", "allowPublic"]
|
|
141
|
-
}[preset.name] ?? [];
|
|
142
|
-
const providedOptions = Object.keys(preset).filter((k) => k !== "name");
|
|
143
|
-
for (const opt of providedOptions) if (!validOptions.includes(opt)) warnings.push({
|
|
144
|
-
field: `presets[${preset.name}].${opt}`,
|
|
145
|
-
message: `Unknown option "${opt}" for preset "${preset.name}"`,
|
|
146
|
-
suggestion: validOptions.length > 0 ? `Valid options: ${validOptions.join(", ")}` : `Preset "${preset.name}" has no configurable options`
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Detect collisions between user-declared `routes:` entries and the
|
|
151
|
-
* auto-CRUD route table that `createCrudRouter` will register.
|
|
152
|
-
*
|
|
153
|
-
* Without this check, a custom `POST /` on the same prefix as auto-CRUD's
|
|
154
|
-
* `create` boots fine through `defineResource()` and only blows up at
|
|
155
|
-
* `app.register()` time with Fastify's opaque `FST_ERR_DUPLICATED_ROUTE`.
|
|
156
|
-
* That error doesn't mention `disabledRoutes` and doesn't distinguish
|
|
157
|
-
* "you collided with auto-CRUD" from "you collided with yourself" — so
|
|
158
|
-
* the consumer has to grep arc's source to discover the fix.
|
|
159
|
-
*
|
|
160
|
-
* We surface the fix in the error message, naming the conflicting op
|
|
161
|
-
* and the literal line to add.
|
|
162
|
-
*
|
|
163
|
-
* Honored config:
|
|
164
|
-
* - `disableDefaultRoutes: true` → no auto-CRUD, no collisions possible
|
|
165
|
-
* - `disabledRoutes: ['create']` → that op's path is free for custom use
|
|
166
|
-
* - `updateMethod: 'PUT' | 'both'` → update slot covers PUT/PATCH accordingly
|
|
167
|
-
*/
|
|
168
|
-
function validateRouteCrudCollisions(config, errors) {
|
|
169
|
-
if (config.disableDefaultRoutes) return;
|
|
170
|
-
if (!config.routes || config.routes.length === 0) return;
|
|
171
|
-
const updateMethod = config.updateMethod ?? "PATCH";
|
|
172
|
-
const updateMethods = updateMethod === "both" ? ["PUT", "PATCH"] : [updateMethod];
|
|
173
|
-
const disabled = new Set(config.disabledRoutes ?? []);
|
|
174
|
-
const crudTable = [
|
|
175
|
-
{
|
|
176
|
-
op: "list",
|
|
177
|
-
method: "GET",
|
|
178
|
-
path: "/"
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
op: "get",
|
|
182
|
-
method: "GET",
|
|
183
|
-
path: "/:id"
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
op: "create",
|
|
187
|
-
method: "POST",
|
|
188
|
-
path: "/"
|
|
189
|
-
},
|
|
190
|
-
...updateMethods.map((m) => ({
|
|
191
|
-
op: "update",
|
|
192
|
-
method: m,
|
|
193
|
-
path: "/:id"
|
|
194
|
-
})),
|
|
195
|
-
{
|
|
196
|
-
op: "delete",
|
|
197
|
-
method: "DELETE",
|
|
198
|
-
path: "/:id"
|
|
199
|
-
}
|
|
200
|
-
];
|
|
201
|
-
for (const [i, route] of config.routes.entries()) {
|
|
202
|
-
if (!route.method || !route.path) continue;
|
|
203
|
-
for (const crud of crudTable) {
|
|
204
|
-
if (disabled.has(crud.op)) continue;
|
|
205
|
-
if (route.method !== crud.method || route.path !== crud.path) continue;
|
|
206
|
-
errors.push({
|
|
207
|
-
field: `routes[${i}]`,
|
|
208
|
-
message: `Route ${route.method} ${route.path} collides with auto-CRUD "${crud.op}"`,
|
|
209
|
-
suggestion: `Add \`disabledRoutes: ['${crud.op}']\` to suppress the auto-CRUD route, or move your custom handler to a different path. Set \`disableDefaultRoutes: true\` to disable all auto-CRUD on this resource.`
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
function validateRoutes(routes, errors) {
|
|
215
|
-
const validMethods = [
|
|
216
|
-
"GET",
|
|
217
|
-
"POST",
|
|
218
|
-
"PUT",
|
|
219
|
-
"PATCH",
|
|
220
|
-
"DELETE",
|
|
221
|
-
"OPTIONS",
|
|
222
|
-
"HEAD"
|
|
223
|
-
];
|
|
224
|
-
const seenRoutes = /* @__PURE__ */ new Set();
|
|
225
|
-
for (const [i, route] of routes.entries()) {
|
|
226
|
-
if (!validMethods.includes(route.method)) errors.push({
|
|
227
|
-
field: `routes[${i}].method`,
|
|
228
|
-
message: `Invalid HTTP method "${route.method}"`,
|
|
229
|
-
suggestion: `Valid methods: ${validMethods.join(", ")}`
|
|
230
|
-
});
|
|
231
|
-
if (!route.path) errors.push({
|
|
232
|
-
field: `routes[${i}].path`,
|
|
233
|
-
message: "Route path is required"
|
|
234
|
-
});
|
|
235
|
-
else if (!route.path.startsWith("/")) errors.push({
|
|
236
|
-
field: `routes[${i}].path`,
|
|
237
|
-
message: `Route path must start with "/" (got "${route.path}")`,
|
|
238
|
-
suggestion: `Change to "/${route.path}"`
|
|
239
|
-
});
|
|
240
|
-
const routeWithRefs = route;
|
|
241
|
-
if (!route.handler && typeof routeWithRefs.controllerMethod !== "function") errors.push({
|
|
242
|
-
field: `routes[${i}].handler`,
|
|
243
|
-
message: "Route must declare either `handler` (string / function) or `controllerMethod`",
|
|
244
|
-
suggestion: "Prefer `controllerMethod: (c: MyController) => c.method` for typed handler refs."
|
|
245
|
-
});
|
|
246
|
-
const routeKey = `${route.method} ${route.path}`;
|
|
247
|
-
if (seenRoutes.has(routeKey)) errors.push({
|
|
248
|
-
field: `routes[${i}]`,
|
|
249
|
-
message: `Duplicate route "${routeKey}"`
|
|
250
|
-
});
|
|
251
|
-
seenRoutes.add(routeKey);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Format validation errors for display
|
|
256
|
-
*/
|
|
257
|
-
function formatValidationErrors(resourceName, result) {
|
|
258
|
-
const lines = [];
|
|
259
|
-
if (result.errors.length > 0) {
|
|
260
|
-
lines.push(`Resource "${resourceName}" validation failed:`);
|
|
261
|
-
lines.push("");
|
|
262
|
-
lines.push("ERRORS:");
|
|
263
|
-
for (const err of result.errors) {
|
|
264
|
-
lines.push(` ✗ ${err.field}: ${err.message}`);
|
|
265
|
-
if (err.suggestion) lines.push(` → ${err.suggestion}`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (result.warnings.length > 0) {
|
|
269
|
-
if (lines.length > 0) lines.push("");
|
|
270
|
-
lines.push("WARNINGS:");
|
|
271
|
-
for (const warn of result.warnings) {
|
|
272
|
-
lines.push(` ⚠ ${warn.field}: ${warn.message}`);
|
|
273
|
-
if (warn.suggestion) lines.push(` → ${warn.suggestion}`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return lines.join("\n");
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Validate and throw if invalid
|
|
280
|
-
*/
|
|
281
|
-
function assertValidConfig(config, options) {
|
|
282
|
-
const result = validateResourceConfig(config, options);
|
|
283
|
-
if (!result.valid) {
|
|
284
|
-
const errorMsg = formatValidationErrors(config.name ?? "unknown", result);
|
|
285
|
-
throw new Error(errorMsg);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
//#endregion
|
|
5
|
+
import { scheduleBackground } from "@classytic/repo-core/cache";
|
|
289
6
|
//#region src/utils/circuitBreaker.ts
|
|
290
7
|
/**
|
|
291
8
|
* Circuit Breaker Pattern
|
|
@@ -337,10 +54,13 @@ var CircuitBreaker = class {
|
|
|
337
54
|
nextAttempt = 0;
|
|
338
55
|
lastCallAt = null;
|
|
339
56
|
openedAt = null;
|
|
57
|
+
halfOpenInFlight = 0;
|
|
340
58
|
failureThreshold;
|
|
341
59
|
resetTimeout;
|
|
342
60
|
timeout;
|
|
343
61
|
successThreshold;
|
|
62
|
+
halfOpenMaxProbes;
|
|
63
|
+
propagateAbort;
|
|
344
64
|
fallback;
|
|
345
65
|
onStateChange;
|
|
346
66
|
onError;
|
|
@@ -352,6 +72,8 @@ var CircuitBreaker = class {
|
|
|
352
72
|
this.resetTimeout = options.resetTimeout ?? 6e4;
|
|
353
73
|
this.timeout = options.timeout ?? 1e4;
|
|
354
74
|
this.successThreshold = options.successThreshold ?? 1;
|
|
75
|
+
this.halfOpenMaxProbes = options.halfOpenMaxProbes ?? 1;
|
|
76
|
+
this.propagateAbort = options.propagateAbort ?? false;
|
|
355
77
|
this.fallback = options.fallback;
|
|
356
78
|
this.onStateChange = options.onStateChange;
|
|
357
79
|
this.onError = options.onError;
|
|
@@ -371,6 +93,15 @@ var CircuitBreaker = class {
|
|
|
371
93
|
}
|
|
372
94
|
this.setState(CircuitState.HALF_OPEN);
|
|
373
95
|
}
|
|
96
|
+
let isProbe = false;
|
|
97
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
98
|
+
if (this.halfOpenInFlight >= this.halfOpenMaxProbes) {
|
|
99
|
+
if (this.fallback) return this.fallback(...args);
|
|
100
|
+
throw new CircuitBreakerError(`Circuit breaker is HALF_OPEN for ${this.name} — probe slots full`, CircuitState.HALF_OPEN);
|
|
101
|
+
}
|
|
102
|
+
this.halfOpenInFlight++;
|
|
103
|
+
isProbe = true;
|
|
104
|
+
}
|
|
374
105
|
try {
|
|
375
106
|
const result = await this.executeWithTimeout(args);
|
|
376
107
|
this.onSuccess();
|
|
@@ -378,17 +109,24 @@ var CircuitBreaker = class {
|
|
|
378
109
|
} catch (err) {
|
|
379
110
|
this.onFailure(err instanceof Error ? err : new Error(String(err)));
|
|
380
111
|
throw err;
|
|
112
|
+
} finally {
|
|
113
|
+
if (isProbe) this.halfOpenInFlight--;
|
|
381
114
|
}
|
|
382
115
|
}
|
|
383
116
|
/**
|
|
384
|
-
* Execute function with timeout
|
|
117
|
+
* Execute function with timeout. With `propagateAbort`, the wrapped fn
|
|
118
|
+
* receives a trailing AbortSignal that fires on timeout — the breaker's
|
|
119
|
+
* timeout otherwise only rejects the CALLER while the underlying work
|
|
120
|
+
* keeps consuming sockets/pool slots to completion.
|
|
385
121
|
*/
|
|
386
122
|
async executeWithTimeout(args) {
|
|
123
|
+
const controller = this.propagateAbort ? new AbortController() : void 0;
|
|
387
124
|
return new Promise((resolve, reject) => {
|
|
388
125
|
const timeoutId = setTimeout(() => {
|
|
126
|
+
controller?.abort(/* @__PURE__ */ new Error(`Request timeout after ${this.timeout}ms`));
|
|
389
127
|
reject(/* @__PURE__ */ new Error(`Request timeout after ${this.timeout}ms`));
|
|
390
128
|
}, this.timeout);
|
|
391
|
-
this.fn(...args).then((result) => {
|
|
129
|
+
(controller ? this.fn(...args, controller.signal) : this.fn(...args)).then((result) => {
|
|
392
130
|
clearTimeout(timeoutId);
|
|
393
131
|
resolve(result);
|
|
394
132
|
}).catch((error) => {
|
|
@@ -571,6 +309,21 @@ function createCircuitBreakerRegistry() {
|
|
|
571
309
|
//#endregion
|
|
572
310
|
//#region src/utils/compensation.ts
|
|
573
311
|
/**
|
|
312
|
+
* Observability hooks must never alter the transaction: a throwing metrics
|
|
313
|
+
* sink must not fail a step that succeeded, trigger compensation, or become
|
|
314
|
+
* an unhandled rejection from a detached fire-and-forget chain. Containment
|
|
315
|
+
* is reported via `hooks.onHookError` so broken sinks stay visible.
|
|
316
|
+
*/
|
|
317
|
+
function safeHook(name, invoke, hooks) {
|
|
318
|
+
try {
|
|
319
|
+
invoke();
|
|
320
|
+
} catch (err) {
|
|
321
|
+
try {
|
|
322
|
+
hooks?.onHookError?.(name, err instanceof Error ? err : new Error(String(err)));
|
|
323
|
+
} catch {}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
574
327
|
* Run steps in order with automatic compensation on failure.
|
|
575
328
|
*
|
|
576
329
|
* @typeParam TCtx - Context type shared across steps (defaults to Record<string, unknown>)
|
|
@@ -582,8 +335,7 @@ async function withCompensation(_name, steps, initialContext, hooks) {
|
|
|
582
335
|
const completed = [];
|
|
583
336
|
for (const step of steps) {
|
|
584
337
|
if (step.fireAndForget) {
|
|
585
|
-
|
|
586
|
-
step.execute(ctx).then((result) => hooks?.onStepComplete?.(step.name, result), () => {});
|
|
338
|
+
step.execute(ctx).then((result) => safeHook("onStepComplete", () => hooks?.onStepComplete?.(step.name, result), hooks), (err) => safeHook("onStepFailed", () => hooks?.onStepFailed?.(step.name, err instanceof Error ? err : new Error(String(err))), hooks)).catch(() => {});
|
|
587
339
|
continue;
|
|
588
340
|
}
|
|
589
341
|
try {
|
|
@@ -594,10 +346,10 @@ async function withCompensation(_name, steps, initialContext, hooks) {
|
|
|
594
346
|
step,
|
|
595
347
|
result
|
|
596
348
|
});
|
|
597
|
-
hooks?.onStepComplete?.(step.name, result);
|
|
349
|
+
safeHook("onStepComplete", () => hooks?.onStepComplete?.(step.name, result), hooks);
|
|
598
350
|
} catch (err) {
|
|
599
351
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
600
|
-
hooks?.onStepFailed?.(step.name, error);
|
|
352
|
+
safeHook("onStepFailed", () => hooks?.onStepFailed?.(step.name, error), hooks);
|
|
601
353
|
const compensationErrors = await rollback(ctx, completed, hooks);
|
|
602
354
|
return {
|
|
603
355
|
success: false,
|
|
@@ -623,7 +375,7 @@ async function rollback(ctx, completed, hooks) {
|
|
|
623
375
|
const compensateFn = entry.step.compensate;
|
|
624
376
|
try {
|
|
625
377
|
await compensateFn(ctx, entry.result);
|
|
626
|
-
hooks?.onCompensate?.(entry.step.name);
|
|
378
|
+
safeHook("onCompensate", () => hooks?.onCompensate?.(entry.step.name), hooks);
|
|
627
379
|
} catch (err) {
|
|
628
380
|
errors.push({
|
|
629
381
|
step: entry.step.name,
|
|
@@ -770,25 +522,6 @@ function handleRaw(handler, statusCode = 200) {
|
|
|
770
522
|
};
|
|
771
523
|
}
|
|
772
524
|
//#endregion
|
|
773
|
-
//#region src/utils/headers.ts
|
|
774
|
-
/**
|
|
775
|
-
* Read a single header value. Lowercases the name (Node normalizes incoming
|
|
776
|
-
* header names to lowercase) and returns the FIRST value when the header
|
|
777
|
-
* was repeated.
|
|
778
|
-
*
|
|
779
|
-
* Duplicate policy: first-value is deterministic, but for security-sensitive
|
|
780
|
-
* headers (authorization material, forwarded-for chains, content-length) a
|
|
781
|
-
* duplicate usually signals smuggling or a broken proxy — consumers that
|
|
782
|
-
* care should check `Array.isArray(headers[name])` and REJECT instead of
|
|
783
|
-
* picking a winner.
|
|
784
|
-
*/
|
|
785
|
-
function getHeader(headers, name) {
|
|
786
|
-
if (!headers) return void 0;
|
|
787
|
-
const value = headers[name.toLowerCase()];
|
|
788
|
-
if (Array.isArray(value)) return value[0];
|
|
789
|
-
return value;
|
|
790
|
-
}
|
|
791
|
-
//#endregion
|
|
792
525
|
//#region src/utils/queryParser.ts
|
|
793
526
|
/**
|
|
794
527
|
* Arc Query Parser - Default URL-to-Query Parser
|
|
@@ -802,6 +535,8 @@ function getHeader(headers, name) {
|
|
|
802
535
|
* defineResource({ name: 'product', adapter: ... });
|
|
803
536
|
*
|
|
804
537
|
* // Use MongoKit's QueryParser (recommended for MongoDB - has $lookup, aggregations, etc.)
|
|
538
|
+
* // Fail-closed by default (mongokit >=3.25): invalid/blocked query input
|
|
539
|
+
* // becomes HTTP 400 instead of a silently broadened result set.
|
|
805
540
|
* import { QueryParser } from '@classytic/mongokit';
|
|
806
541
|
* defineResource({
|
|
807
542
|
* name: 'product',
|
|
@@ -1604,13 +1339,6 @@ function getDefaultCrudSchemas() {
|
|
|
1604
1339
|
});
|
|
1605
1340
|
}
|
|
1606
1341
|
//#endregion
|
|
1607
|
-
//#region src/utils/runtime.ts
|
|
1608
|
-
/**
|
|
1609
|
-
* Portable "run on next tick" scheduler. `setImmediate` is Node-only — not
|
|
1610
|
-
* available in Bun workers, Deno, Cloudflare Workers, or edge runtimes.
|
|
1611
|
-
*/
|
|
1612
|
-
const scheduleBackground = typeof setImmediate === "function" ? (cb) => void setImmediate(cb) : (cb) => queueMicrotask(cb);
|
|
1613
|
-
//#endregion
|
|
1614
1342
|
//#region src/utils/stateMachine.ts
|
|
1615
1343
|
/**
|
|
1616
1344
|
* Create a state machine for validating transitions
|
|
@@ -1712,22 +1440,4 @@ function createStateMachine(name, transitions = {}, options = {}) {
|
|
|
1712
1440
|
};
|
|
1713
1441
|
}
|
|
1714
1442
|
//#endregion
|
|
1715
|
-
|
|
1716
|
-
/**
|
|
1717
|
-
* Extract a user ID from a user object. Accepts `id` or `_id` — returns
|
|
1718
|
-
* `undefined` when neither is present. Used by arc's controllers to
|
|
1719
|
-
* populate `createdBy` / `updatedBy` fields and for cache scoping.
|
|
1720
|
-
*
|
|
1721
|
-
* @example
|
|
1722
|
-
* ```ts
|
|
1723
|
-
* import { getUserId } from '@classytic/arc/utils';
|
|
1724
|
-
* const uid = getUserId(request.user);
|
|
1725
|
-
* ```
|
|
1726
|
-
*/
|
|
1727
|
-
function getUserId(user) {
|
|
1728
|
-
if (!user) return void 0;
|
|
1729
|
-
const id = user.id ?? user._id;
|
|
1730
|
-
return id ? String(id) : void 0;
|
|
1731
|
-
}
|
|
1732
|
-
//#endregion
|
|
1733
|
-
export { CircuitState as A, defineGuard as C, CircuitBreaker as D, withCompensation as E, validateResourceConfig as F, createCircuitBreakerRegistry as M, assertValidConfig as N, CircuitBreakerError as O, formatValidationErrors as P, NEUTRAL_DIALECT as S, defineCompensation as T, ArcQueryParser as _, bareListResponse as a, handleRaw as b, errorDetailSchema$1 as c, keysetListResponse as d, listResponse as f, responses as g, queryParams as h, aggregateListResponse as i, createCircuitBreaker as j, CircuitBreakerRegistry as k, getDefaultCrudSchemas as l, paginationSchema as m, createStateMachine as n, deleteResponse as o, offsetListResponse as p, scheduleBackground as r, errorContractSchema$1 as s, getUserId as t, getListQueryParams as u, createQueryParser as v, defineErrorMapper as w, MONGO_DIALECT as x, getHeader as y };
|
|
1443
|
+
export { createCircuitBreakerRegistry as A, defineCompensation as C, CircuitBreakerRegistry as D, CircuitBreakerError as E, CircuitState as O, defineErrorMapper as S, CircuitBreaker as T, createQueryParser as _, deleteResponse as a, NEUTRAL_DIALECT as b, getDefaultCrudSchemas as c, listResponse as d, offsetListResponse as f, ArcQueryParser as g, responses as h, bareListResponse as i, createCircuitBreaker as k, getListQueryParams as l, queryParams as m, scheduleBackground as n, errorContractSchema$1 as o, paginationSchema as p, aggregateListResponse as r, errorDetailSchema$1 as s, createStateMachine as t, keysetListResponse as u, handleRaw as v, withCompensation as w, defineGuard as x, MONGO_DIALECT as y };
|
|
@@ -123,7 +123,7 @@ function resolveLimitPolicy(config) {
|
|
|
123
123
|
function resolveAggRequestLimit(normalized, query) {
|
|
124
124
|
const policy = normalized.compiled.limitPolicy;
|
|
125
125
|
if (!policy) return normalized.compiled.limit;
|
|
126
|
-
const raw = query
|
|
126
|
+
const raw = query.limit;
|
|
127
127
|
if (raw === void 0 || raw === "") return policy.defaultLimit;
|
|
128
128
|
const parsed = typeof raw === "number" ? raw : Number.parseInt(String(raw), 10);
|
|
129
129
|
if (!Number.isFinite(parsed) || parsed <= 0) return policy.defaultLimit;
|
|
@@ -1,33 +1,5 @@
|
|
|
1
1
|
import { n as DomainEvent } from "./EventTransport-QmY7A258.mjs";
|
|
2
2
|
import { FastifyPluginAsync, FastifyRequest } from "fastify";
|
|
3
|
-
//#region src/plugins/health.d.ts
|
|
4
|
-
interface HealthCheck {
|
|
5
|
-
/** Name of the dependency */
|
|
6
|
-
name: string;
|
|
7
|
-
/** Function that returns true if healthy, false otherwise */
|
|
8
|
-
check: () => Promise<boolean> | boolean;
|
|
9
|
-
/** Optional timeout in ms (default: 5000) */
|
|
10
|
-
timeout?: number;
|
|
11
|
-
/** Whether this check is critical for readiness (default: true) */
|
|
12
|
-
critical?: boolean;
|
|
13
|
-
}
|
|
14
|
-
interface HealthOptions {
|
|
15
|
-
/** Route prefix (default: '/_health') */
|
|
16
|
-
prefix?: string;
|
|
17
|
-
/** Health check dependencies */
|
|
18
|
-
checks?: HealthCheck[];
|
|
19
|
-
/** Enable metrics endpoint (default: false) */
|
|
20
|
-
metrics?: boolean;
|
|
21
|
-
/** Custom metrics collector function */
|
|
22
|
-
metricsCollector?: () => Promise<string> | string;
|
|
23
|
-
/** Version info to include in responses */
|
|
24
|
-
version?: string;
|
|
25
|
-
/** Collect HTTP request metrics (default: true if metrics enabled) */
|
|
26
|
-
collectHttpMetrics?: boolean;
|
|
27
|
-
}
|
|
28
|
-
declare const healthPlugin: FastifyPluginAsync<HealthOptions>;
|
|
29
|
-
declare const _default$5: FastifyPluginAsync<HealthOptions>;
|
|
30
|
-
//#endregion
|
|
31
3
|
//#region src/plugins/caching.d.ts
|
|
32
4
|
interface CachingRule {
|
|
33
5
|
/** Path prefix to match (e.g., '/api/products') */
|
|
@@ -174,6 +146,16 @@ interface VersioningOptions {
|
|
|
174
146
|
headerName?: string;
|
|
175
147
|
/** Response header name (default: 'x-api-version') */
|
|
176
148
|
responseHeader?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Supported versions. When set, a request for any OTHER version is
|
|
151
|
+
* rejected with `400 arc.unsupported_api_version` (details list the
|
|
152
|
+
* supported set). When omitted, every requested version is accepted
|
|
153
|
+
* and merely annotated — detection-only mode.
|
|
154
|
+
*
|
|
155
|
+
* The `defaultVersion` must be in this list (boot-time check) so a
|
|
156
|
+
* versionless request can never be rejected by its own default.
|
|
157
|
+
*/
|
|
158
|
+
versions?: string[];
|
|
177
159
|
/** Deprecated versions — adds Deprecation + Sunset headers */
|
|
178
160
|
deprecated?: string[];
|
|
179
161
|
/** Sunset date for deprecated versions (ISO 8601) */
|
|
@@ -187,4 +169,4 @@ declare module "fastify" {
|
|
|
187
169
|
declare const versioningPlugin: FastifyPluginAsync<VersioningOptions>;
|
|
188
170
|
declare const _default: FastifyPluginAsync<VersioningOptions>;
|
|
189
171
|
//#endregion
|
|
190
|
-
export {
|
|
172
|
+
export { CachingRule as _, MetricsCollector as a, metricsPlugin as c, _default$2 as d, realtimePlugin as f, CachingOptions as g, ssePlugin as h, MetricEntry as i, ChangeOperation as l, _default$3 as m, _default as n, MetricsOptions as o, SSEOptions as p, versioningPlugin as r, _default$1 as s, VersioningOptions as t, RealtimeOptions as u, _default$4 as v, cachingPlugin as y };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
|
+
import { d as createDomainError } from "./errors-DkO8Nk6-.mjs";
|
|
2
3
|
import fp from "fastify-plugin";
|
|
3
4
|
//#region src/plugins/versioning.ts
|
|
4
5
|
var versioning_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -7,8 +8,10 @@ var versioning_exports = /* @__PURE__ */ __exportAll({
|
|
|
7
8
|
});
|
|
8
9
|
const PREFIX_REGEX = /^\/v(\d+)\//;
|
|
9
10
|
const versioningPlugin = async (fastify, opts) => {
|
|
10
|
-
const { type, defaultVersion = "1", headerName = "accept-version", responseHeader = "x-api-version", deprecated = [], sunset } = opts;
|
|
11
|
+
const { type, defaultVersion = "1", headerName = "accept-version", responseHeader = "x-api-version", versions, deprecated = [], sunset } = opts;
|
|
11
12
|
const deprecatedSet = new Set(deprecated);
|
|
13
|
+
const supportedSet = versions ? new Set(versions) : void 0;
|
|
14
|
+
if (supportedSet && !supportedSet.has(defaultVersion)) throw new Error(`versioningPlugin: defaultVersion '${defaultVersion}' is not in versions [${[...supportedSet].join(", ")}]`);
|
|
12
15
|
fastify.decorateRequest("apiVersion", defaultVersion);
|
|
13
16
|
fastify.addHook("onRequest", async (request, reply) => {
|
|
14
17
|
let version = defaultVersion;
|
|
@@ -19,6 +22,10 @@ const versioningPlugin = async (fastify, opts) => {
|
|
|
19
22
|
const match = request.url.match(PREFIX_REGEX);
|
|
20
23
|
if (match) version = match[1] ?? defaultVersion;
|
|
21
24
|
}
|
|
25
|
+
if (supportedSet && !supportedSet.has(version)) throw createDomainError("arc.unsupported_api_version", `API version '${version}' is not supported`, 400, {
|
|
26
|
+
requested: version,
|
|
27
|
+
supported: [...supportedSet]
|
|
28
|
+
});
|
|
22
29
|
request.apiVersion = version;
|
|
23
30
|
reply.header(responseHeader, version);
|
|
24
31
|
if (deprecatedSet.has(version)) {
|
|
@@ -66,7 +66,15 @@ interface SendQueueOptions {
|
|
|
66
66
|
*/
|
|
67
67
|
declare class SendQueue {
|
|
68
68
|
private entries;
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Logical queue head — `entries[head..]` is the live queue. Draining
|
|
71
|
+
* advances the pointer instead of `shift()`ing (which moves every
|
|
72
|
+
* trailing element AND invalidated every tracked coalesce index,
|
|
73
|
+
* making a full drain O(n²)). The prefix is garbage-collected by
|
|
74
|
+
* {@link compactIfNeeded} — amortized O(1) per drained frame.
|
|
75
|
+
*/
|
|
76
|
+
private head;
|
|
77
|
+
/** `coalesceKey → ABSOLUTE entries[] index` for O(1) replacement. */
|
|
70
78
|
private coalesceIndex;
|
|
71
79
|
private socket;
|
|
72
80
|
private capacity;
|
|
@@ -92,6 +100,12 @@ declare class SendQueue {
|
|
|
92
100
|
*/
|
|
93
101
|
flush(): void;
|
|
94
102
|
size(): number;
|
|
103
|
+
/**
|
|
104
|
+
* Drop the drained prefix once it dominates the array. Rebuilding the
|
|
105
|
+
* (small) coalesce index here is the amortized cost that replaces the
|
|
106
|
+
* per-frame O(n) reindex the old `shift()` drain paid.
|
|
107
|
+
*/
|
|
108
|
+
private compactIfNeeded;
|
|
95
109
|
dispose(): void;
|
|
96
110
|
/**
|
|
97
111
|
* One-shot retry timer. Called when `flush()` hits backpressure and
|