@aws-blocks/core 0.3.0 → 0.5.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 +25 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +32 -0
- package/dist/api.test.js +22 -0
- package/dist/bb-utils.d.ts +1 -0
- package/dist/bb-utils.d.ts.map +1 -1
- package/dist/bb-utils.js +3 -0
- package/dist/cdk/apigateway-account.d.ts +33 -0
- package/dist/cdk/apigateway-account.d.ts.map +1 -0
- package/dist/cdk/apigateway-account.js +60 -0
- package/dist/cdk/blocks-backend.d.ts +11 -0
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +72 -13
- package/dist/cdk/blocks-backend.test.js +88 -1
- package/dist/cdk/blocks-defaults.d.ts +76 -1
- package/dist/cdk/blocks-defaults.d.ts.map +1 -1
- package/dist/cdk/blocks-defaults.js +17 -1
- package/dist/cdk/blocks-defaults.test.js +17 -0
- package/dist/cdk/blocks-stack.test.js +44 -3
- package/dist/cdk/compute/compute-registry.d.ts +19 -0
- package/dist/cdk/compute/compute-registry.d.ts.map +1 -0
- package/dist/cdk/compute/compute-registry.js +38 -0
- package/dist/cdk/compute/compute.d.ts +82 -2
- package/dist/cdk/compute/compute.d.ts.map +1 -1
- package/dist/cdk/compute/compute.js +64 -2
- package/dist/cdk/compute/default-compute-factory.d.ts +1 -0
- package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -1
- package/dist/cdk/config-registry.d.ts +34 -4
- package/dist/cdk/config-registry.d.ts.map +1 -1
- package/dist/cdk/config-registry.js +83 -25
- package/dist/cdk/config-registry.test.d.ts +2 -0
- package/dist/cdk/config-registry.test.d.ts.map +1 -0
- package/dist/cdk/config-registry.test.js +127 -0
- package/dist/cdk/dashboard-registry.d.ts +41 -0
- package/dist/cdk/dashboard-registry.d.ts.map +1 -0
- package/dist/cdk/dashboard-registry.js +61 -0
- package/dist/cdk/index.d.ts +90 -6
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +135 -13
- package/dist/cdk/internal.d.ts +3 -1
- package/dist/cdk/internal.d.ts.map +1 -1
- package/dist/cdk/internal.js +4 -1
- package/dist/cdk/tracer-registry.d.ts +31 -0
- package/dist/cdk/tracer-registry.d.ts.map +1 -0
- package/dist/cdk/tracer-registry.js +49 -0
- package/dist/cdk/vpc-requirements-registry.d.ts +33 -0
- package/dist/cdk/vpc-requirements-registry.d.ts.map +1 -0
- package/dist/cdk/vpc-requirements-registry.js +46 -0
- package/dist/cdk/vpc-types.d.ts +151 -0
- package/dist/cdk/vpc-types.d.ts.map +1 -0
- package/dist/cdk/vpc-types.js +3 -0
- package/dist/cdk/vpc.d.ts +59 -0
- package/dist/cdk/vpc.d.ts.map +1 -0
- package/dist/cdk/vpc.js +298 -0
- package/dist/cdk/vpc.test.d.ts +2 -0
- package/dist/cdk/vpc.test.d.ts.map +1 -0
- package/dist/cdk/vpc.test.js +285 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/common/config.d.ts +34 -0
- package/dist/common/config.d.ts.map +1 -1
- package/dist/common/config.js +45 -3
- package/dist/common/config.test.js +19 -0
- package/dist/common/index.d.ts +8 -0
- package/dist/common/index.d.ts.map +1 -1
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +25 -0
- package/dist/hosting.d.ts +9 -0
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +12 -1
- package/dist/hosting.test.js +63 -1
- package/dist/index.cdk.d.ts +3 -2
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lambda-handler.d.ts +17 -0
- package/dist/lambda-handler.d.ts.map +1 -1
- package/dist/lambda-handler.js +61 -5
- package/dist/lambda-handler.test.js +191 -2
- package/dist/raw-route.d.ts +15 -1
- package/dist/raw-route.d.ts.map +1 -1
- package/dist/raw-route.js +96 -12
- package/dist/raw-route.test.js +332 -1
- package/dist/scripts/dev-server.d.ts.map +1 -1
- package/dist/scripts/dev-server.js +11 -0
- package/dist/scripts/extract-ts-types.d.ts.map +1 -1
- package/dist/scripts/extract-ts-types.js +107 -23
- package/dist/scripts/extract-ts-types.test.js +225 -26
- package/dist/scripts/generate-spec.d.ts.map +1 -1
- package/dist/scripts/generate-spec.js +14 -5
- package/dist/scripts/generate-spec.test.js +93 -0
- package/dist/scripts/sandbox-empty-buckets.test.d.ts +2 -0
- package/dist/scripts/sandbox-empty-buckets.test.d.ts.map +1 -0
- package/dist/scripts/sandbox-empty-buckets.test.js +171 -0
- package/dist/scripts/sandbox.d.ts +54 -0
- package/dist/scripts/sandbox.d.ts.map +1 -1
- package/dist/scripts/sandbox.js +163 -25
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -1
- package/src/api.test.ts +25 -0
- package/src/api.ts +39 -0
- package/src/bb-utils.ts +3 -0
- package/src/cdk/apigateway-account.ts +66 -0
- package/src/cdk/blocks-backend.test.ts +168 -60
- package/src/cdk/blocks-backend.ts +299 -227
- package/src/cdk/blocks-defaults.test.ts +21 -0
- package/src/cdk/blocks-defaults.ts +79 -1
- package/src/cdk/blocks-stack.test.ts +57 -13
- package/src/cdk/compute/compute-registry.ts +45 -0
- package/src/cdk/compute/compute.ts +114 -2
- package/src/cdk/compute/default-compute-factory.ts +1 -0
- package/src/cdk/config-registry.test.ts +149 -0
- package/src/cdk/config-registry.ts +92 -34
- package/src/cdk/dashboard-registry.ts +68 -0
- package/src/cdk/index.ts +427 -256
- package/src/cdk/internal.ts +6 -2
- package/src/cdk/tracer-registry.ts +54 -0
- package/src/cdk/vpc-requirements-registry.ts +63 -0
- package/src/cdk/vpc-types.ts +158 -0
- package/src/cdk/vpc.test.ts +348 -0
- package/src/cdk/vpc.ts +336 -0
- package/src/client/index.ts +1 -1
- package/src/common/config.test.ts +21 -0
- package/src/common/config.ts +47 -3
- package/src/common/index.ts +8 -0
- package/src/errors.ts +26 -0
- package/src/hosting.test.ts +87 -1
- package/src/hosting.ts +24 -1
- package/src/index.cdk.ts +11 -1
- package/src/index.ts +1 -1
- package/src/lambda-handler.test.ts +220 -2
- package/src/lambda-handler.ts +65 -4
- package/src/raw-route.test.ts +427 -1
- package/src/raw-route.ts +125 -12
- package/src/scripts/dev-server.ts +12 -1
- package/src/scripts/extract-ts-types.test.ts +228 -26
- package/src/scripts/extract-ts-types.ts +104 -20
- package/src/scripts/generate-spec.test.ts +101 -0
- package/src/scripts/generate-spec.ts +15 -5
- package/src/scripts/sandbox-empty-buckets.test.ts +191 -0
- package/src/scripts/sandbox.ts +185 -24
- package/src/version.ts +1 -1
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import { describe, it, beforeEach } from 'node:test';
|
|
4
4
|
import assert from 'node:assert';
|
|
5
|
-
import { createLambdaHandler, _resetCorsPatterns, requestCookies, isApiGatewayHttpEvent, computeHttpDeadlineMs, classifyEvent, buildEventUrl, isLoopbackForwardedHost } from './lambda-handler.js';
|
|
6
|
-
import { registerRoute, clearRouteRegistry } from './raw-route.js';
|
|
5
|
+
import { createLambdaHandler, _resetCorsPatterns, requestCookies, isApiGatewayHttpEvent, computeHttpDeadlineMs, classifyEvent, buildEventUrl, isLoopbackForwardedHost, TransientConfigError } from './lambda-handler.js';
|
|
6
|
+
import { registerRoute, clearRouteRegistry, getRegisteredRoutes } from './raw-route.js';
|
|
7
7
|
import { decodeRpcResponse } from './rpc.js';
|
|
8
|
+
import { _resetConfigCache, _setS3Fetcher } from './common/config.js';
|
|
8
9
|
beforeEach(() => {
|
|
9
10
|
clearRouteRegistry();
|
|
10
11
|
});
|
|
@@ -22,6 +23,134 @@ async function invoke(backend, event) {
|
|
|
22
23
|
const handler = createLambdaHandler(async () => backend);
|
|
23
24
|
return handler(event);
|
|
24
25
|
}
|
|
26
|
+
// ── init self-heal (retry on failed initialization) ─────────────────────────
|
|
27
|
+
describe('createLambdaHandler — init self-heal', () => {
|
|
28
|
+
it('retries initialize() on a later request instead of caching a failed init', async () => {
|
|
29
|
+
let initCalls = 0;
|
|
30
|
+
const backend = {
|
|
31
|
+
api: (_ctx) => ({
|
|
32
|
+
async echo(msg) {
|
|
33
|
+
return { msg };
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
// Fail the first initialization (e.g. config not readable yet in the brief post-deploy
|
|
38
|
+
// window), then succeed. The SAME handler instance must recover — a cached rejected
|
|
39
|
+
// initPromise would poison the container and fail every subsequent request.
|
|
40
|
+
const backendFactory = async () => {
|
|
41
|
+
initCalls++;
|
|
42
|
+
if (initCalls === 1)
|
|
43
|
+
throw new Error('transient init failure');
|
|
44
|
+
return backend;
|
|
45
|
+
};
|
|
46
|
+
const handler = createLambdaHandler(backendFactory);
|
|
47
|
+
// 1st request: init fails, so the handler rejects.
|
|
48
|
+
await assert.rejects(() => handler(makeEvent()), /transient init failure/);
|
|
49
|
+
// 2nd request: init is retried and succeeds → 200 (not a re-thrown cached rejection).
|
|
50
|
+
const res = (await handler(makeEvent()));
|
|
51
|
+
assert.strictEqual(res.statusCode, 200);
|
|
52
|
+
assert.strictEqual(initCalls, 2, 'initialize() must be retried on the next request, not cached');
|
|
53
|
+
});
|
|
54
|
+
it('recovers from a transient-empty (post-deploy 404) config load on the next request', async () => {
|
|
55
|
+
// Reproduces the poisoned-container bug: the first S3 load hits the transient
|
|
56
|
+
// post-deploy window (NoSuchKey), so config resolves empty; the handler must
|
|
57
|
+
// NOT lock in that empty config, and the NEXT request must re-fetch and pick
|
|
58
|
+
// up the now-present config.
|
|
59
|
+
_resetConfigCache();
|
|
60
|
+
process.env.BLOCKS_CONFIG_BUCKET = 'test-bucket';
|
|
61
|
+
process.env.BLOCKS_CONFIG_KEY = 'blocks-config.json';
|
|
62
|
+
delete process.env.SELFHEAL_KEY;
|
|
63
|
+
let fetchCall = 0;
|
|
64
|
+
const notFound = new Error('The specified key does not exist.');
|
|
65
|
+
notFound.name = 'NoSuchKey';
|
|
66
|
+
_setS3Fetcher(async () => {
|
|
67
|
+
fetchCall++;
|
|
68
|
+
if (fetchCall === 1)
|
|
69
|
+
throw notFound; // config not readable yet
|
|
70
|
+
return JSON.stringify({ SELFHEAL_KEY: 'ready' });
|
|
71
|
+
});
|
|
72
|
+
let backendImports = 0;
|
|
73
|
+
const handler = createLambdaHandler(async () => {
|
|
74
|
+
backendImports++;
|
|
75
|
+
return {
|
|
76
|
+
api: (_ctx) => ({
|
|
77
|
+
async echo(msg) { return { msg, cfg: process.env.SELFHEAL_KEY }; },
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
// 1st request: transient-empty load → initialize() throws TransientConfigError
|
|
83
|
+
// BEFORE importing the backend (so we never lock in empty config), and the
|
|
84
|
+
// request rejects.
|
|
85
|
+
await assert.rejects(() => handler(makeEvent()), TransientConfigError);
|
|
86
|
+
assert.strictEqual(backendImports, 0, 'backend must NOT be imported while config is unresolved');
|
|
87
|
+
// 2nd request: config now present → initialize() re-runs, re-fetches, and
|
|
88
|
+
// injects the config into process.env before importing the backend.
|
|
89
|
+
const res = (await handler(makeEvent()));
|
|
90
|
+
assert.strictEqual(res.statusCode, 200);
|
|
91
|
+
const body = JSON.parse(res.body);
|
|
92
|
+
assert.strictEqual(body.result.cfg, 'ready', 'now-present config was injected on the retry');
|
|
93
|
+
assert.strictEqual(backendImports, 1, 'backend imported exactly once, on the successful retry');
|
|
94
|
+
assert.strictEqual(fetchCall, 2, 'S3 was re-fetched on the retry (transient miss is not cached)');
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
_resetConfigCache();
|
|
98
|
+
delete process.env.BLOCKS_CONFIG_BUCKET;
|
|
99
|
+
delete process.env.BLOCKS_CONFIG_KEY;
|
|
100
|
+
delete process.env.SELFHEAL_KEY;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
it('does NOT re-init for a config-less app (no bucket, local dev) — initialize runs once', async () => {
|
|
104
|
+
_resetConfigCache();
|
|
105
|
+
delete process.env.BLOCKS_CONFIG_BUCKET;
|
|
106
|
+
delete process.env.BLOCKS_CONFIG_KEY;
|
|
107
|
+
let backendImports = 0;
|
|
108
|
+
const handler = createLambdaHandler(async () => {
|
|
109
|
+
backendImports++;
|
|
110
|
+
return { api: (_ctx) => ({ async echo(msg) { return { msg }; } }) };
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
const r1 = (await handler(makeEvent()));
|
|
114
|
+
const r2 = (await handler(makeEvent()));
|
|
115
|
+
assert.strictEqual(r1.statusCode, 200);
|
|
116
|
+
assert.strictEqual(r2.statusCode, 200);
|
|
117
|
+
assert.strictEqual(backendImports, 1, 'no re-init for a genuinely config-less (local dev) app');
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
_resetConfigCache();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
it('does NOT re-init or spin for a genuinely-empty ({}) S3 config', async () => {
|
|
124
|
+
// A real, readable empty config must be treated as resolved — distinct from
|
|
125
|
+
// the transient 404 miss — so the container does not throw/retry forever nor
|
|
126
|
+
// re-fetch S3 on every request.
|
|
127
|
+
_resetConfigCache();
|
|
128
|
+
process.env.BLOCKS_CONFIG_BUCKET = 'test-bucket';
|
|
129
|
+
process.env.BLOCKS_CONFIG_KEY = 'blocks-config.json';
|
|
130
|
+
let fetchCall = 0;
|
|
131
|
+
_setS3Fetcher(async () => { fetchCall++; return JSON.stringify({}); });
|
|
132
|
+
let backendImports = 0;
|
|
133
|
+
const handler = createLambdaHandler(async () => {
|
|
134
|
+
backendImports++;
|
|
135
|
+
return { api: (_ctx) => ({ async echo(msg) { return { msg }; } }) };
|
|
136
|
+
});
|
|
137
|
+
try {
|
|
138
|
+
const r1 = (await handler(makeEvent()));
|
|
139
|
+
const r2 = (await handler(makeEvent()));
|
|
140
|
+
const r3 = (await handler(makeEvent()));
|
|
141
|
+
assert.strictEqual(r1.statusCode, 200);
|
|
142
|
+
assert.strictEqual(r2.statusCode, 200);
|
|
143
|
+
assert.strictEqual(r3.statusCode, 200);
|
|
144
|
+
assert.strictEqual(backendImports, 1, 'genuinely-empty config resolves; no re-init');
|
|
145
|
+
assert.strictEqual(fetchCall, 1, 'S3 fetched once and cached — no per-request re-fetch spin');
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
_resetConfigCache();
|
|
149
|
+
delete process.env.BLOCKS_CONFIG_BUCKET;
|
|
150
|
+
delete process.env.BLOCKS_CONFIG_KEY;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
25
154
|
// ── RPC body tests ──────────────────────────────────────────────────────────
|
|
26
155
|
describe('createLambdaHandler — RPC body handling', () => {
|
|
27
156
|
it('populates ctx.request.body as a ReadableStream for RPC calls', async () => {
|
|
@@ -224,6 +353,66 @@ describe('createLambdaHandler — RawRoute body handling', () => {
|
|
|
224
353
|
});
|
|
225
354
|
});
|
|
226
355
|
// ── Cookie extraction tests ─────────────────────────────────────────────────
|
|
356
|
+
// ── Unmatched RawRoute diagnostics ──────────────────────────────────────────
|
|
357
|
+
//
|
|
358
|
+
// A request that matches no RawRoute used to return 404 without a trace in
|
|
359
|
+
// CloudWatch, which is what made the duplicate-core-copy bug invisible.
|
|
360
|
+
describe('createLambdaHandler — unmatched RawRoute diagnostics', () => {
|
|
361
|
+
/** Run `fn` with console.error captured; returns the collected lines. */
|
|
362
|
+
async function captureErrors(fn) {
|
|
363
|
+
const lines = [];
|
|
364
|
+
const original = console.error;
|
|
365
|
+
console.error = (...args) => {
|
|
366
|
+
lines.push(args.map((a) => String(a)).join(' '));
|
|
367
|
+
};
|
|
368
|
+
try {
|
|
369
|
+
await fn();
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
console.error = original;
|
|
373
|
+
}
|
|
374
|
+
return lines;
|
|
375
|
+
}
|
|
376
|
+
it('logs method, path and route count when no RawRoute matches', async () => {
|
|
377
|
+
registerRoute({ method: 'GET', path: '/known', handler: async (ctx) => ctx.response.send({ ok: true }) });
|
|
378
|
+
let result;
|
|
379
|
+
const logged = await captureErrors(async () => {
|
|
380
|
+
result = await invoke({}, makeEvent({ httpMethod: 'GET', path: '/unknown', body: null }));
|
|
381
|
+
});
|
|
382
|
+
assert.strictEqual(result.statusCode, 404);
|
|
383
|
+
// The count must match the registry the dispatcher actually consulted —
|
|
384
|
+
// a "0 routes registered" line is the fingerprint of a split registry.
|
|
385
|
+
const expectedCount = getRegisteredRoutes().length;
|
|
386
|
+
assert.ok(expectedCount > 0, 'precondition: routes are registered');
|
|
387
|
+
assert.ok(logged.some((line) => line.includes('No RawRoute matched GET /unknown') &&
|
|
388
|
+
line.includes(`${expectedCount} routes registered`)), `expected a diagnostic for the unmatched route, got: ${JSON.stringify(logged)}`);
|
|
389
|
+
});
|
|
390
|
+
it('does not log when a RawRoute matches', async () => {
|
|
391
|
+
registerRoute({ method: 'GET', path: '/known', handler: async (ctx) => ctx.response.send({ ok: true }) });
|
|
392
|
+
const logged = await captureErrors(async () => {
|
|
393
|
+
const result = await invoke({}, makeEvent({ httpMethod: 'GET', path: '/known', body: null }));
|
|
394
|
+
assert.strictEqual(result.statusCode, 200);
|
|
395
|
+
});
|
|
396
|
+
assert.deepStrictEqual(logged, []);
|
|
397
|
+
});
|
|
398
|
+
it('names duplicate core copies as the likely cause when more than one is loaded', async () => {
|
|
399
|
+
const REGISTRY_KEY = '__AWS_BLOCKS_RAW_ROUTE_REGISTRY_V1__';
|
|
400
|
+
const state = globalThis[REGISTRY_KEY];
|
|
401
|
+
assert.ok(state, `route registry state must live on globalThis['${REGISTRY_KEY}']`);
|
|
402
|
+
const originalCopies = state.copies;
|
|
403
|
+
state.copies = 2;
|
|
404
|
+
try {
|
|
405
|
+
const logged = await captureErrors(async () => {
|
|
406
|
+
const result = await invoke({}, makeEvent({ httpMethod: 'GET', path: '/unknown', body: null }));
|
|
407
|
+
assert.strictEqual(result.statusCode, 404);
|
|
408
|
+
});
|
|
409
|
+
assert.ok(logged.some((line) => line.includes('2 copies of @aws-blocks/core are loaded')), `expected the copy count in the diagnostic, got: ${JSON.stringify(logged)}`);
|
|
410
|
+
}
|
|
411
|
+
finally {
|
|
412
|
+
state.copies = originalCopies;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
});
|
|
227
416
|
describe('createLambdaHandler — cookie extraction', () => {
|
|
228
417
|
it('extracts cookies from lowercase "cookie" header (API Gateway v2)', async () => {
|
|
229
418
|
let capturedCookies = '';
|
package/dist/raw-route.d.ts
CHANGED
|
@@ -72,6 +72,16 @@ export interface RegisteredRoute {
|
|
|
72
72
|
paramNames: string[];
|
|
73
73
|
handler: (context: BlocksContext) => Promise<void>;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Number of `@aws-blocks/core` copies taking part in the shared route registry.
|
|
77
|
+
*
|
|
78
|
+
* `1` for a healthy install. A higher number means the dependency tree carries
|
|
79
|
+
* duplicates — routes still work (the registry is shared), but it is worth
|
|
80
|
+
* reporting in diagnostics.
|
|
81
|
+
*
|
|
82
|
+
* **Internal.** Not re-exported from the package entry point.
|
|
83
|
+
*/
|
|
84
|
+
export declare function getLoadedCoreCopies(): number;
|
|
75
85
|
/** Prevent further route registration. Call after initialization is complete. */
|
|
76
86
|
export declare function lockRouteRegistry(): void;
|
|
77
87
|
/** Re-allow route registration (useful in tests after lock). */
|
|
@@ -92,7 +102,11 @@ export declare function registerRoute(options: RawRouteOptions & {
|
|
|
92
102
|
}): void;
|
|
93
103
|
/** Return a read-only snapshot of all registered routes. */
|
|
94
104
|
export declare function getRegisteredRoutes(): readonly RegisteredRoute[];
|
|
95
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Remove all registered routes and unlock registration (used by tests).
|
|
107
|
+
*
|
|
108
|
+
* Leaves the copy counter alone — it tracks module copies, not routes.
|
|
109
|
+
*/
|
|
96
110
|
export declare function clearRouteRegistry(): void;
|
|
97
111
|
/**
|
|
98
112
|
* Match an incoming HTTP request against registered routes.
|
package/dist/raw-route.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"raw-route.d.ts","sourceRoot":"","sources":["../src/raw-route.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAKrD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAE1F,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,UAAU,CAAC;IACnB;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc;;CAEjB,CAAC;AAIX;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAc1E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,CAEjG;AAID,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;
|
|
1
|
+
{"version":3,"file":"raw-route.d.ts","sourceRoot":"","sources":["../src/raw-route.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAKrD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAE1F,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,UAAU,CAAC;IACnB;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc;;CAEjB,CAAC;AAIX;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAc1E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,CAEjG;AAID,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAmGD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAID,iFAAiF;AACjF,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,gEAAgE;AAChE,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAwC/E;AAED,4DAA4D;AAC5D,wBAAgB,mBAAmB,IAAI,SAAS,eAAe,EAAE,CAEhE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAIzC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAoBnE;AA6BD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,CAgEnF"}
|
package/dist/raw-route.js
CHANGED
|
@@ -60,16 +60,94 @@ export function derivePathFromScope(scope, id) {
|
|
|
60
60
|
export function resolveRoutePath(scope, id, options) {
|
|
61
61
|
return options.path ?? derivePathFromScope(scope, id);
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Well-known `globalThis` key holding the shared registry state.
|
|
65
|
+
*
|
|
66
|
+
* A plain string rather than a `Symbol.for()` key, deliberately: duplicate
|
|
67
|
+
* copies are diagnosed by counting strings in the deployed bundle, and a
|
|
68
|
+
* symbol would be invisible to that. It also matches the existing
|
|
69
|
+
* `__BLOCKS_LAMBDA_EVENT_HANDLERS__` precedent.
|
|
70
|
+
*
|
|
71
|
+
* **The key is a permanent contract — do not bump it.** `_V1_` names the shape
|
|
72
|
+
* this file introduced; it is not a license to mint `_V2_`. The copies that
|
|
73
|
+
* share a process are usually *different versions* of `@aws-blocks/core` — that
|
|
74
|
+
* is how a tree ends up with duplicates in the first place — so a copy reading
|
|
75
|
+
* a different key does not get a private namespace, it re-splits the registry:
|
|
76
|
+
* routes registered through one key, dispatched through the other, 404. And
|
|
77
|
+
* silently, because each key carries its own `copies` counter, so both sides
|
|
78
|
+
* report `copies: 1` and the duplicate warning never fires. That is the bug
|
|
79
|
+
* this module exists to fix, reintroduced with its own diagnostic switched off.
|
|
80
|
+
*
|
|
81
|
+
* Evolve the shape **additively** instead: add optional fields, and tolerate
|
|
82
|
+
* fields a peer copy did not write (`getState()` does). If a `_V2_` key ever
|
|
83
|
+
* proves unavoidable, it has to adopt the V1 state — read it, and keep writing
|
|
84
|
+
* it — so that a mixed tree still routes through one table.
|
|
85
|
+
*/
|
|
86
|
+
const REGISTRY_KEY = '__AWS_BLOCKS_RAW_ROUTE_REGISTRY_V1__';
|
|
87
|
+
/** Whether *this* module copy has already counted itself. Intentionally module-local. */
|
|
88
|
+
let thisCopyCounted = false;
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the shared registry state, creating it on first access.
|
|
91
|
+
*
|
|
92
|
+
* The copy count is incremented here rather than at module load so that it
|
|
93
|
+
* counts the copies actually taking part in routing, not every copy a bundle
|
|
94
|
+
* happens to carry. Bundled code is where duplicate copies occur, and a copy
|
|
95
|
+
* that is imported but never routes is not what the diagnostic is about.
|
|
96
|
+
*
|
|
97
|
+
* Tree-shaking is not the reason. `sideEffects` in package.json lets a bundler
|
|
98
|
+
* drop this module whole when nothing it exports is used, but a module that
|
|
99
|
+
* *is* used keeps its top-level statements — measured with esbuild under
|
|
100
|
+
* `--bundle --minify`, and covered by the bundling test in `raw-route.test.ts`.
|
|
101
|
+
*/
|
|
102
|
+
function getState() {
|
|
103
|
+
const g = globalThis;
|
|
104
|
+
let state = g[REGISTRY_KEY];
|
|
105
|
+
if (!state) {
|
|
106
|
+
state = { routes: [], locked: false, copies: 0 };
|
|
107
|
+
g[REGISTRY_KEY] = state;
|
|
108
|
+
}
|
|
109
|
+
// The state may have been left by a copy that evolved the shape (see the
|
|
110
|
+
// contract on REGISTRY_KEY), so fill in what this copy needs rather than
|
|
111
|
+
// assume it is there. A missing `routes` would throw on the first
|
|
112
|
+
// registration, and a missing `copies` would go NaN on the increment below —
|
|
113
|
+
// silencing the duplicate warning, which is the failure this module exists to
|
|
114
|
+
// report. `locked` needs no default: absent reads as unlocked, which is what
|
|
115
|
+
// a registry still accepting registrations means.
|
|
116
|
+
if (!Array.isArray(state.routes))
|
|
117
|
+
state.routes = [];
|
|
118
|
+
if (typeof state.copies !== 'number')
|
|
119
|
+
state.copies = 0;
|
|
120
|
+
if (!thisCopyCounted) {
|
|
121
|
+
thisCopyCounted = true;
|
|
122
|
+
state.copies += 1;
|
|
123
|
+
if (state.copies > 1) {
|
|
124
|
+
console.warn(`AWS Blocks: ${state.copies} copies of @aws-blocks/core loaded in this process. ` +
|
|
125
|
+
'They now share one RawRoute registry, but duplicate copies indicate a ' +
|
|
126
|
+
'dependency-tree problem — deduplicate @aws-blocks/core.');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return state;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Number of `@aws-blocks/core` copies taking part in the shared route registry.
|
|
133
|
+
*
|
|
134
|
+
* `1` for a healthy install. A higher number means the dependency tree carries
|
|
135
|
+
* duplicates — routes still work (the registry is shared), but it is worth
|
|
136
|
+
* reporting in diagnostics.
|
|
137
|
+
*
|
|
138
|
+
* **Internal.** Not re-exported from the package entry point.
|
|
139
|
+
*/
|
|
140
|
+
export function getLoadedCoreCopies() {
|
|
141
|
+
return getState().copies;
|
|
142
|
+
}
|
|
65
143
|
const VALID_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']);
|
|
66
144
|
/** Prevent further route registration. Call after initialization is complete. */
|
|
67
145
|
export function lockRouteRegistry() {
|
|
68
|
-
|
|
146
|
+
getState().locked = true;
|
|
69
147
|
}
|
|
70
148
|
/** Re-allow route registration (useful in tests after lock). */
|
|
71
149
|
export function unlockRouteRegistry() {
|
|
72
|
-
|
|
150
|
+
getState().locked = false;
|
|
73
151
|
}
|
|
74
152
|
/**
|
|
75
153
|
* Register a raw HTTP route for runtime dispatch.
|
|
@@ -83,7 +161,8 @@ export function unlockRouteRegistry() {
|
|
|
83
161
|
* @throws {RawRouteErrors.DuplicateRoute} If the same method+path is registered twice.
|
|
84
162
|
*/
|
|
85
163
|
export function registerRoute(options) {
|
|
86
|
-
|
|
164
|
+
const state = getState();
|
|
165
|
+
if (state.locked) {
|
|
87
166
|
throw new Error('Routes must be registered during initialization. Cannot register routes after handler creation.');
|
|
88
167
|
}
|
|
89
168
|
if (!VALID_METHODS.has(options.method)) {
|
|
@@ -97,14 +176,14 @@ export function registerRoute(options) {
|
|
|
97
176
|
if (lowerPath === BLOCKS_NAMESPACE || lowerPath === BLOCKS_RPC_PREFIX || lowerPath.startsWith(`${BLOCKS_RPC_PREFIX}/`)) {
|
|
98
177
|
throw new Error(`Cannot register RawRoute at ${BLOCKS_NAMESPACE} or ${BLOCKS_RPC_PREFIX}/* — these paths are reserved for RPC dispatch`);
|
|
99
178
|
}
|
|
100
|
-
const existing =
|
|
179
|
+
const existing = state.routes.find((r) => r.method === options.method && r.path === normalizedPath);
|
|
101
180
|
if (existing) {
|
|
102
181
|
const err = new Error(`${RawRouteErrors.DuplicateRoute}: ${options.method} ${normalizedPath} is already registered`);
|
|
103
182
|
err.name = RawRouteErrors.DuplicateRoute;
|
|
104
183
|
throw err;
|
|
105
184
|
}
|
|
106
185
|
const { pattern, paramNames } = compilePath(normalizedPath);
|
|
107
|
-
|
|
186
|
+
state.routes.push({
|
|
108
187
|
method: options.method,
|
|
109
188
|
path: normalizedPath,
|
|
110
189
|
pattern,
|
|
@@ -114,12 +193,17 @@ export function registerRoute(options) {
|
|
|
114
193
|
}
|
|
115
194
|
/** Return a read-only snapshot of all registered routes. */
|
|
116
195
|
export function getRegisteredRoutes() {
|
|
117
|
-
return
|
|
196
|
+
return getState().routes;
|
|
118
197
|
}
|
|
119
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* Remove all registered routes and unlock registration (used by tests).
|
|
200
|
+
*
|
|
201
|
+
* Leaves the copy counter alone — it tracks module copies, not routes.
|
|
202
|
+
*/
|
|
120
203
|
export function clearRouteRegistry() {
|
|
121
|
-
|
|
122
|
-
|
|
204
|
+
const state = getState();
|
|
205
|
+
state.routes.length = 0;
|
|
206
|
+
state.locked = false;
|
|
123
207
|
}
|
|
124
208
|
/**
|
|
125
209
|
* Match an incoming HTTP request against registered routes.
|
|
@@ -129,7 +213,7 @@ export function clearRouteRegistry() {
|
|
|
129
213
|
export function matchRoute(method, path) {
|
|
130
214
|
// Normalize incoming path: collapse double slashes
|
|
131
215
|
path = path.replace(/\/+/g, '/');
|
|
132
|
-
for (const route of
|
|
216
|
+
for (const route of getState().routes) {
|
|
133
217
|
if (route.method !== method)
|
|
134
218
|
continue;
|
|
135
219
|
const match = route.pattern.exec(path);
|