Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add next-dev internal-package #486

Merged
merged 21 commits into from
Nov 3, 2023
Merged
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2bbc432
add dev-bindings internal-package
dario-piotrowicz Oct 5, 2023
e7a5cbd
refactor dev-bindings
dario-piotrowicz Oct 6, 2023
76e2886
add Request, Response and Headers patching to dev-bindings (UNTESTED)
dario-piotrowicz Oct 7, 2023
b2cf93f
run prettier:fix
dario-piotrowicz Oct 9, 2023
815e4b7
make DOs work with local registry
dario-piotrowicz Oct 10, 2023
da950fb
add DO support via local registry
dario-piotrowicz Oct 10, 2023
557e863
rename dev-bindings to next-dev + start cleanup
dario-piotrowicz Oct 11, 2023
49d348e
update READMEs with next-dev
dario-piotrowicz Oct 12, 2023
bb55db1
pass bindings to the entrypoint worker
dario-piotrowicz Oct 12, 2023
eddf08c
avoid processing bindings that are not initialized
dario-piotrowicz Oct 12, 2023
a56d309
run prettier:fix
dario-piotrowicz Oct 12, 2023
4190a0c
update lock file
dario-piotrowicz Oct 12, 2023
eb693d0
bump turbo
dario-piotrowicz Oct 12, 2023
b78bdc3
add support for service bindings
dario-piotrowicz Oct 13, 2023
a999013
add error handling for disconnected service
dario-piotrowicz Oct 13, 2023
86a4db0
rename do.ts to durableObjects.ts
dario-piotrowicz Oct 13, 2023
e7b0c43
add comments in service.ts file
dario-piotrowicz Oct 13, 2023
63d7d6c
add comment for services option
dario-piotrowicz Oct 13, 2023
1768e9c
add text bindings support
dario-piotrowicz Oct 16, 2023
4339bbc
Apply suggestions from code review
dario-piotrowicz Nov 2, 2023
f4d6fc7
use symbol instead of 'BINDINGS_PROXY_SET' string (to avoid potential…
dario-piotrowicz Nov 2, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
make DOs work with local registry
dario-piotrowicz committed Oct 23, 2023
commit 815e4b7d8888a51af49644bb5f93c9dbe98aeb98
3 changes: 2 additions & 1 deletion internal-packages/dev-bindings/package.json
Original file line number Diff line number Diff line change
@@ -20,7 +20,8 @@
"devBindingsOptions.ts"
],
"dependencies": {
"miniflare": "^3.20231002.0"
"miniflare": "^3.20231002.0",
"node-fetch": "^3.3.2"
},
"devDependencies": {
"@cloudflare/workers-types": "4.20231002.0",
189 changes: 189 additions & 0 deletions internal-packages/dev-bindings/src/do.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import fetch from 'node-fetch';
import type { DevBindingsOptions } from './index';
import type { WorkerOptions } from 'miniflare';
// import assert from "node:assert";

export type WorkerRegistry = Record<string, WorkerDefinition>;

type WorkerDefinition = {
port: number | undefined;
protocol: 'http' | 'https' | undefined;
host: string | undefined;
mode: 'local' | 'remote';
headers?: Record<string, string>;
durableObjects: { name: string; className: string }[];
durableObjectsHost?: string;
durableObjectsPort?: number;
};

const DEV_REGISTRY_PORT = '6284';
const DEV_REGISTRY_HOST = `http://localhost:${DEV_REGISTRY_PORT}`;

// This worker proxies all external Durable Objects to the Wrangler session
// where they're defined, and receives all requests from other Wrangler sessions
// for this session's Durable Objects. Note the original request URL may contain
// non-standard protocols, so we store it in a header to restore later.
const EXTERNAL_DURABLE_OBJECTS_WORKER_NAME =
'__WRANGLER_EXTERNAL_DURABLE_OBJECTS_WORKER';
// noinspection HttpUrlsUsage
const EXTERNAL_DURABLE_OBJECTS_WORKER_SCRIPT = `
const HEADER_URL = "X-Miniflare-Durable-Object-URL";
const HEADER_NAME = "X-Miniflare-Durable-Object-Name";
const HEADER_ID = "X-Miniflare-Durable-Object-Id";

function createClass({ className, proxyUrl }) {
return class {
constructor(state) {
this.id = state.id.toString();
}
fetch(request) {
if (proxyUrl === undefined) {
return new Response(\`[wrangler] Couldn't find \\\`wrangler dev\\\` session for class "\${className}" to proxy to\`, { status: 503 });
}
const proxyRequest = new Request(proxyUrl, request);
proxyRequest.headers.set(HEADER_URL, request.url);
proxyRequest.headers.set(HEADER_NAME, className);
proxyRequest.headers.set(HEADER_ID, this.id);
return fetch(proxyRequest);
}
}
}

export default {
async fetch(request, env) {
const originalUrl = request.headers.get(HEADER_URL);
const className = request.headers.get(HEADER_NAME);
const idString = request.headers.get(HEADER_ID);
if (originalUrl === null || className === null || idString === null) {
return new Response("[wrangler] Received Durable Object proxy request with missing headers", { status: 400 });
}
request = new Request(originalUrl, request);
request.headers.delete(HEADER_URL);
request.headers.delete(HEADER_NAME);
request.headers.delete(HEADER_ID);
const ns = env[className];
const id = ns.idFromString(idString);
const stub = ns.get(id);
return stub.fetch(request);
}
}
`;

export async function getRegisteredWorkers(): Promise<
WorkerRegistry | undefined
> {
try {
const response = await fetch(`${DEV_REGISTRY_HOST}/workers`);
return (await response.json()) as WorkerRegistry;
} catch (e) {
if (
!['ECONNRESET', 'ECONNREFUSED'].includes(
(e as unknown as { cause?: { code?: string } }).cause?.code || '___',
)
) {
return;
}
}

return;
}

export async function getDOWorkerOptions(
durableObjects: DevBindingsOptions['durableObjects'],
): Promise<WorkerOptions | undefined> {
if (!durableObjects || Object.keys(durableObjects).length === 0) {
return;
}

const registeredWorkers = await getRegisteredWorkers();

if (!registeredWorkers) {
return;
}

const registeredWorkersWithDOs: WorkerRegistry[string][] = [];

for (const workerName of Object.keys(registeredWorkers ?? {})) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const worker = registeredWorkers![workerName]!;
const containsUsedDO = !!worker.durableObjects.find(
durableObject => !!durableObjects?.[durableObject.name],
);
if (containsUsedDO) {
registeredWorkersWithDOs.push(worker);
}
}

// TODO: the following is just copy pasted we need to actually create this
// (as it is done here: https://github.com/cloudflare/workers-sdk/blob/3077016f6112754585c05b7952e456be44b9d8cd/packages/wrangler/src/dev/miniflare.ts#L312-L330)
const externalObjects = [
{
className: 'do_worker_DurableObjectClass',
scriptName: '__WRANGLER_EXTERNAL_DURABLE_OBJECTS_WORKER',
unsafeUniqueKey: 'do_worker-DurableObjectClass',
},
];

// Setup Durable Object bindings and proxy worker
const externalDurableObjectWorker: WorkerOptions = {
name: EXTERNAL_DURABLE_OBJECTS_WORKER_NAME,
// Use this worker instead of the user worker if the pathname is
// `/${EXTERNAL_DURABLE_OBJECTS_WORKER_NAME}`
routes: [`*/${EXTERNAL_DURABLE_OBJECTS_WORKER_NAME}`],
// Use in-memory storage for the stub object classes *declared* by this
// script. They don't need to persist anything, and would end up using the
// incorrect unsafe unique key.
unsafeEphemeralDurableObjects: true,
modules: true,
script:
EXTERNAL_DURABLE_OBJECTS_WORKER_SCRIPT +
// Add stub object classes that proxy requests to the correct session
externalObjects
.map(({ className }) => {
// assert(scriptName !== undefined);

// // const identifier = getIdentifier(`${scriptName}_${className}`);
// // const classNameJson = JSON.stringify(className);

// debugger;
// return `export const do_worker_DurableObjectClass = () => {};`
// // return `export const ${identifier} = createClass({ className: ${classNameJson} });`;
// assert(scriptName !== undefined);
// const targetHasClass = durableObjects.some(
// ({ className }) => className === className
// );

// const identifier = getIdentifier(`${scriptName}_${className}`);
const classNameJson = JSON.stringify(className);
// if (
// target?.host === undefined ||
// target.port === undefined ||
// !targetHasClass
// ) {
// // If we couldn't find the target or the class, create a stub object
// // that just returns `503 Service Unavailable` responses.
// return `export const ${identifier} = createClass({ className: ${classNameJson} });`;
// } else {
// Otherwise, create a stub object that proxies request to the
// target session at `${hostname}:${port}`.

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const targetHost = registeredWorkersWithDOs[0]?.host!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const targetPort = registeredWorkersWithDOs[0]?.port!;
const proxyUrl = `http://${targetHost}:${targetPort}/${EXTERNAL_DURABLE_OBJECTS_WORKER_NAME}`;
const proxyUrlJson = JSON.stringify(proxyUrl);
// debugger;
return `export const ${className} = createClass({ className: ${classNameJson}, proxyUrl: ${proxyUrlJson} });`;
// }
})
.join('\n'),
};

return externalDurableObjectWorker;
}

