Skip to content

Commit d14bda4

Browse files
maxdaytrivikr
andauthored
feat: lazy loading (#13)
Co-authored-by: Trivikram Kamat <[email protected]>
1 parent ab63ff9 commit d14bda4

13 files changed

+960
-701
lines changed

.changeset/changelog.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,19 @@ export const getDependencyReleaseLine = (changesets, dependenciesUpdated) => {
1414
.join(", ")}]:`;
1515

1616
const updatedDepsList = dependenciesUpdated.map(
17-
(dependency) => ` - ${dependency.name}@${dependency.newVersion}`
17+
(dependency) => ` - ${dependency.name}@${dependency.newVersion}`,
1818
);
1919

2020
return [changesetLink, ...updatedDepsList].join("\n");
2121
};
2222

2323
export const getReleaseLine = (changeset, _type) => {
2424
const { commit, summary } = changeset;
25-
const [firstLine, ...futureLines] = summary.split("\n").map((l) => l.trimRight());
25+
const [firstLine, ...futureLines] = summary
26+
.split("\n")
27+
.map((l) => l.trimRight());
2628

2729
return `- ${firstLine} (${getGithubCommitWithLink(commit)})${
2830
futureLines.length > 0 ? futureLines.map((l) => ` ${l}`).join("\n") : ""
2931
}`;
30-
};
32+
};

.changeset/goofy-planes-tease.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@aws/lambda-invoke-store": minor
3+
---
4+
5+
Invoke Store is now accessible via `InvokeStore.getInstanceAsync()` instead of direct instantiation
6+
7+
- Lazy loads `node:async_hooks` to improve startup performance
8+
- Selects dynamic implementation based on Lambda environment:
9+
- Single-context implementation for standard Lambda executions
10+
- Multi-context implementation (using AsyncLocalStorage)

README.md

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,109 +33,117 @@ export const handler = async (event, context) => {
3333
// The RIC has already initialized the InvokeStore with requestId and X-Ray traceId
3434

3535
// Access Lambda context data
36-
console.log(`Processing request: ${InvokeStore.getRequestId()}`);
36+
const invokeStore = await InvokeStore.getInstanceAsync();
37+
console.log(`Processing request: ${invokeStore.getRequestId()}`);
3738

3839
// Store custom data
39-
InvokeStore.set("userId", event.userId);
40+
invokeStore.set("userId", event.userId);
4041

4142
// Data persists across async operations
4243
await processData(event);
4344

4445
// Retrieve custom data
45-
const userId = InvokeStore.get("userId");
46+
const userId = invokeStore.get("userId");
4647

4748
return {
48-
requestId: InvokeStore.getRequestId(),
49+
requestId: invokeStore.getRequestId(),
4950
userId,
5051
};
5152
};
5253

5354
// Context is preserved in async operations
5455
async function processData(event) {
5556
// Still has access to the same invoke context
56-
console.log(`Processing in same context: ${InvokeStore.getRequestId()}`);
57+
const invokeStore = await InvokeStore.getInstanceAsync();
58+
console.log(`Processing in same context: ${invokeStore.getRequestId()}`);
5759

5860
// Can set additional data
59-
InvokeStore.set("processedData", { result: "success" });
61+
invokeStore.set("processedData", { result: "success" });
6062
}
6163
```
6264

6365
## API Reference
6466

65-
### InvokeStore.getContext()
67+
### InvokeStore.getInstanceAsync()
68+
First, get an instance of the InvokeStore:
69+
```typescript
70+
const invokeStore = await InvokeStore.getInstanceAsync();
71+
```
72+
73+
### invokeStore.getContext()
6674

6775
Returns the complete current context or `undefined` if outside a context.
6876

6977
```typescript
70-
const context = InvokeStore.getContext();
78+
const context = invokeStore.getContext();
7179
```
7280

73-
### InvokeStore.get(key)
81+
### invokeStore.get(key)
7482

7583
Gets a value from the current context.
7684

7785
```typescript
78-
const requestId = InvokeStore.get(InvokeStore.PROTECTED_KEYS.REQUEST_ID);
79-
const customValue = InvokeStore.get("customKey");
86+
const requestId = invokeStore.get(InvokeStoreBase.PROTECTED_KEYS.REQUEST_ID);
87+
const customValue = invokeStore.get("customKey");
8088
```
8189

82-
### InvokeStore.set(key, value)
90+
### invokeStore.set(key, value)
8391

8492
Sets a custom value in the current context. Protected Lambda fields cannot be modified.
8593

