-
Notifications
You must be signed in to change notification settings - Fork 1.2k
[Stream] Add Stream binding local mode #13030
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
Changes from all commits
5183f9d
1eb2f9f
1d81d81
2fb08df
daebf5e
78c47db
43fcb47
f3ea61f
c56448e
e71a122
b2d629d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| --- | ||
| "miniflare": minor | ||
| "wrangler": minor | ||
| --- | ||
|
|
||
| Add local mode support for Stream bindings | ||
|
|
||
| Miniflare and `wrangler dev` now support using [Cloudflare Stream](https://developers.cloudflare.com/stream/) bindings locally. | ||
|
|
||
| Supported operations: | ||
|
|
||
| - `upload()` — upload video via URL | ||
| - `video(id).details()`, `.update()`, `.delete()`, `.generateToken()` | ||
| - `videos.list()` | ||
| - `captions.generate()`, `.list()`, `.delete()` | ||
| - `downloads.generate()`, `.get()`, `.delete()` | ||
| - `watermarks.generate()`, `.list()`, `.get()`, `.delete()` | ||
|
|
||
| The following are not yet supported in local mode and will throw: | ||
|
|
||
| - `createDirectUpload()` | ||
| - Caption upload via `File` | ||
| - Watermark generation via `File` | ||
|
|
||
| Data is persisted across restarts by default. You must set `streamPersist: false` in Miniflare options to disable persistence. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,185 @@ | ||||||
| import fs from "node:fs/promises"; | ||||||
| import BINDING_SCRIPT from "worker:stream/binding"; | ||||||
| import OBJECT_SCRIPT from "worker:stream/object"; | ||||||
| import { z } from "zod"; | ||||||
| import { SharedBindings } from "../../workers"; | ||||||
| import { | ||||||
| getMiniflareObjectBindings, | ||||||
| getPersistPath, | ||||||
| getUserBindingServiceName, | ||||||
| PersistenceSchema, | ||||||
| Plugin, | ||||||
| ProxyNodeBinding, | ||||||
| remoteProxyClientWorker, | ||||||
| RemoteProxyConnectionString, | ||||||
| } from "../shared"; | ||||||
| import type { Service } from "../../runtime"; | ||||||
|
|
||||||
| const StreamSchema = z.object({ | ||||||
| binding: z.string(), | ||||||
| remoteProxyConnectionString: z | ||||||
| .custom<RemoteProxyConnectionString>() | ||||||
| .optional(), | ||||||
| }); | ||||||
|
|
||||||
| export const StreamOptionsSchema = z.object({ | ||||||
| stream: StreamSchema.optional(), | ||||||
| }); | ||||||
|
|
||||||
| export const StreamSharedOptionsSchema = z.object({ | ||||||
| streamPersist: PersistenceSchema, | ||||||
| }); | ||||||
|
|
||||||
| export const STREAM_PLUGIN_NAME = "stream"; | ||||||
| const STREAM_STORAGE_SERVICE_NAME = `${STREAM_PLUGIN_NAME}:storage`; | ||||||
| const STREAM_OBJECT_SERVICE_NAME = `${STREAM_PLUGIN_NAME}:object`; | ||||||
| export const STREAM_OBJECT_CLASS_NAME = "StreamObject"; | ||||||
|
|
||||||
| export const STREAM_COMPAT_DATE = "2026-03-23"; | ||||||
|
|
||||||
| export const STREAM_PLUGIN: Plugin< | ||||||
| typeof StreamOptionsSchema, | ||||||
| typeof StreamSharedOptionsSchema | ||||||
| > = { | ||||||
| options: StreamOptionsSchema, | ||||||
| sharedOptions: StreamSharedOptionsSchema, | ||||||
| async getBindings(options) { | ||||||
| if (!options.stream) { | ||||||
| return []; | ||||||
| } | ||||||
|
|
||||||
| return [ | ||||||
| { | ||||||
| name: options.stream.binding, | ||||||
| service: { | ||||||
| name: getUserBindingServiceName( | ||||||
| STREAM_PLUGIN_NAME, | ||||||
| options.stream.binding, | ||||||
| options.stream.remoteProxyConnectionString | ||||||
| ), | ||||||
| entrypoint: "StreamBinding", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @natewong1313 - I think there is a bug here. When in remote mode the binding proxy doesn't have a named entry point. It just has a default one. So this should be:
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was already making the same change for Flagship, so I made the same patch for this as well. Feel free to discard it if it doesn't look right!
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @roerohan Thank you 🙏 |
||||||
| }, | ||||||
| }, | ||||||
| ]; | ||||||
| }, | ||||||
| getNodeBindings(options: z.infer<typeof StreamOptionsSchema>) { | ||||||
| if (!options.stream) { | ||||||
| return {}; | ||||||
| } | ||||||
| return { | ||||||
| [options.stream.binding]: new ProxyNodeBinding(), | ||||||
| }; | ||||||
| }, | ||||||
| async getServices({ | ||||||
| options, | ||||||
| sharedOptions, | ||||||
| tmpPath, | ||||||
| defaultPersistRoot, | ||||||
| unsafeStickyBlobs, | ||||||
| }) { | ||||||
| if (!options.stream) { | ||||||
| return []; | ||||||
| } | ||||||
|
|
||||||
| const serviceName = getUserBindingServiceName( | ||||||
| STREAM_PLUGIN_NAME, | ||||||
| options.stream.binding, | ||||||
| options.stream.remoteProxyConnectionString | ||||||
| ); | ||||||
|
|
||||||
| if (options.stream.remoteProxyConnectionString) { | ||||||
| return [ | ||||||
| { | ||||||
| name: serviceName, | ||||||
| worker: remoteProxyClientWorker( | ||||||
| options.stream.remoteProxyConnectionString, | ||||||
| options.stream.binding | ||||||
| ), | ||||||
| }, | ||||||
| ]; | ||||||
| } | ||||||
|
|
||||||
| const persistPath = getPersistPath( | ||||||
| STREAM_PLUGIN_NAME, | ||||||
| tmpPath, | ||||||
| defaultPersistRoot, | ||||||
| sharedOptions.streamPersist | ||||||
| ); | ||||||
| await fs.mkdir(persistPath, { recursive: true }); | ||||||
|
|
||||||
| // Disk storage for blobs and SQL | ||||||
| const storageService = { | ||||||
| name: STREAM_STORAGE_SERVICE_NAME, | ||||||
| disk: { path: persistPath, writable: true }, | ||||||
| } satisfies Service; | ||||||
|
|
||||||
| // StreamObject | ||||||
| const objectService = { | ||||||
| name: STREAM_OBJECT_SERVICE_NAME, | ||||||
| worker: { | ||||||
| compatibilityDate: STREAM_COMPAT_DATE, | ||||||
| compatibilityFlags: ["nodejs_compat", "experimental"], | ||||||
| modules: [ | ||||||
| { | ||||||
| name: "object.worker.js", | ||||||
| esModule: OBJECT_SCRIPT(), | ||||||
| }, | ||||||
| ], | ||||||
| durableObjectNamespaces: [ | ||||||
| { | ||||||
| className: STREAM_OBJECT_CLASS_NAME, | ||||||
| uniqueKey: `miniflare-${STREAM_OBJECT_CLASS_NAME}`, | ||||||
| enableSql: true, | ||||||
| }, | ||||||
| ], | ||||||
| durableObjectStorage: { localDisk: STREAM_STORAGE_SERVICE_NAME }, | ||||||
| bindings: [ | ||||||
| { | ||||||
| name: SharedBindings.MAYBE_SERVICE_BLOBS, | ||||||
| service: { name: STREAM_STORAGE_SERVICE_NAME }, | ||||||
| }, | ||||||
| ...getMiniflareObjectBindings(unsafeStickyBlobs), | ||||||
| ], | ||||||
| // Allow the DO to send outbound HTTP requests (fetching watermark images) | ||||||
| globalOutbound: { name: "internet" }, | ||||||
| }, | ||||||
| } satisfies Service; | ||||||
|
|
||||||
| // Entrypoint with RPC | ||||||
| const bindingService = { | ||||||
| name: serviceName, | ||||||
| worker: { | ||||||
| compatibilityDate: STREAM_COMPAT_DATE, | ||||||
| compatibilityFlags: ["nodejs_compat", "experimental"], | ||||||
| modules: [ | ||||||
| { | ||||||
| name: "binding.worker.js", | ||||||
| esModule: BINDING_SCRIPT(), | ||||||
| }, | ||||||
| ], | ||||||
| bindings: [ | ||||||
| { | ||||||
| name: "store", | ||||||
| durableObjectNamespace: { | ||||||
| className: STREAM_OBJECT_CLASS_NAME, | ||||||
| serviceName: STREAM_OBJECT_SERVICE_NAME, | ||||||
| }, | ||||||
| }, | ||||||
| ], | ||||||
| // Allow the binding worker to send outbound HTTP requests | ||||||
| // (e.g. fetching video from URL in upload fn) | ||||||
| globalOutbound: { name: "internet" }, | ||||||
| }, | ||||||
| } satisfies Service; | ||||||
|
|
||||||
| return [storageService, objectService, bindingService]; | ||||||
| }, | ||||||
| getPersistPath({ streamPersist }, tmpPath) { | ||||||
| return getPersistPath( | ||||||
| STREAM_PLUGIN_NAME, | ||||||
| tmpPath, | ||||||
| undefined, | ||||||
| streamPersist | ||||||
| ); | ||||||
| }, | ||||||
| }; | ||||||
Uh oh!
There was an error while loading. Please reload this page.