// const IDENTIFIER_UNSAFE_REGEXP = /[^a-zA-Z0-9_$]/g;
// function getIdentifier(name: string) {
// return name.replace(IDENTIFIER_UNSAFE_REGEXP, "_");
// }
51 changes: 47 additions & 4 deletions internal-packages/dev-bindings/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,66 @@
import type { WorkerOptions } from 'miniflare';
import { Miniflare, Request, Response, Headers } from 'miniflare';
import { getDOWorkerOptions } from './do';

export async function setupDevBindings(options: DevBindingsOptions) {
const mf = instantiateMiniflare(options);
const mf = await instantiateMiniflare(options);

const bindings = await collectBindings(mf, options);

monkeyPatchVmModule(bindings);
}

function instantiateMiniflare(options: DevBindingsOptions) {
async function instantiateMiniflare(options: DevBindingsOptions) {
const workerOptions = await getDOWorkerOptions(options.durableObjects);

const workers: WorkerOptions[] = [
{
bindings: {},
compatibilityDate: '2023-10-10',
compatibilityFlags: [],
d1Databases: {},
dataBlobBindings: {},
durableObjects: {
MY_DO: {
className: 'do_worker_DurableObjectClass',
scriptName: '__WRANGLER_EXTERNAL_DURABLE_OBJECTS_WORKER',
unsafeUniqueKey: 'do_worker-DurableObjectClass',
},
},
kvNamespaces: {},
modules: true,
script: `export default {
async fetch(request, env) {
const doId = env.MY_DO.idFromName('my-do-name');

const doObj = env.MY_DO.get(doId);

const resp = await doObj.fetch(request.url);

const txt = await resp.text();

return new Response(\`Response from DO: \n\n\n\${txt}\`);
}
}`,
name: '',
queueConsumers: {},
queueProducers: {},
r2Buckets: {},
serviceBindings: {},
textBlobBindings: {},
wasmBindings: {},
},
...(workerOptions ? [workerOptions] : []),
];

// we let the user define where to persist the data, we default back
// to .wrangler/state/v3 which is the currently used wrangler path
// (this is so that when they switch to wrangler pages dev they can
// still interact with the same data)
const persist = options?.persist ?? '.wrangler/state/v3';

const mf = new Miniflare({
modules: true,
script: '',
workers,
...(persist === false
? {
// the user specifically requested no data persistence
130 changes: 129 additions & 1 deletion package-lock.json