@vltpkg/vsr 0.2.0 → 0.2.1

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.
@@ -1,208 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
-
3
- /**
4
- * This test correctly demonstrates Cloudflare's waitUntil API behavior
5
- * by properly simulating how background tasks are queued and executed.
6
- */
7
- describe('Cloudflare waitUntil Pattern', () => {
8
- /**
9
- * The key issue with the waitUntil API is that when using an IIFE, it executes immediately
10
- * before waitUntil gets the resulting promise:
11
- *
12
- * c.waitUntil((async () => { ... })()); <-- runs immediately!
13
- *
14
- * To simulate properly in tests, we need to:
15
- * 1. Store the function reference, not the promise
16
- * 2. Execute the function only when we want to run background tasks
17
- */
18
-
19
- // Simulate the Cloudflare Worker environment
20
- function createCloudflareEnvironment() {
21
- // Queue to store background task functions
22
- const backgroundTaskFns = [];
23
-
24
- // Execution context with waitUntil method
25
- const executionCtx = {
26
- waitUntil: vi.fn((promise) => {
27
- // Instead of accepting the promise, we'll wrap it
28
- // This stops the execution until we explicitly run it
29
- const promiseCreator = () => promise;
30
- backgroundTaskFns.push(promiseCreator);
31
- })
32
- };
33
-
34
- // Function to run all queued background tasks
35
- const runBackgroundTasks = async () => {
36
- const tasks = [...backgroundTaskFns];
37
- backgroundTaskFns.length = 0;
38
-
39
- for (const taskFn of tasks) {
40
- try {
41
- await taskFn();
42
- } catch (error) {
43
- console.error('Background task error:', error);
44
- }
45
- }
46
- };
47
-
48
- return {
49
- executionCtx,
50
- runBackgroundTasks,
51
- backgroundTaskFns
52
- };
53
- }
54
-
55
- it('should run the main handler first, then background tasks', async () => {
56
- // Setup
57
- const { executionCtx, runBackgroundTasks, backgroundTaskFns } = createCloudflareEnvironment();
58
-
59
- // Track execution order
60
- const executionOrder = [];
61
- let backgroundRan = false;
62
-
63
- // Handler function (simulates a Cloudflare Worker)
64
- async function handler() {
65
- executionOrder.push('main-start');
66
-
67
- // Queue a background task - this is how it's done in Cloudflare
68
- executionCtx.waitUntil(new Promise((resolve) => {
69
- const bgTask = () => {
70
- executionOrder.push('background');
71
- backgroundRan = true;
72
- resolve();
73
- };
74
-
75
- // Store the function reference so we can call it later
76
- // In real code, this would start executing immediately
77
- // but in our mock, we'll delay it
78
- setTimeout(bgTask, 0);
79
- }));
80
-
81
- executionOrder.push('main-end');
82
-
83
- return { status: 'success' };
84
- }
85
-
86
- // Execute the handler
87
- const result = await handler();
88
-
89
- // Verify:
90
- // 1. The main handler completed
91
- expect(result).toEqual({ status: 'success' });
92
-
93
- // 2. Main handler started and completed before any background tasks ran
94
- expect(executionOrder).toEqual(['main-start', 'main-end']);
95
- expect(backgroundRan).toBe(false);
96
-
97
- // 3. Background task was queued but not executed yet
98
- expect(backgroundTaskFns.length).toBe(1);
99
-
100
- // Now run the background tasks
101
- await runBackgroundTasks();
102
-
103
- // 4. Verify the execution order after background tasks were run
104
- expect(executionOrder).toEqual(['main-start', 'main-end', 'background']);
105
- expect(backgroundRan).toBe(true);
106
- });
107
-
108
- it('should implement the stale-while-revalidate caching pattern', async () => {
109
- // Setup
110
- const { executionCtx, runBackgroundTasks, backgroundTaskFns } = createCloudflareEnvironment();
111
-
112
- // Mock cache with stale data
113
- const cache = {
114
- data: {
115
- value: 'stale-data',
116
- timestamp: Date.now() - 3600000 // 1 hour old (stale)
117
- },
118
- get: vi.fn(function() { return this.data; }),
119
- set: vi.fn(function(newData) { this.data = newData; })
120
- };
121
-
122
- // Mock external API
123
- const fetchFromUpstream = vi.fn().mockResolvedValue('fresh-data');
124
-
125
- // Track execution
126
- const executionOrder = [];
127
- let refreshStarted = false;
128
- let refreshCompleted = false;
129
-
130
- // Handler function implementing stale-while-revalidate
131
- async function handleRequest() {
132
- executionOrder.push('handler-start');
133
-
134
- // Get data from cache
135
- const cachedData = cache.get();
136
-
137
- // Check if data is stale (older than 5 minutes)
138
- const isStale = Date.now() - cachedData.timestamp > 5 * 60 * 1000;
139
-
140
- if (isStale) {
141
- // Queue background refresh without waiting
142
- executionCtx.waitUntil(new Promise(async (resolve) => {
143
- // In a real implementation this would run immediately
144
- // In our test, we'll run it only when specifically requested
145
- const bgRefresh = async () => {
146
- refreshStarted = true;
147
- executionOrder.push('refresh-start');
148
- const freshData = await fetchFromUpstream();
149
- cache.set({
150
- value: freshData,
151
- timestamp: Date.now()
152
- });
153
- executionOrder.push('refresh-end');
154
- refreshCompleted = true;
155
- resolve();
156
- };
157
-
158
- // Don't call bgRefresh() here - we'll call it during runBackgroundTasks
159
- setTimeout(bgRefresh, 0);
160
- }));
161
- }
162
-
163
- // Return cached data immediately
164
- executionOrder.push('handler-end');
165
- return cachedData.value;
166
- }
167
-
168
- // Execute the handler
169
- const result = await handleRequest();
170
-
171
- // Verify:
172
- // 1. We got the stale data immediately
173
- expect(result).toBe('stale-data');
174
-
175
- // 2. Handler completed without waiting for refresh
176
- expect(executionOrder).toEqual(['handler-start', 'handler-end']);
177
- expect(refreshStarted).toBe(false);
178
- expect(refreshCompleted).toBe(false);
179
-
180
- // 3. The upstream API was not called yet
181
- expect(fetchFromUpstream).not.toHaveBeenCalled();
182
-
183
- // 4. Background task was queued
184
- expect(backgroundTaskFns.length).toBe(1);
185
-
186
- // Now run the background tasks
187
- await runBackgroundTasks();
188
-
189
- // 5. Verify the API was called during background refresh
190
- expect(fetchFromUpstream).toHaveBeenCalled();
191
- expect(refreshStarted).toBe(true);
192
- expect(refreshCompleted).toBe(true);
193
-
194
- // 6. Cache was updated with fresh data
195
- expect(cache.set).toHaveBeenCalledWith({
196
- value: 'fresh-data',
197
- timestamp: expect.any(Number)
198
- });
199
-
200
- // 7. Execution order shows background refresh happened after handler
201
- expect(executionOrder).toEqual([
202
- 'handler-start',
203
- 'handler-end',
204
- 'refresh-start',
205
- 'refresh-end'
206
- ]);
207
- });
208
- });
@@ -1,138 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
-
3
- /**
4
- * This is a minimalist test to demonstrate the waitUntil API in Cloudflare Workers.
5
- * The waitUntil API allows a Worker to return a response to the client immediately
6
- * while continuing to perform work in the background.
7
- *
8
- * This is especially useful for caching scenarios where:
9
- * 1. We want to return cached data to the client as quickly as possible
10
- * 2. We may need to refresh that cache in the background without blocking the response
11
- */
12
- describe('waitUntil API', () => {
13
- it('allows background work to continue after a response is sent', async () => {
14
- // In a Cloudflare Worker, the execution context has a waitUntil method
15
- // Here we create a mock of that context
16
- let backgroundWorkCompleted = false;
17
-
18
- const executionContext = {
19
- // This mock implementation runs the task after a short delay
20
- waitUntil: (promise) => {
21
- // In a real Worker, this would track the promise and ensure
22
- // the Worker doesn't terminate until all promises are resolved
23
- setTimeout(async () => {
24
- try {
25
- await promise;
26
- } catch (error) {
27
- console.error('Background task error:', error);
28
- }
29
- }, 0);
30
-
31
- // waitUntil itself returns immediately
32
- return Promise.resolve();
33
- }
34
- };
35
-
36
- // Record start time
37
- const startTime = Date.now();
38
-
39
- // This is how you use waitUntil in a real worker handler:
40
- async function handleRequest(request, env, ctx) {
41
- // Do some quick work and prepare a response
42
- const quickData = { message: "Here's your fast response!" };
43
-
44
- // Queue up some slow work to happen in the background
45
- ctx.waitUntil((async () => {
46
- // Simulate slow work (e.g., refreshing a cache, calling external API)
47
- await new Promise(resolve => setTimeout(resolve, 500));
48
- backgroundWorkCompleted = true;
49
- })());
50
-
51
- // Return the response immediately, without waiting for background work
52
- return quickData;
53
- }
54
-
55
- // Call our handler with the mock execution context
56
- const response = await handleRequest({}, {}, executionContext);
57
-
58
- // Measure response time
59
- const responseTime = Date.now() - startTime;
60
-
61
- // VERIFY:
62
-
63
- // 1. We got a quick response (much less than 500ms)
64
- expect(response).toEqual({ message: "Here's your fast response!" });
65
- expect(responseTime).toBeLessThan(100);
66
-
67
- // 2. Background work has NOT completed yet
68
- expect(backgroundWorkCompleted).toBe(false);
69
-
70
- // 3. Wait for background work and verify it completes
71
- await new Promise(resolve => setTimeout(resolve, 600));
72
- expect(backgroundWorkCompleted).toBe(true);
73
- });
74
-
75
- /**
76
- * Conceptual demonstration of the caching pattern
77
- */
78
- it('explains the stale-while-revalidate caching pattern', async () => {
79
- // This test explains the concept without actually testing code
80
-
81
- // 1. Mock a waitUntil function
82
- const waitUntil = vi.fn();
83
-
84
- // 2. Mock a database record with stale cached data
85
- const mockCacheResponse = {
86
- data: { version: '1.0.0' },
87
- updated: Date.now() - 3600000 // 1 hour old (stale)
88
- };
89
-
90
- // 3. Log what would happen in an actual implementation
91
- console.log('In a real implementation, a stale-while-revalidate caching strategy would:');
92
- console.log('1. Check if data exists in cache');
93
- console.log('2. If it exists but is stale, queue a background task to refresh it');
94
- console.log('3. Return the cached data immediately, without waiting for the refresh');
95
-
96
- // 4. Show what the implementation pattern looks like conceptually
97
- function conceptualImplementationPattern() {
98
- // Simulate the pattern without actually executing code
99
- const code = `
100
- async function getDataWithBackgroundRefresh(key, db, ctx) {
101
- // Get data from cache
102
- const cached = await db.getData(key);
103
-
104
- if (cached) {
105
- // Check if cache is stale
106
- const isStale = Date.now() - cached.updated > FIVE_MINUTES;
107
-
108
- if (isStale) {
109
- // Queue background refresh WITHOUT awaiting it
110
- ctx.waitUntil((async () => {
111
- const fresh = await fetchFromUpstream(key);
112
- await db.saveToCache(key, fresh);
113
- })());
114
- }
115
-
116
- // Return cached data immediately
117
- return cached.data;
118
- }
119
-
120
- // If not in cache, fetch directly
121
- const data = await fetchFromUpstream(key);
122
- await db.saveToCache(key, data);
123
- return data;
124
- }`;
125
- return code;
126
- }
127
-
128
- // 5. Verify the explanation was provided
129
- expect(conceptualImplementationPattern()).toContain('ctx.waitUntil');
130
- expect(conceptualImplementationPattern()).toContain('return cached.data');
131
-
132
- // Note: In a real implementation, we would:
133
- // 1. Return stale cached data immediately
134
- // 2. In parallel, refresh the cache in the background
135
- // 3. The client gets a fast response with slightly stale data
136
- // 4. The next client gets fresh data
137
- });
138
- });
@@ -1,113 +0,0 @@
1
- # Testing Cloudflare Workers' `waitUntil` API
2
-
3
- ## Background
4
-
5
- Cloudflare Workers have a special `waitUntil` API that allows background tasks to continue running after a response has been sent. This is particularly useful for implementing caching patterns like "stale-while-revalidate" where:
6
-
7
- 1. We return cached data to the client immediately
8
- 2. We refresh stale data in the background
9
- 3. The next client gets the fresh data
10
-
11
- ## The Challenge with Testing
12
-
13
- Testing `waitUntil` behavior can be tricky because:
14
-
15
- 1. In real Cloudflare Workers, the main handler function completes and returns a response, *then* the background tasks run.
16
- 2. In a test environment, we need to simulate this behavior correctly.
17
- 3. Most test implementations incorrectly execute background tasks immediately.
18
-
19
- ## Common Testing Mistake
20
-
21
- The most common mistake when testing `waitUntil` is to use an Immediately Invoked Function Expression (IIFE) that gets executed before `waitUntil` can store it:
22
-
23
- ```javascript
24
- // PROBLEMATIC: This will execute the fetch immediately, before waitUntil gets it
25
- ctx.waitUntil((async () => {
26
- const freshData = await fetch('https://api.example.com/data');
27
- return freshData;
28
- })());
29
- ```
30
-
31
- JavaScript evaluates function arguments before passing them to functions, so the IIFE executes immediately, and only its *resulting Promise* is passed to `waitUntil`.
32
-
33
- ## Correct Testing Approach
34
-
35
- The correct way to test `waitUntil` is to simulate the Cloudflare Worker runtime:
36
-
37
- ```javascript
38
- function simulateCloudflareWorker(handler) {
39
- const bgTasks = [];
40
- const executionCtx = {
41
- waitUntil: (promise) => {
42
- bgTasks.push(promise);
43
- }
44
- };
45
-
46
- return async () => {
47
- // Run the main handler first to completion
48
- const response = await handler(executionCtx);
49
-
50
- // Then run background tasks
51
- for (const task of bgTasks) {
52
- await task;
53
- }
54
-
55
- return response;
56
- };
57
- }
58
- ```
59
-
60
- This approach:
61
- 1. Collects promises passed to `waitUntil` without executing them
62
- 2. Completes the main handler function first
63
- 3. Only then executes the background tasks
64
-
65
- ## Test Files in This Directory
66
-
67
- 1. `waituntil-demo.test.js` - A conceptual explanation of how `waitUntil` works
68
- 2. `cloudflare-waituntil.test.js` - A test that correctly simulates the Cloudflare Worker runtime
69
-
70
- ## Best Practices for Testing `waitUntil`
71
-
72
- 1. **Use a task queue:** Collect tasks to be executed later, don't execute them immediately.
73
- 2. **Complete the main handler first:** Make sure the main handler completes before running background tasks.
74
- 3. **Avoid relying on mock implementations:** Most mock implementations don't correctly simulate the timing behavior.
75
- 4. **Test the end state:** Verify that background tasks eventually complete, not when they run.
76
-
77
- ## Implementing the Pattern in Your Code
78
-
79
- When implementing the stale-while-revalidate pattern:
80
-
81
- ```javascript
82
- async function getDataWithBackgroundRefresh(c) {
83
- // Get data from cache
84
- const cached = await c.db.getData(key);
85
-
86
- if (cached) {
87
- // Check if cache is stale
88
- const isStale = Date.now() - cached.timestamp > FIVE_MINUTES;
89
-
90
- if (isStale) {
91
- // Queue background refresh
92
- c.executionCtx.waitUntil((async () => {
93
- try {
94
- const fresh = await fetch('https://upstream.api/data');
95
- await c.db.saveData(fresh);
96
- } catch (err) {
97
- console.error('Background refresh failed:', err);
98
- }
99
- })());
100
- }
101
-
102
- // Return cached data immediately
103
- return cached.data;
104
- }
105
-
106
- // If not in cache, fetch directly
107
- const data = await fetch('https://upstream.api/data');
108
- await c.db.saveData(data);
109
- return data;
110
- }
111
- ```
112
-
113
- This pattern ensures users get a fast response while keeping your data fresh.