8694
```typescript
87-
InvokeStore.set("userId", "user-123");
88-
InvokeStore.set("timestamp", Date.now());
95+
invokeStore.set("userId", "user-123");
96+
invokeStore.set("timestamp", Date.now());
8997

9098
// This will throw an error:
91-
// InvokeStore.set(InvokeStore.PROTECTED_KEYS.REQUEST_ID, 'new-id');
99+
// invokeStore.set(InvokeStoreBase.PROTECTED_KEYS.REQUEST_ID, 'new-id');
92100
```
93101

94-
### InvokeStore.getRequestId()
102+
### invokeStore.getRequestId()
95103

96104
Convenience method to get the current request ID.
97105

98106
```typescript
99-
const requestId = InvokeStore.getRequestId(); // Returns '-' if outside context
107+
const requestId = invokeStore.getRequestId(); // Returns '-' if outside context
100108
```
101109

102-
### InvokeStore.getTenantId()
110+
### invokeStore.getTenantId()
103111

104112
Convenience method to get the tenant ID.
105113

106114
```typescript
107-
const requestId = InvokeStore.getTenantId();
115+
const requestId = invokeStore.getTenantId();
108116
```
109117

110-
### InvokeStore.getXRayTraceId()
118+
### invokeStore.getXRayTraceId()
111119

112120
Convenience method to get the current [X-Ray trace ID](https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-traces). This ID is used for distributed tracing across AWS services.
113121

114122
```typescript
115-
const traceId = InvokeStore.getXRayTraceId(); // Returns undefined if not set or outside context
123+
const traceId = invokeStore.getXRayTraceId(); // Returns undefined if not set or outside context
116124
```
117125

118-
### InvokeStore.hasContext()
126+
### invokeStore.hasContext()
119127

120128
Checks if code is currently running within an invoke context.
121129

122130
```typescript
123-
if (InvokeStore.hasContext()) {
131+
if (invokeStore.hasContext()) {
124132
// We're inside an invoke context
125133
}
126134
```
127135

128-
### InvokeStore.run(context, fn)
136+
### invokeStore.run(context, fn)
129137

130138
> **Note**: This method is primarily used by the Lambda Runtime Interface Client (RIC) to initialize the context for each invocation. Lambda function developers typically don't need to call this method directly.
131139
132140
Runs a function within an invoke context.
133141

