Skip to content

Commit 1a6c6d9

Browse files
committed
Add function overload to startAndWaitForPorts
and document it better
1 parent 359dd1f commit 1a6c6d9

5 files changed

Lines changed: 126 additions & 57 deletions

File tree

.changeset/orange-keys-chew.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@cloudflare/containers': patch
3+
---
4+
5+
add function overload to startAndWaitForPorts()
6+
7+
You can now use `startAndWaitForPorts({startOptions: {envVars: {FOO:"BAR"}}})` instead of `startAndWaitForPorts(undefined, {}, {envVars: {FOO:"BAR"}})`, although that is still supported.

README.md

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { Container, getRandom } from '@cloudflare/containers';
2424
export class MyContainer extends Container {
2525
// Configure default port for the container
2626
defaultPort = 8080;
27-
sleepAfter = "1m";
27+
sleepAfter = '1m';
2828
}
2929

3030
export default {
@@ -34,7 +34,7 @@ export default {
3434
// If you want to route requests to a specific container,
3535
// pass a unique container identifier to .get()
3636

37-
if (pathname.startsWith("/specific/")) {
37+
if (pathname.startsWith('/specific/')) {
3838
// In this case, each unique pathname will spawn a new container
3939
let id = env.MY_CONTAINER.idFromName(pathname);
4040
let stub = env.MY_CONTAINER.get(id);
@@ -54,9 +54,9 @@ export default {
5454

5555
## API Reference
5656

57-
### Container Class
57+
### `Container` Class
5858

59-
The main class that extends a container-enbled Durable Object to provide additional container-specific functionality.
59+
The `Container` class that extends a container-enbled Durable Object to provide additional container-specific functionality.
6060

6161
#### Properties
6262

@@ -83,6 +83,7 @@ constructor(ctx: any, env: Env, options?: {
8383
#### Methods
8484

8585
##### Lifecycle Methods
86+
8687
All lifecycle methods can be implemented as async if needed.
8788

8889
- `onStart()`: Called when container starts successfully - override to add custom behavior
@@ -97,16 +98,45 @@ If you don't stop the container here, the activity tracker will be renewed, and
9798

9899
- `fetch(request)`: Default handler to forward HTTP requests to the container. Can be overridden.
99100
- `containerFetch(...)`: Sends an HTTP request to the container. Supports both standard fetch API signatures:
101+
100102
- `containerFetch(request, port?)`: Traditional signature with Request object
101103
- `containerFetch(url, init?, port?)`: Standard fetch-like signature with URL string/object and RequestInit options
102-
Either port parameter or defaultPort must be specified.
103-
When you call any of the fetch functions, the activity will be automatically renewed, and if the container will be started if not already running.
104-
**Do not use 'containerFetch' when trying to send a Request object with a websocket, until [this issue is addressed](https://github.com/cloudflare/workerd/issues/2319).
105-
You can overcome this limitation by doing:
106-
`container.fetch(switchPort(request, port))`
104+
Either port parameter or defaultPort must be specified.
105+
When you call any of the fetch functions, the activity will be automatically renewed, and if the container will be started if not already running.
106+
\*\*Do not use 'containerFetch' when trying to send a Request object with a websocket, until [this issue is addressed](https://github.com/cloudflare/workerd/issues/2319).
107+
You can overcome this limitation by doing:
108+
`container.fetch(switchPort(request, port))`
109+
110+
- `startAndWaitForPorts(args: StartAndWaitForPortsOptions): Promise<void>`
111+
112+
Starts the container and then waits for specified ports to be ready. Prioritises `ports` passed in to the function, then `requiredPorts` if set, then `defaultPort`.
113+
114+
```typescript
115+
export interface StartAndWaitForPortsOptions {
116+
startOptions?: {
117+
/** Environment variables to pass to the container */
118+
envVars?: Record<string, string>;
119+
/** Custom entrypoint to override container default */
120+
entrypoint?: string[];
121+
/** Whether to enable internet access for the container */
122+
enableInternet?: boolean;
123+
};
124+
/** Ports to check */
125+
ports?: number | number[];
126+
cancellationOptions?: {
127+
/** Abort signal to cancel start and port checking */
128+
abort?: AbortSignal;
129+
/** Max time to wait for container to start, in milliseconds */
130+
instanceGetTimeoutMS?: number;
131+
/** Max time to wait for ports to be ready, in milliseconds */
132+
portReadyTimeoutMS?: number;
133+
/** Polling interval for checking container has started or ports are ready, in milliseconds */
134+
waitInterval?: number;
135+
};
136+
}
137+
```
107138

108139
- `start()`: Starts the container if it's not running and sets up monitoring, without waiting for any ports to be ready.
109-
- `startAndWaitForPorts(ports?, maxTries?)`: Starts the container using `start()` and then waits for specified ports to be ready. If no ports are specified, uses `requiredPorts` or `defaultPort`. If no ports can be determined, just starts the container without port checks.
110140
- `stop(signal = SIGTERM)`: Sends the specified signal to the container.
111141
- `destroy()`: Forcefully destroys the container.
112142
- `getState()`: Get the current container state.
@@ -131,7 +161,7 @@ export class MyContainer extends Container {
131161

132162
// Set how long the container should stay active without requests
133163
// Supported formats: "10m" (minutes), "30s" (seconds), "1h" (hours), or a number (seconds)
134-
sleepAfter = "10m";
164+
sleepAfter = '10m';
135165

136166
// Lifecycle method called when container starts
137167
override onStart(): void {
@@ -153,9 +183,7 @@ export class MyContainer extends Container {
153183

154184
// Lifecycle method when the container class considers the activity to be expired
155185
override onActivityExpired() {
156-
console.log(
157-
'Container activity expired'
158-
);
186+
console.log('Container activity expired');
159187
await this.destroy();
160188
}
161189

@@ -170,7 +198,6 @@ export class MyContainer extends Container {
170198

171199
// Handle incoming requests
172200
async fetch(request: Request): Promise<Response> {
173-
174201
// Default implementation forwards requests to the container
175202
// This will automatically renew the activity timeout
176203
return await this.containerFetch(request);
@@ -205,13 +232,13 @@ export class ConfiguredContainer extends Container {
205232
defaultPort = 9000;
206233

207234
// Set the timeout for sleeping the container after inactivity
208-
sleepAfter = "2h";
235+
sleepAfter = '2h';
209236

210237
// Environment variables to pass to the container
211238
envVars = {
212239
NODE_ENV: 'production',
213240
LOG_LEVEL: 'info',
214-
APP_PORT: '9000'
241+
APP_PORT: '9000',
215242
};
216243

217244
// Custom entrypoint to run in the container
@@ -249,18 +276,16 @@ export class MultiPortContainer extends Container {
249276
if (url.pathname.startsWith('/api')) {
250277
// API server runs on port 3000
251278
return await this.containerFetch(request, 3000);
252-
}
253-
else if (url.pathname.startsWith('/admin')) {
279+
} else if (url.pathname.startsWith('/admin')) {
254280
// Admin interface runs on port 8080
255281
return await this.containerFetch(request, 8080);
256-
}
257-
else {
282+
} else {
258283
// Public website runs on port 80
259284
return await this.containerFetch(request, 80);
260285
}
261286
} catch (error) {
262287
return new Response(`Error: ${error instanceof Error ? error.message : String(error)}`, {
263-
status: 500
288+
status: 500,
264289
});
265290
}
266291
}
@@ -283,21 +308,22 @@ export class FetchStyleContainer extends Container {
283308
const response = await this.containerFetch('/api/data', {
284309
method: 'POST',
285310
headers: {
286-
'Content-Type': 'application/json'
311+
'Content-Type': 'application/json',
287312
},
288-
body: JSON.stringify({ query: 'example' })
313+
body: JSON.stringify({ query: 'example' }),
289314
});
290315

291316
// You can also specify a port with this syntax
292-
const adminResponse = await this.containerFetch('https://example.com/admin',
317+
const adminResponse = await this.containerFetch(
318+
'https://example.com/admin',
293319
{ method: 'GET' },
294-
3000 // port
320+
3000 // port
295321
);
296322

297323
return response;
298324
} catch (error) {
299325
return new Response(`Error: ${error instanceof Error ? error.message : String(error)}`, {
300-
status: 500
326+
status: 500,
301327
});
302328
}
303329
}
@@ -316,7 +342,7 @@ export class TimeoutContainer extends Container {
316342
defaultPort = 8080;
317343

318344
// Set timeout to 30 minutes of inactivity
319-
sleepAfter = "30m"; // Supports "30s", "5m", "1h" formats, or a number in seconds
345+
sleepAfter = '30m'; // Supports "30s", "5m", "1h" formats, or a number in seconds
320346

321347
// Custom method that will extend the container's lifetime
322348
async performBackgroundTask(data: any): Promise<void> {
@@ -337,11 +363,14 @@ export class TimeoutContainer extends Container {
337363
if (url.pathname === '/task') {
338364
await this.performBackgroundTask();
339365

340-
return new Response(JSON.stringify({
341-
success: true,
342-
message: 'Background task executed',
343-
nextStop: `Container will shut down after ${this.sleepAfter} of inactivity`
344-
}), { headers: { 'Content-Type': 'application/json' } });
366+
return new Response(
367+
JSON.stringify({
368+
success: true,
369+
message: 'Background task executed',
370+
nextStop: `Container will shut down after ${this.sleepAfter} of inactivity`,
371+
}),
372+
{ headers: { 'Content-Type': 'application/json' } }
373+
);
345374
}
346375

347376
// For all other requests, forward to the container
@@ -354,7 +383,7 @@ export class TimeoutContainer extends Container {
354383
### Using Load Balancing
355384

356385
This package includes a `getRandom` helper which routes requests to one of N instances.
357-
In the future, this will be automatically handled with smart by Cloudflare Containers
386+
In the future, this will be automatically handled with smart by Cloudflare Containers
358387
with autoscaling set to true, but is not yet implemented.
359388

360389
```typescript
@@ -382,7 +411,7 @@ export default {
382411
}
383412

384413
return new Response('Not found', { status: 404 });
385-
}
414+
},
386415
};
387416
```
388417

example-node/src/index.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,10 @@ export default {
3333

3434
if (pathname.startsWith('/startAndWaitForPorts')) {
3535
const containerInstance = getContainer(env.MY_CONTAINER, pathname);
36-
await containerInstance.startAndWaitForPorts(
37-
undefined,
38-
{},
39-
{
40-
envVars: { MESSAGE: 'hi from start and wait for ports' },
41-
}
42-
);
36+
await containerInstance.startAndWaitForPorts({
37+
startOptions: { envVars: { MESSAGE: 'hi from start and wait for ports' } },
38+
});
39+
4340
return containerInstance.fetch(request);
4441
}
4542

src/lib/container.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type {
77
ScheduleSQL,
88
State,
99
WaitOptions,
10+
CancellationOptions,
11+
StartAndWaitForPortsOptions,
1012
} from '../types';
1113
import { generateId, parseTimeExpression } from './helpers';
1214
import { DurableObject } from 'cloudflare:workers';
@@ -379,16 +381,38 @@ export class Container<Env = unknown> extends DurableObject<Env> {
379381
* @param startOptions Override configuration on a per instance for env vars, entrypoint command and internet access
380382
* @throws Error if port checks fail after maxTries attempts
381383
*/
384+
public async startAndWaitForPorts(args: StartAndWaitForPortsOptions): Promise<void>;
382385
public async startAndWaitForPorts(
383386
ports?: number | number[],
384-
cancellationOptions?: {
385-
abort?: AbortSignal;
386-
instanceGetTimeoutMS?: number;
387-
portReadyTimeoutMS?: number;
388-
waitInterval?: number;
389-
},
387+
cancellationOptions?: CancellationOptions,
388+
startOptions?: ContainerStartConfigOptions
389+
): Promise<void>;
390+
public async startAndWaitForPorts(
391+
portsOrArgs?: number | number[] | StartAndWaitForPortsOptions,
392+
cancellationOptions?: CancellationOptions,
393+
startOptions?: ContainerStartConfigOptions
394+
): Promise<void>;
395+
public async startAndWaitForPorts(
396+
portsOrArgs?: number | number[] | StartAndWaitForPortsOptions,
397+
cancellationOptions?: CancellationOptions,
390398
startOptions?: ContainerStartConfigOptions
391399
): Promise<void> {
400+
// Parse arguments to handle different overload signatures
401+
let ports: number | number[] | undefined;
402+
let resolvedCancellationOptions: CancellationOptions | undefined = {};
403+
let resolvedStartOptions: ContainerStartConfigOptions | undefined = {};
404+
405+
if (typeof portsOrArgs === 'object' && portsOrArgs !== null && !Array.isArray(portsOrArgs)) {
406+
// Object-based overload: { startOptions?, ports?, cancellationOptions? }
407+
ports = portsOrArgs.ports;
408+
resolvedCancellationOptions = portsOrArgs.cancellationOptions;
409+
resolvedStartOptions = portsOrArgs.startOptions;
410+
} else {
411+
ports = portsOrArgs;
412+
resolvedCancellationOptions = cancellationOptions;
413+
resolvedStartOptions = startOptions;
414+
}
415+
392416
// Determine which ports to check
393417
const portsToCheck = await this.getPortsToCheck(ports);
394418

@@ -409,15 +433,15 @@ export class Container<Env = unknown> extends DurableObject<Env> {
409433
await this.syncPendingStoppedEvents();
410434

411435
// Prepare to start the container
412-
cancellationOptions ??= {};
413-
let containerGetRetries = cancellationOptions.instanceGetTimeoutMS
414-
? Math.ceil(cancellationOptions.instanceGetTimeoutMS / INSTANCE_POLL_INTERVAL_MS)
436+
resolvedCancellationOptions ??= {};
437+
let containerGetRetries = resolvedCancellationOptions.instanceGetTimeoutMS
438+
? Math.ceil(resolvedCancellationOptions.instanceGetTimeoutMS / INSTANCE_POLL_INTERVAL_MS)
415439
: TRIES_TO_GET_CONTAINER;
416440

417441
const waitOptions = {
418-
abort: cancellationOptions.abort,
442+
abort: resolvedCancellationOptions.abort,
419443
retries: containerGetRetries,
420-
waitInterval: cancellationOptions.waitInterval ?? INSTANCE_POLL_INTERVAL_MS,
444+
waitInterval: resolvedCancellationOptions.waitInterval ?? INSTANCE_POLL_INTERVAL_MS,
421445
portToCheck: portsToCheck[0],
422446
};
423447

@@ -428,11 +452,11 @@ export class Container<Env = unknown> extends DurableObject<Env> {
428452
});
429453

430454
// Start the container if it's not running
431-
const triesUsed = await this.startContainerIfNotRunning(waitOptions, startOptions);
455+
const triesUsed = await this.startContainerIfNotRunning(waitOptions, resolvedStartOptions);
432456

433457
// Check each port
434-
let totalPortReadyTries = cancellationOptions.portReadyTimeoutMS
435-
? Math.ceil(cancellationOptions.portReadyTimeoutMS / INSTANCE_POLL_INTERVAL_MS)
458+
let totalPortReadyTries = resolvedCancellationOptions.portReadyTimeoutMS
459+
? Math.ceil(resolvedCancellationOptions.portReadyTimeoutMS / INSTANCE_POLL_INTERVAL_MS)
436460
: TRIES_TO_GET_PORTS;
437461
const triesLeft = totalPortReadyTries - triesUsed;
438462

src/types/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export interface ContainerOptions {
4343
enableInternet?: boolean;
4444
}
4545

46-
4746
/**
4847
* Function to handle container events
4948
*/
@@ -61,6 +60,19 @@ export interface ContainerStartConfigOptions {
6160
enableInternet?: boolean;
6261
}
6362

63+
export interface StartAndWaitForPortsOptions {
64+
startOptions?: ContainerStartConfigOptions;
65+
ports?: number | number[];
66+
cancellationOptions?: CancellationOptions;
67+
}
68+
69+
export interface CancellationOptions {
70+
abort?: AbortSignal;
71+
instanceGetTimeoutMS?: number;
72+
portReadyTimeoutMS?: number;
73+
waitInterval?: number;
74+
}
75+
6476
export interface WaitOptions {
6577
abort?: AbortSignal;
6678
retries: number;

0 commit comments

Comments
 (0)