134142
```typescript
135-
InvokeStore.run(
143+
invokeStore.run(
136144
{
137-
[InvokeStore.PROTECTED_KEYS.REQUEST_ID]: "request-123",
138-
[InvokeStore.PROTECTED_KEYS.X_RAY_TRACE_ID]: "trace-456", // Optional X-Ray trace ID
145+
[InvokeStoreBase.PROTECTED_KEYS.REQUEST_ID]: "request-123",
146+
[InvokeStoreBase.PROTECTED_KEYS.X_RAY_TRACE_ID]: "trace-456", // Optional X-Ray trace ID
139147
customField: "value", // Optional custom fields
140148
},
141149
() => {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"build": "yarn clean && yarn build:types && node ./scripts/build-rollup.js",
3333
"build:types": "tsc -p tsconfig.types.json",
3434
"clean": "rm -rf dist-types dist-cjs dist-es",
35-
"test": "vitest run",
35+
"test": "vitest run --reporter verbose",
3636
"test:watch": "vitest watch",
3737
"release": "yarn build && changeset publish"
3838
},

src/invoke-store.benchmark.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { performance, PerformanceObserver } from "node:perf_hooks";
2+
3+
const obs = new PerformanceObserver((list) => {
4+
const entries = list.getEntries();
5+
entries.forEach((entry) => {
6+
console.log(`${entry.name}: ${entry.duration}ms`);
7+
});
8+
});
9+
obs.observe({ entryTypes: ["measure"] });
10+
11+
async function runBenchmark() {
12+
const iterations = 1000;
13+
process.env["AWS_LAMBDA_BENCHMARK_MODE"] = "1";
14+
15+
performance.mark("direct-single-start");
16+
for (let i = 0; i < iterations; i++) {
17+
const invokeStore = (await import("./invoke-store.js")).InvokeStore;
18+
await invokeStore.getInstanceAsync();
19+
const testing = invokeStore._testing;
20+
if (testing) {
21+
testing.reset();
22+
} else {
23+
throw "testing needs to be defined";
24+
}
25+
}
26+
performance.mark("direct-single-end");
27+
performance.measure(
28+
"Direct SingleStore Creation (1000 iterations)",
29+
"direct-single-start",
30+
"direct-single-end",
31+
);
32+
33+
performance.mark("direct-multi-start");
34+
process.env["AWS_LAMBDA_MAX_CONCURRENCY"] = "2";
35+
for (let i = 0; i < iterations; i++) {
36+
const invokeStore = (await import("./invoke-store.js")).InvokeStore;
37+
await invokeStore.getInstanceAsync();
38+
const testing = invokeStore._testing;
39+
if (testing) {
40+
testing.reset();
41+
} else {
42+
throw "testing needs to be defined";
43+
}
44+
}
45+
performance.mark("direct-multi-end");
46+
performance.measure(
47+
"Direct MultiStore Creation (1000 iterations)",
48+
"direct-multi-start",
49+
"direct-multi-end",
50+
);
51+
}
52+
53+
runBenchmark().catch(console.error);
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
2+
import { InvokeStore, InvokeStoreBase } from "./invoke-store.js";
3+
4+
describe("InvokeStore", async () => {
5+
let invokeStore: InvokeStoreBase;
6+
7+
beforeEach(async () => {
8+
vi.stubEnv("AWS_LAMBDA_MAX_CONCURRENCY", "2");
9+
vi.useFakeTimers();
10+
invokeStore = await InvokeStore.getInstanceAsync();
11+
});
12+
13+
afterEach(() => {
14+
vi.useRealTimers();
15+
});
16+
17+
describe("run", () => {
18+
it("should handle nested runs with different IDs", async () => {
19+
// GIVEN
20+
const traces: string[] = [];
21+
22+
// WHEN
23+
await invokeStore.run(
24+
{
25+
[InvokeStoreBase.PROTECTED_KEYS.REQUEST_ID]: "outer",
26+
},
27+
async () => {
28+
traces.push(`outer-${invokeStore.getRequestId()}`);
29+
await invokeStore.run(
30+
{
31+
[InvokeStoreBase.PROTECTED_KEYS.REQUEST_ID]: "inner",
32+
},
33+
async () => {
34+
traces.push(`inner-${invokeStore.getRequestId()}`);
35+
},
36+
);
37+
traces.push(`outer-again-${invokeStore.getRequestId()}`);
38+
},
39+
);
40+
41+
// THEN
42+
expect(traces).toEqual([
43+
"outer-outer",
44+
"inner-inner",
45+
"outer-again-outer",
46+
]);
47+
});
48+
49+
it("should maintain isolation between concurrent executions", async () => {
50+
// GIVEN
51+
const traces: string[] = [];
52+
53+
// WHEN - Simulate concurrent invocations
54+
const isolateTasks = Promise.all([
55+
invokeStore.run(
56+
{
57+
[InvokeStoreBase.PROTECTED_KEYS.REQUEST_ID]: "request-1",
58+
[InvokeStoreBase.PROTECTED_KEYS.X_RAY_TRACE_ID]: "trace-1",
59+
},
60+
async () => {
61+
traces.push(`start-1-${invokeStore.getRequestId()}`);
62+
await new Promise((resolve) => setTimeout(resolve, 10));
63+
traces.push(`end-1-${invokeStore.getRequestId()}`);
64+
},
65+
),
66+
invokeStore.run(
67+
{
68+
[InvokeStoreBase.PROTECTED_KEYS.REQUEST_ID]: "request-2",
69+
[InvokeStoreBase.PROTECTED_KEYS.X_RAY_TRACE_ID]: "trace-2",
70+
},
71+
async () => {
72+
traces.push(`start-2-${invokeStore.getRequestId()}`);
73+
await new Promise((resolve) => setTimeout(resolve, 5));
74+
traces.push(`end-2-${invokeStore.getRequestId()}`);
75+
},
76+
),
77+
]);
78+
vi.runAllTimers();
79+
await isolateTasks;
80+
81+
// THEN
82+
expect(traces).toEqual([
83+
"start-1-request-1",
84+
"start-2-request-2",
85+
"end-2-request-2",
86+
"end-1-request-1",
87+
]);
88+
});
89+
90+
it("should maintain isolation across async operations", async () => {
91+
// GIVEN
92+
const traces: string[] = [];
93+
94+
// WHEN
95+
await invokeStore.run(
96+
{
97+
[InvokeStoreBase.PROTECTED_KEYS.REQUEST_ID]: "request-1",
98+
},
99+
async () => {
100+
traces.push(`before-${invokeStore.getRequestId()}`);
101+
const task = new Promise((resolve) => {
102+
setTimeout(resolve, 1);
103+
}).then(() => {
104+
traces.push(`inside-${invokeStore.getRequestId()}`);
105+
});
106+
vi.runAllTimers();
107+
await task;
108+
traces.push(`after-${invokeStore.getRequestId()}`);
109+
},
110+
);
111+
112+
// THEN
113+
expect(traces).toEqual([
114+
"before-request-1",
115+
"inside-request-1",
116+
"after-request-1",
117+
]);
118+
});
119+
});
120+
});

0 commit comments

Comments
 (0)