diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b42984ce..b951d2ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,13 +12,14 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x] + suite: [node] # See supported Node.js release schedule at https://nodejs.org/en/about/previous-releases + node-version: [18.x, 20.x, 22.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: "npm" @@ -27,28 +28,205 @@ jobs: - run: npm run check - run: npm run lint - integration: - needs: test + + # Build a production tarball and use it to run the integration + build: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/previous-releases + node-version: [20.x] + + outputs: + tarball-name: ${{ steps.pack.outputs.tarball-name }} + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + - name: Build tarball + id: pack + run: | + echo "tarball-name=$(npm --loglevel error pack)" >> $GITHUB_OUTPUT + - uses: actions/upload-artifact@v4 + with: + name: package-tarball + path: ${{ steps.pack.outputs.tarball-name }} + + + integration-node: + needs: [test, build] + runs-on: ubuntu-latest + + env: + REPLICATE_API_TOKEN: ${{ secrets.REPLICATE_API_TOKEN }} + + strategy: + matrix: suite: [commonjs, esm, typescript] # See supported Node.js release schedule at https://nodejs.org/en/about/previous-releases + node-version: [18.x, 20.x] + fail-fast: false + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4.1.7 + with: + name: package-tarball + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + - run: | + npm --prefix integration/${{ matrix.suite }} install + npm --prefix integration/${{ matrix.suite }} install "./${{ needs.build.outputs.tarball-name }}" + npm --prefix integration/${{ matrix.suite }} test + + integration-browser: + needs: [test, build] + runs-on: ubuntu-latest + + env: + REPLICATE_API_TOKEN: ${{ secrets.REPLICATE_API_TOKEN }} + + strategy: + matrix: + suite: ["browser"] + browser: ["chromium", "firefox", "webkit"] + node-version: [20.x] + fail-fast: false + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4.1.7 + with: + name: package-tarball + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + - run: | + cd integration/${{ matrix.suite }} + npm install + npm install "../../${{ needs.build.outputs.tarball-name }}" + npm exec -- playwright install ${{ matrix.browser }} + npm exec -- playwright install-deps ${{ matrix.browser }} + npm exec -- playwright test --browser ${{ matrix.browser }} + + integration-edge: + needs: [test, build] + runs-on: ubuntu-latest env: REPLICATE_API_TOKEN: ${{ secrets.REPLICATE_API_TOKEN }} + strategy: + matrix: + suite: [cloudflare-worker] + node-version: [20.x] + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4.1.7 + with: + name: package-tarball - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: "npm" - # Build a production tarball and run the integration tests against it. - run: | - PKG_TARBALL=$(npm --loglevel error pack) - npm --prefix integration/${{ matrix.suite }} install "file:/./$PKG_TARBALL" + test "${{ matrix.suite }}" = "cloudflare-worker" && echo "REPLICATE_API_TOKEN=${{ secrets.REPLICATE_API_TOKEN }}" > integration/${{ matrix.suite }}/.dev.vars + npm --prefix integration/${{ matrix.suite }} install + npm --prefix integration/${{ matrix.suite }} install "./${{ needs.build.outputs.tarball-name }}" npm --prefix integration/${{ matrix.suite }} test + + integration-bun: + needs: [test, build] + runs-on: ubuntu-latest + + env: + REPLICATE_API_TOKEN: ${{ secrets.REPLICATE_API_TOKEN }} + + strategy: + matrix: + suite: [bun] + bun-version: [1.0.11] + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4.1.7 + with: + name: package-tarball + - name: Use Bun ${{ matrix.bun-version }} + uses: oven-sh/setup-bun@v1 + with: + bun-version: ${{ matrix.bun-version }} + - run: | + cd integration/${{ matrix.suite }} + bun uninstall replicate + bun install "file:../../${{ needs.build.outputs.tarball-name }}" + retries=3 + for ((i=0; i [!NOTE] +> The vendored implementation of `TextDecoderStream` requires +> the following patch to be applied to the output of bundlejs.com: +> +> ```diff +> constructor(label, options) { +> - this[decDecoder] = new TextDecoder(label, options); +> - this[decTransform] = new TransformStream(new TextDecodeTransformer(this[decDecoder])); +> + const decoder = new TextDecoder(label || "utf-8", options || {}); +> + this[decDecoder] = decoder; +> + this[decTransform] = new TransformStream(new TextDecodeTransformer(decoder)); +> } +> ``` diff --git a/README.md b/README.md index 222deffd..cf12865c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,17 @@ and everything else you can do with > For more information about how to build a web application > check out our ["Build a website with Next.js"](https://replicate.com/docs/get-started/nextjs) guide. +## Supported platforms + +- [Node.js](https://nodejs.org) >= 18 +- [Bun](https://bun.sh) >= 1.0 +- [Deno](https://deno.com) >= 1.28 + +You can also use this client library on most serverless platforms, including +[Cloudflare Workers](https://developers.cloudflare.com/workers/), +[Vercel functions](https://vercel.com/docs/functions), and +[AWS Lambda](https://aws.amazon.com/lambda/). + ## Installation Install it from npm: @@ -20,7 +31,7 @@ npm install replicate ## Usage -Create the client: +Import or require the package: ```js // CommonJS (default or using .cjs extension) @@ -30,9 +41,11 @@ const Replicate = require("replicate"); import Replicate from "replicate"; ``` -``` +Instantiate the client: + +```js const replicate = new Replicate({ - // get your token from https://replicate.com/account + // get your token from https://replicate.com/account/api-tokens auth: "my api token", // defaults to process.env.REPLICATE_API_TOKEN }); ``` @@ -44,10 +57,21 @@ const model = "stability-ai/stable-diffusion:27b93a2413e7f36cd83da926f3656280b29 const input = { prompt: "a 19th century portrait of a raccoon gentleman wearing a suit", }; -const output = await replicate.run(model, { input }); -// ['https://replicate.delivery/pbxt/GtQb3Sgve42ZZyVnt8xjquFk9EX5LP0fF68NTIWlgBMUpguQA/out-0.png'] +const [output] = await replicate.run(model, { input }); +// FileOutput('https://replicate.delivery/pbxt/GtQb3Sgve42ZZyVnt8xjquFk9EX5LP0fF68NTIWlgBMUpguQA/out-0.png') + +console.log(output.url()); // 'https://replicate.delivery/pbxt/GtQb3Sgve42ZZyVnt8xjquFk9EX5LP0fF68NTIWlgBMUpguQA/out-0.png' +console.log(output.blob()); // Blob ``` +> [!NOTE] +> A model that outputs file data returns a `FileOutput` object by default. This is an implementation +> of `ReadableStream` that returns the file contents. It has a `.blob()` method for accessing a +> `Blob` representation and a `.url()` method that will return the underlying data-source. +> +> We recommend accessing file data directly either as readable stream or via `.blob()` as the +> `.url()` property may not always return a HTTP URL in future. + You can also run a model in the background: ```js @@ -73,7 +97,9 @@ console.log(prediction.output); // ['https://replicate.delivery/pbxt/RoaxeXqhL0xaYyLm6w3bpGwF5RaNBjADukfFnMbhOyeoWBdhA/out-0.png'] ``` -To run a model that takes a file input, pass a URL to a publicly accessible file. Or, for smaller files (<10MB), you can pass the data directly. +To run a model that takes a file input you can pass either +a URL to a publicly accessible file on the Internet +or a handle to a file on your local device. ```js const fs = require("node:fs/promises"); @@ -85,12 +111,170 @@ const model = "nightmareai/real-esrgan:42fed1c4974146d4d2414e2be2c5277c7fcf05fcc const input = { image: await fs.readFile("path/to/image.png"), }; -const output = await replicate.run(model, { input }); -// ['https://replicate.delivery/mgxm/e7b0e122-9daa-410e-8cde-006c7308ff4d/output.png'] +const [output] = await replicate.run(model, { input }); +// FileOutput('https://replicate.delivery/mgxm/e7b0e122-9daa-410e-8cde-006c7308ff4d/output.png') +``` + +> [!NOTE] +> File handle inputs are automatically uploaded to Replicate. +> See [`replicate.files.create`](#replicatefilescreate) for more information. +> The maximum size for uploaded files is 100MiB. +> To run a model with a larger file as an input, +> upload the file to your own storage provider +> and pass a publicly accessible URL. + +## TypeScript usage + +This library exports TypeScript definitions. You can import them like this: + +```ts +import Replicate, { type Prediction } from 'replicate'; +``` + +Here's an example that uses the `Prediction` type with a custom `onProgress` callback: + +```ts +import Replicate, { type Prediction } from 'replicate'; + +const replicate = new Replicate(); +const model = "black-forest-labs/flux-schnell"; +const prompt = "a 19th century portrait of a raccoon gentleman wearing a suit"; +function onProgress(prediction: Prediction) { + console.log({ prediction }); +} + +const output = await replicate.run(model, { input: { prompt } }, onProgress) +console.log({ output }) +``` + +See the full list of exported types in [index.d.ts](./index.d.ts). + +### Webhooks + +Webhooks provide real-time updates about your prediction. Specify an endpoint when you create a prediction, and Replicate will send HTTP POST requests to that URL when the prediction is created, updated, and finished. + +It is possible to provide a URL to the predictions.create() function that will be requested by Replicate when the prediction status changes. This is an alternative to polling. + +To receive webhooks you’ll need a web server. The following example uses Hono, a web standards based server, but this pattern applies to most frameworks. + +
+ See example + +```js +import { serve } from '@hono/node-server'; +import { Hono } from 'hono'; + +const app = new Hono(); +app.get('/webhooks/replicate', async (c) => { + // Get the prediction from the request. + const prediction = await c.req.json(); + console.log(prediction); + //=> {"id": "xyz", "status": "successful", ... } + + // Acknowledge the webhook. + c.status(200); + c.json({ok: true}); +})); + +serve(app, (info) => { + console.log(`Listening on http://localhost:${info.port}`) + //=> Listening on http://localhost:3000 +}); +``` + +
+ +Create the prediction passing in the webhook URL to `webhook` and specify which events you want to receive in `webhook_events_filter` out of "start", "output", ”logs” and "completed": + +```js +const Replicate = require("replicate"); +const replicate = new Replicate(); + +const input = { + image: "https://replicate.delivery/pbxt/KWDkejqLfER3jrroDTUsSvBWFaHtapPxfg4xxZIqYmfh3zXm/Screenshot%202024-02-28%20at%2022.14.00.png", + denoising_strength: 0.5, + instant_id_strength: 0.8 +}; + +const callbackURL = `https://my.app/webhooks/replicate`; +await replicate.predictions.create({ + version: "19deaef633fd44776c82edf39fd60e95a7250b8ececf11a725229dc75a81f9ca", + input: input, + webhook: callbackURL, + webhook_events_filter: ["completed"], +}); + +// The server will now handle the event and log: +// => {"id": "xyz", "status": "successful", ... } ``` +## Verifying webhooks + +To prevent unauthorized requests, Replicate signs every webhook and its metadata with a unique key for each user or organization. You can use this signature to verify the webhook indeed comes from Replicate before you process it. + +This client includes a `validateWebhook` convenience function that you can use to validate webhooks. + +To validate webhooks: + +1. Check out the [webhooks guide](https://replicate.com/docs/webhooks) to get started. +1. [Retrieve your webhook signing secret](https://replicate.com/docs/webhooks#retrieving-the-webhook-signing-key) and store it in your enviroment. +1. Update your webhook handler to call `validateWebhook(request, secret)`, where `request` is an instance of a [web-standard `Request` object](https://developer.mozilla.org/en-US/docs/Web/API/object), and `secret` is the signing secret for your environment. + +Here's an example of how to validate webhooks using Next.js: + +```js +import { NextResponse } from 'next/server'; +import { validateWebhook } from 'replicate'; + +export async function POST(request) { + const secret = process.env.REPLICATE_WEBHOOK_SIGNING_SECRET; + + if (!secret) { + console.log("Skipping webhook validation. To validate webhooks, set REPLICATE_WEBHOOK_SIGNING_SECRET") + const body = await request.json(); + console.log(body); + return NextResponse.json({ detail: "Webhook received (but not validated)" }, { status: 200 }); + } + + const webhookIsValid = await validateWebhook(request.clone(), secret); + + if (!webhookIsValid) { + return NextResponse.json({ detail: "Webhook is invalid" }, { status: 401 }); + } + + // process validated webhook here... + console.log("Webhook is valid!"); + const body = await request.json(); + console.log(body); + + return NextResponse.json({ detail: "Webhook is valid" }, { status: 200 }); +} +``` + +If your environment doesn't support `Request` objects, you can pass the required information to `validateWebhook` directly: + +```js +const requestData = { + id: "123", // the `Webhook-Id` header + timestamp: "0123456", // the `Webhook-Timestamp` header + signature: "xyz", // the `Webhook-Signature` header + body: "{...}", // the request body as a string, ArrayBuffer or ReadableStream + secret: "shhh", // the webhook secret, obtained from the `replicate.webhooks.defaul.secret` endpoint +}; +const webhookIsValid = await validateWebhook(requestData); +``` + +> [!NOTE] +> The `validateWebhook` function uses the global `crypto` API available in most JavaScript runtimes. Node <= 18 does not provide this global so in this case you need to either call node with the `--no-experimental-global-webcrypto` or provide the `webcrypto` module manually. +> ```js +> const crypto = require("node:crypto").webcrypto; +> const webhookIsValid = await valdiateWebhook(requestData, crypto); +> ``` + ## TypeScript +The `Replicate` constructor and all `replicate.*` methods are fully typed. + Currently in order to support the module format used by `replicate` you'll need to set `esModuleInterop` to `true` in your tsconfig.json. ## API @@ -101,12 +285,15 @@ Currently in order to support the module format used by `replicate` you'll need const replicate = new Replicate(options); ``` -| name | type | description | -| ------------------- | -------- | --------------------------------------------------------------------------------- | -| `options.auth` | string | **Required**. API access token | -| `options.userAgent` | string | Identifier of your app. Defaults to `replicate-javascript/${packageJSON.version}` | -| `options.baseUrl` | string | Defaults to https://api.replicate.com/v1 | -| `options.fetch` | function | Fetch function to use. Defaults to `globalThis.fetch` | +| name | type | description | +| ------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `options.auth` | string | **Required**. API access token | +| `options.userAgent` | string | Identifier of your app. Defaults to `replicate-javascript/${packageJSON.version}` | +| `options.baseUrl` | string | Defaults to https://api.replicate.com/v1 | +| `options.fetch` | function | Fetch function to use. Defaults to `globalThis.fetch` | +| `options.fileEncodingStrategy` | string | Determines the file encoding strategy to use. Possible values: `"default"`, `"upload"`, or `"data-uri"`. Defaults to `"default"` | +| `options.useFileOutput` | boolean | Determines if the `replicate.run()` method should convert file output into `FileOutput` objects | + The client makes requests to Replicate's API using [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch). @@ -114,7 +301,7 @@ By default, the `globalThis.fetch` function is used, which is available on [Node.js 18](https://nodejs.org/en/blog/announcements/v18-release-announce#fetch-experimental) and later, as well as [Cloudflare Workers](https://developers.cloudflare.com/workers/runtime-apis/fetch/), -[Vercel Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions), +[Vercel Functions](https://vercel.com/docs/functions), and other environments. On earlier versions of Node.js @@ -158,20 +345,27 @@ Run a model and await the result. Unlike [`replicate.prediction.create`](#replic const output = await replicate.run(identifier, options, progress); ``` -| name | type | description | -| ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `identifier` | string | **Required**. The model version identifier in the format `{owner}/{name}:{version}`, for example `stability-ai/sdxl:8beff3369e81422112d93b89ca01426147de542cd4684c244b673b105188fe5f` | -| `options.input` | object | **Required**. An object with the model inputs. | -| `options.wait` | object | Options for waiting for the prediction to finish | -| `options.wait.interval` | number | Polling interval in milliseconds. Defaults to 500 | -| `options.webhook` | string | An HTTPS URL for receiving a webhook when the prediction has new output | -| `options.webhook_events_filter` | string[] | An array of events which should trigger [webhooks](https://replicate.com/docs/webhooks). Allowable values are `start`, `output`, `logs`, and `completed` | -| `options.signal` | object | An [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) to cancel the prediction | -| `progress` | function | Callback function that receives the prediction object as it's updated. The function is called when the prediction is created, each time its updated while polling for completion, and when it's completed. | +| name | type | description | +| ------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `identifier` | string | **Required**. The model version identifier in the format `{owner}/{name}:{version}`, for example `stability-ai/sdxl:8beff3369e81422112d93b89ca01426147de542cd4684c244b673b105188fe5f` | +| `options.input` | object | **Required**. An object with the model inputs. | +| `options.wait` | object | Options for waiting for the prediction to finish | +| `options.wait.mode` | `"poll" \| "block"` | `"block"` holds the request open, `"poll"` makes repeated requests to fetch the prediction. Defaults to `"block"` | +| `options.wait.interval` | number | Polling interval in milliseconds. Defaults to 500 | +| `options.wait.timeout` | number | In `"block"` mode determines how long the request will be held open until falling back to polling. Defaults to 60 | +| `options.webhook` | string | An HTTPS URL for receiving a webhook when the prediction has new output | +| `options.webhook_events_filter` | string[] | An array of events which should trigger [webhooks](https://replicate.com/docs/webhooks). Allowable values are `start`, `output`, `logs`, and `completed` | +| `options.signal` | object | An [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) to cancel the prediction | +| `progress` | function | Callback function that receives the prediction object as it's updated. The function is called when the prediction is created, each time it's updated while polling for completion, and when it's completed. | Throws `Error` if the prediction failed. -Returns `Promise` which resolves with the output of running the model. +Returns `Promise` which resolves with the output of running the model. + +> [!NOTE] +> Currently the TypeScript return type of `replicate.run()` is `Promise` this is +> misleading as a model can return array types as well as primative types like strings, +> numbers and booleans. Example: @@ -181,6 +375,25 @@ const input = { prompt: "a 19th century portrait of a raccoon gentleman wearing const output = await replicate.run(model, { input }); ``` +Example that logs progress as the model is running: + +```js +const model = "stability-ai/sdxl:8beff3369e81422112d93b89ca01426147de542cd4684c244b673b105188fe5f"; +const input = { prompt: "a 19th century portrait of a raccoon gentleman wearing a suit" }; +const onProgress = (prediction) => { + const last_log_line = prediction.logs.split("\n").pop() + console.log({id: prediction.id, log: last_log_line}) +} +const output = await replicate.run(model, { input }, onProgress) +``` + +#### Sync vs. Async API (`"poll"` vs. `"block"`) + +The `replicate.run()` API takes advantage of the [Replicate sync API](https://replicate.com/docs/topics/predictions/create-a-prediction) +which is optimized for low latency requests to file models like `black-forest-labs/flux-dev` and +`black-forest-labs/flux-schnell`. When creating a prediction this will hold a connection open to the +server and return a `FileObject` containing the generated file as quickly as possible. + ### `replicate.stream` Run a model and stream its output. Unlike [`replicate.prediction.create`](#replicatepredictionscreate), this method returns only the prediction output rather than the entire prediction object. @@ -301,6 +514,18 @@ const response = await replicate.models.list(); } ``` +### `replicate.models.search` + +Search for public models on Replicate. + +```js +const response = await replicate.models.search(query); +``` + +| name | type | description | +| ------- | ------ | -------------------------------------- | +| `query` | string | **Required**. The search query string. | + ### `replicate.models.create` Create a new public or private model. @@ -314,7 +539,7 @@ const response = await replicate.models.create(model_owner, model_name, options) | `model_owner` | string | **Required**. The name of the user or organization that will own the model. This must be the same as the user or organization that is making the API request. In other words, the API token used in the request must belong to this user or organization. | | `model_name` | string | **Required**. The name of the model. This must be unique among all models owned by the user or organization. | | `options.visibility` | string | **Required**. Whether the model should be public or private. A public model can be viewed and run by anyone, whereas a private model can be viewed and run only by the user or organization members that own the model. | -| `options.hardware` | string | **Required**. The SKU for the hardware used to run the model. Possible values can be found by calling [`replicate.hardware.list()](#replicatehardwarelist)`. | +| `options.hardware` | string | **Required**. The SKU for the hardware used to run the model. Possible values can be found by calling [`replicate.hardware.list()`](#replicatehardwarelist). | | `options.description` | string | A description of the model. | | `options.github_url` | string | A URL for the model's source code on GitHub. | | `options.paper_url` | string | A URL for the model's paper. | @@ -378,7 +603,7 @@ const response = await replicate.models.versions.list(model_owner, model_name); ### `replicate.models.versions.get` -Get metatadata for a specific version of a model. +Get metadata for a specific version of a model. ```js const response = await replicate.models.versions.get(model_owner, model_name, version_id); @@ -423,8 +648,10 @@ const response = await replicate.predictions.create(options); | name | type | description | | ------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `options.version` | string | **Required**. The model version | | `options.input` | object | **Required**. An object with the model's inputs | +| `options.model` | string | The name of the model, e.g. `black-forest-labs/flux-schnell`. This is required if you're running an [official model](https://replicate.com/explore#official-models). | +| `options.version` | string | The 64-character [model version id](https://replicate.com/docs/topics/models/versions), e.g. `80537f9eead1a5bfa72d5ac6ea6414379be41d4d4f6679fd776e9535d1eb58bb`. This is required if you're not specifying a `model`. | +| `options.wait` | number | Wait up to 60s for the prediction to finish before returning. Disabled by default. See [Synchronous predictions](https://replicate.com/docs/topics/predictions/create-a-prediction#sync-mode) for more information. | | `options.stream` | boolean | Requests a URL for streaming output output | | `options.webhook` | string | An HTTPS URL for receiving a webhook when the prediction has new output | | `options.webhook_events_filter` | string[] | You can change which events trigger webhook requests by specifying webhook events (`start` \| `output` \| `logs` \| `completed`) | @@ -757,6 +984,205 @@ const response = await replicate.deployments.predictions.create(deployment_owner Use `replicate.wait` to wait for a prediction to finish, or `replicate.predictions.cancel` to cancel a prediction before it finishes. +### `replicate.deployments.list` + +List your deployments. + +```js +const response = await replicate.deployments.list(); +``` + +```jsonc +{ + "next": null, + "previous": null, + "results": [ + { + "owner": "acme", + "name": "my-app-image-generator", + "current_release": { /* ... */ } + } + /* ... */ + ] +} +``` + +### `replicate.deployments.create` + +Create a new deployment. + +```js +const response = await replicate.deployments.create(options); +``` + +| name | type | description | +| ----------------------- | ------ | -------------------------------------------------------------------------------- | +| `options.name` | string | Required. Name of the new deployment | +| `options.model` | string | Required. Name of the model in the format `{username}/{model_name}` | +| `options.version` | string | Required. ID of the model version | +| `options.hardware` | string | Required. SKU of the hardware to run the deployment on (`cpu`, `gpu-a100`, etc.) | +| `options.min_instances` | number | Minimum number of instances to run. Defaults to 0 | +| `options.max_instances` | number | Maximum number of instances to scale up to based on traffic. Defaults to 1 | + +```jsonc +{ + "owner": "acme", + "name": "my-app-image-generator", + "current_release": { + "number": 1, + "model": "stability-ai/sdxl", + "version": "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", + "created_at": "2024-03-14T11:43:32.049157Z", + "created_by": { + "type": "organization", + "username": "acme", + "name": "Acme, Inc.", + "github_url": "https://github.com/replicate" + }, + "configuration": { + "hardware": "gpu-a100", + "min_instances": 1, + "max_instances": 0 + } + } +} +``` + +### `replicate.deployments.update` + +Update an existing deployment. + +```js +const response = await replicate.deployments.update(deploymentOwner, deploymentName, options); +``` + +| name | type | description | +| ----------------------- | ------ | -------------------------------------------------------------------------------- | +| `deploymentOwner` | string | Required. Owner of the deployment | +| `deploymentName` | string | Required. Name of the deployment to update | +| `options.model` | string | Name of the model in the format `{username}/{model_name}` | +| `options.version` | string | ID of the model version | +| `options.hardware` | string | Required. SKU of the hardware to run the deployment on (`cpu`, `gpu-a100`, etc.) | +| `options.min_instances` | number | Minimum number of instances to run | +| `options.max_instances` | number | Maximum number of instances to scale up to | + +```jsonc +{ + "owner": "acme", + "name": "my-app-image-generator", + "current_release": { + "number": 2, + "model": "stability-ai/sdxl", + "version": "39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b", + "created_at": "2024-03-14T11:43:32.049157Z", + "created_by": { + "type": "organization", + "username": "acme", + "name": "Acme, Inc.", + "github_url": "https://github.com/replicate" + }, + "configuration": { + "hardware": "gpu-a100", + "min_instances": 1, + "max_instances": 0 + } + } +} +``` + +### `replicate.files.create` + +Upload a file to Replicate. + +> [!TIP] +> The client library calls this endpoint automatically to upload the contents of +> file handles provided as prediction and training inputs. +> You don't need to call this method directly unless you want more control. +> For example, you might want to reuse a file across multiple predictions +> without re-uploading it each time, +> or you may want to set custom metadata on the file resource. +> +> You can configure how a client handles file handle inputs +> by setting the `fileEncodingStrategy` option in the +> [client constructor](#constructor). + +```js +const response = await replicate.files.create(file, metadata); +``` + +| name | type | description | +| ---------- | --------------------- | ---------------------------------------------------------- | +| `file` | Blob, File, or Buffer | **Required**. The file to upload. | +| `metadata` | object | Optional. User-provided metadata associated with the file. | + +```jsonc +{ + "id": "MTQzODcyMDct0YjZkLWE1ZGYtMmRjZTViNWIwOGEyNjNhNS0", + "name": "photo.webp", + "content_type": "image/webp", + "size": 96936, + "etag": "f211779ff7502705bbf42e9874a17ab3", + "checksums": { + "sha256": "7282eb6991fa4f38d80c312dc207d938c156d714c94681623aedac846488e7d3", + "md5": "f211779ff7502705bbf42e9874a17ab3" + }, + "metadata": { + "customer_reference_id": "123" + }, + "created_at": "2024-06-28T10:16:04.062Z", + "expires_at": "2024-06-29T10:16:04.062Z", + "urls": { + "get": "https://api.replicate.com/v1/files/MTQzODcyMDct0YjZkLWE1ZGYtMmRjZTViNWIwOGEyNjNhNS0" + } +} +``` + +Files uploaded to Replicate using this endpoint expire after 24 hours. + +Pass the `urls.get` property of a file resource +to use it as an input when running a model on Replicate. +The value of `urls.get` is opaque, +and shouldn't be inferred from other attributes. + +The contents of a file are only made accessible to a model running on Replicate, +and only when passed as a prediction or training input +by the user or organization who created the file. + +### `replicate.files.list` + +List all files you've uploaded. + +```js +const response = await replicate.files.list(); +``` + +### `replicate.files.get` + +Get metadata for a specific file. + +```js +const response = await replicate.files.get(file_id); +``` + +| name | type | description | +| --------- | ------ | --------------------------------- | +| `file_id` | string | **Required**. The ID of the file. | + +### `replicate.files.delete` + +Delete a file. + +Files uploaded using the `replicate.files.create` method expire after 24 hours. +You can use this method to delete them sooner. + +```js +const response = await replicate.files.delete(file_id); +``` + +| name | type | description | +| --------- | ------ | --------------------------------- | +| `file_id` | string | **Required**. The ID of the file. | + ### `replicate.paginate` Pass another method as an argument to iterate over results @@ -787,15 +1213,67 @@ Low-level method used by the Replicate client to interact with API endpoints. const response = await replicate.request(route, parameters); ``` -| name | type | description | -| -------------------- | ------ | ------------------------------------------------------------ | -| `options.route` | string | Required. REST API endpoint path. | -| `options.parameters` | object | URL, query, and request body parameters for the given route. | +| name | type | description | +| -------------------- | ------------------- | ----------- | +| `options.route` | `string` | Required. REST API endpoint path. +| `options.params` | `object` | URL query parameters for the given route. | +| `options.method` | `string` | HTTP method for the given route. | +| `options.headers` | `object` | Additional HTTP headers for the given route. | +| `options.data` | `object \| FormData` | Request body. | +| `options.signal` | `AbortSignal` | Optional `AbortSignal`. | The `replicate.request()` method is used by the other methods to interact with the Replicate API. You can call this method directly to make other requests to the API. -## TypeScript +The method accepts an `AbortSignal` which can be used to cancel the request in flight. -The `Replicate` constructor and all `replicate.*` methods are fully typed. +### `FileOutput` + +`FileOutput` is a `ReadableStream` instance that represents a model file output. It can be used to stream file data to disk or as a `Response` body to an HTTP request. + +```javascript +const [output] = await replicate.run("black-forest-labs/flux-schnell", { + input: { prompt: "astronaut riding a rocket like a horse" } +}); + +// To access the file URL: +console.log(output.url()); //=> "http://example.com" + +// To write the file to disk: +fs.writeFile("my-image.png", output); + +// To stream the file back to a browser: +return new Response(output); + +// To read the file in chunks: +for await (const chunk of output) { + console.log(chunk); // UInt8Array +} +``` + +You can opt out of FileOutput by passing `useFileOutput: false` to the `Replicate` constructor: + +```javascript +const replicate = new Replicate({ useFileOutput: false }); +``` + +| method | returns | description | +| -------------------- | ------ | ------------------------------------------------------------ | +| `blob()` | object | A `Blob` instance containing the binary file | +| `url()` | string | A `URL` object pointing to the underlying data source. Please note that this may not always be an HTTP URL in future. | + +## Troubleshooting + +### Predictions hanging in Next.js + +Next.js App Router adds some extensions to `fetch` to make it cache responses. To disable this behavior, set the `cache` option to `"no-store"` on the Replicate client's fetch object: + +```js +replicate = new Replicate({/*...*/}) +replicate.fetch = (url, options) => { + return fetch(url, { ...options, cache: "no-store" }); +}; +``` + +Alternatively you can use Next.js [`noStore`](https://github.com/replicate/replicate-javascript/issues/136#issuecomment-1847442879) to opt out of caching for your component. diff --git a/biome.json b/biome.json index 807b9013..094cf0ec 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,12 @@ { "$schema": "https://biomejs.dev/schemas/1.0.0/schema.json", + "files": { + "ignore": [ + ".wrangler", + "node_modules", + "vendor/*" + ] + }, "formatter": { "indentStyle": "space", "indentWidth": 2 diff --git a/index.d.ts b/index.d.ts index 69e651b1..2b183d04 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,6 +8,12 @@ declare module "replicate" { response: Response; } + export interface FileOutput extends ReadableStream { + blob(): Promise; + url(): URL; + toString(): string; + } + export interface Account { type: "user" | "organization"; username: string; @@ -39,6 +45,21 @@ declare module "replicate" { }; } + export interface FileObject { + id: string; + name: string; + content_type: string; + size: number; + etag: string; + checksum: string; + metadata: Record; + created_at: string; + expires_at: string | null; + urls: { + get: string; + }; + } + export interface Hardware { sku: string; name: string; @@ -72,9 +93,9 @@ declare module "replicate" { model: string; version: string; input: object; - output?: any; + output?: any; // TODO: this should be `unknown` source: "api" | "web"; - error?: any; + error?: unknown; logs?: string; metrics?: { predict_time?: number; @@ -93,6 +114,8 @@ declare module "replicate" { export type Training = Prediction; + export type FileEncodingStrategy = "default" | "upload" | "data-uri"; + export interface Page { previous?: string; next?: string; @@ -119,18 +142,23 @@ declare module "replicate" { input: Request | string, init?: RequestInit ) => Promise; + fileEncodingStrategy?: FileEncodingStrategy; + useFileOutput?: boolean; }); auth: string; userAgent?: string; baseUrl?: string; fetch: (input: Request | string, init?: RequestInit) => Promise; + fileEncodingStrategy: FileEncodingStrategy; run( identifier: `${string}/${string}` | `${string}/${string}:${string}`, options: { input: object; - wait?: { interval?: number }; + wait?: + | { mode: "block"; interval?: number; timeout?: number } + | { mode: "poll"; interval?: number }; webhook?: string; webhook_events_filter?: WebhookEventType[]; signal?: AbortSignal; @@ -155,10 +183,14 @@ declare module "replicate" { headers?: object | Headers; params?: object; data?: object; + signal?: AbortSignal; } ): Promise; - paginate(endpoint: () => Promise>): AsyncGenerator<[T]>; + paginate( + endpoint: () => Promise>, + options?: { signal?: AbortSignal } + ): AsyncGenerator; wait( prediction: Prediction, @@ -169,12 +201,15 @@ declare module "replicate" { ): Promise; accounts: { - current(): Promise; + current(options?: { signal?: AbortSignal }): Promise; }; collections: { - list(): Promise>; - get(collection_slug: string): Promise; + list(options?: { signal?: AbortSignal }): Promise>; + get( + collection_slug: string, + options?: { signal?: AbortSignal } + ): Promise; }; deployments: { @@ -184,25 +219,83 @@ declare module "replicate" { deployment_name: string, options: { input: object; + /** @deprecated */ stream?: boolean; webhook?: string; webhook_events_filter?: WebhookEventType[]; + wait?: number | boolean; + signal?: AbortSignal; } ): Promise; }; get( deployment_owner: string, - deployment_name: string + deployment_name: string, + options?: { signal?: AbortSignal } + ): Promise; + create( + deployment_config: { + name: string; + model: string; + version: string; + hardware: string; + min_instances: number; + max_instances: number; + }, + options?: { signal?: AbortSignal } + ): Promise; + update( + deployment_owner: string, + deployment_name: string, + deployment_config: { + version?: string; + hardware?: string; + min_instances?: number; + max_instances?: number; + } & ( + | { version: string } + | { hardware: string } + | { min_instances: number } + | { max_instances: number } + ), + options?: { signal?: AbortSignal } ): Promise; + delete( + deployment_owner: string, + deployment_name: string, + options?: { signal?: AbortSignal } + ): Promise; + list(options?: { signal?: AbortSignal }): Promise>; + }; + + files: { + create( + file: Blob | Buffer, + metadata?: Record, + options?: { signal?: AbortSignal } + ): Promise; + list(options?: { signal?: AbortSignal }): Promise>; + get( + file_id: string, + options?: { signal?: AbortSignal } + ): Promise; + delete( + file_id: string, + options?: { signal?: AbortSignal } + ): Promise; }; hardware: { - list(): Promise; + list(options?: { signal?: AbortSignal }): Promise; }; models: { - get(model_owner: string, model_name: string): Promise; - list(): Promise>; + get( + model_owner: string, + model_name: string, + options?: { signal?: AbortSignal } + ): Promise; + list(options?: { signal?: AbortSignal }): Promise>; create( model_owner: string, model_name: string, @@ -214,16 +307,26 @@ declare module "replicate" { paper_url?: string; license_url?: string; cover_image_url?: string; + signal?: AbortSignal; } ): Promise; versions: { - list(model_owner: string, model_name: string): Promise; + list( + model_owner: string, + model_name: string, + options?: { signal?: AbortSignal } + ): Promise>; get( model_owner: string, model_name: string, - version_id: string + version_id: string, + options?: { signal?: AbortSignal } ): Promise; }; + search( + query: string, + options?: { signal?: AbortSignal } + ): Promise>; }; predictions: { @@ -232,14 +335,23 @@ declare module "replicate" { model?: string; version?: string; input: object; + /** @deprecated */ stream?: boolean; webhook?: string; webhook_events_filter?: WebhookEventType[]; + wait?: boolean | number; + signal?: AbortSignal; } & ({ version: string } | { model: string }) ): Promise; - get(prediction_id: string): Promise; - cancel(prediction_id: string): Promise; - list(): Promise>; + get( + prediction_id: string, + options?: { signal?: AbortSignal } + ): Promise; + cancel( + prediction_id: string, + options?: { signal?: AbortSignal } + ): Promise; + list(options?: { signal?: AbortSignal }): Promise>; }; trainings: { @@ -252,32 +364,49 @@ declare module "replicate" { input: object; webhook?: string; webhook_events_filter?: WebhookEventType[]; + signal?: AbortSignal; } ): Promise; - get(training_id: string): Promise; - cancel(training_id: string): Promise; - list(): Promise>; + get( + training_id: string, + options?: { signal?: AbortSignal } + ): Promise; + cancel( + training_id: string, + options?: { signal?: AbortSignal } + ): Promise; + list(options?: { signal?: AbortSignal }): Promise>; }; webhooks: { default: { secret: { - get(): Promise; + get(options?: { signal?: AbortSignal }): Promise; }; }; }; } export function validateWebhook( - requestData: - | Request - | { - id?: string; - timestamp?: string; - body: string; - secret?: string; - signature?: string; - }, - secret: string - ): boolean; + request: Request, + secret: string, + crypto?: Crypto + ): Promise; + + export function validateWebhook( + requestData: { + id: string; + timestamp: string; + signature: string; + body: string; + secret: string; + }, + crypto?: Crypto + ): Promise; + + export function parseProgressFromLogs(logs: Prediction | string): { + percentage: number; + current: number; + total: number; + } | null; } diff --git a/index.js b/index.js index 83b98887..b1248e70 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,18 @@ const ApiError = require("./lib/error"); const ModelVersionIdentifier = require("./lib/identifier"); -const { Stream } = require("./lib/stream"); -const { withAutomaticRetries, validateWebhook } = require("./lib/util"); +const { createReadableStream, createFileOutput } = require("./lib/stream"); +const { + transform, + withAutomaticRetries, + validateWebhook, + parseProgressFromLogs, + streamAsyncIterator, +} = require("./lib/util"); const accounts = require("./lib/accounts"); const collections = require("./lib/collections"); const deployments = require("./lib/deployments"); +const files = require("./lib/files"); const hardware = require("./lib/hardware"); const models = require("./lib/models"); const predictions = require("./lib/predictions"); @@ -41,13 +48,19 @@ class Replicate { * @param {string} options.userAgent - Identifier of your app * @param {string} [options.baseUrl] - Defaults to https://api.replicate.com/v1 * @param {Function} [options.fetch] - Fetch function to use. Defaults to `globalThis.fetch` + * @param {boolean} [options.useFileOutput] - Set to `false` to disable `FileOutput` objects from `run` instead of URLs, defaults to true. + * @param {"default" | "upload" | "data-uri"} [options.fileEncodingStrategy] - Determines the file encoding strategy to use */ constructor(options = {}) { - this.auth = options.auth || process.env.REPLICATE_API_TOKEN; + this.auth = + options.auth || + (typeof process !== "undefined" ? process.env.REPLICATE_API_TOKEN : null); this.userAgent = options.userAgent || `replicate-javascript/${packageJSON.version}`; this.baseUrl = options.baseUrl || "https://api.replicate.com/v1"; this.fetch = options.fetch || globalThis.fetch; + this.fileEncodingStrategy = options.fileEncodingStrategy || "default"; + this.useFileOutput = options.useFileOutput === false ? false : true; this.accounts = { current: accounts.current.bind(this), @@ -60,11 +73,22 @@ class Replicate { this.deployments = { get: deployments.get.bind(this), + create: deployments.create.bind(this), + update: deployments.update.bind(this), + delete: deployments.delete.bind(this), + list: deployments.list.bind(this), predictions: { create: deployments.predictions.create.bind(this), }, }; + this.files = { + create: files.create.bind(this), + get: files.get.bind(this), + list: files.list.bind(this), + delete: files.delete.bind(this), + }; + this.hardware = { list: hardware.list.bind(this), }; @@ -77,6 +101,7 @@ class Replicate { list: models.versions.list.bind(this), get: models.versions.get.bind(this), }, + search: models.search.bind(this), }; this.predictions = { @@ -108,8 +133,7 @@ class Replicate { * @param {string} ref - Required. The model version identifier in the format "owner/name" or "owner/name:version" * @param {object} options * @param {object} options.input - Required. An object with the model inputs - * @param {object} [options.wait] - Options for waiting for the prediction to finish - * @param {number} [options.wait.interval] - Polling interval in milliseconds. Defaults to 500 + * @param {{mode: "block", timeout?: number, interval?: number} | {mode: "poll", interval?: number }} [options.wait] - Options for waiting for the prediction to finish. If `wait` is explicitly true, the function will block and wait for the prediction to finish. * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the prediction has new output * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) * @param {AbortSignal} [options.signal] - AbortSignal to cancel the prediction @@ -119,7 +143,7 @@ class Replicate { * @returns {Promise} - Resolves with the output of running the model */ async run(ref, options, progress) { - const { wait, ...data } = options; + const { wait = { mode: "block" }, signal, ...data } = options; const identifier = ModelVersionIdentifier.parse(ref); @@ -128,11 +152,13 @@ class Replicate { prediction = await this.predictions.create({ ...data, version: identifier.version, + wait: wait.mode === "block" ? wait.timeout ?? true : false, }); } else if (identifier.owner && identifier.name) { prediction = await this.predictions.create({ ...data, model: `${identifier.owner}/${identifier.name}`, + wait: wait.mode === "block" ? wait.timeout ?? true : false, }); } else { throw new Error("Invalid model version identifier"); @@ -143,25 +169,30 @@ class Replicate { progress(prediction); } - const { signal } = options; - - prediction = await this.wait( - prediction, - wait || {}, - async (updatedPrediction) => { - // Call progress callback with the updated prediction object - if (progress) { - progress(updatedPrediction); - } - - if (signal && signal.aborted) { - await this.predictions.cancel(updatedPrediction.id); - return true; // stop polling + const isDone = wait.mode === "block" && prediction.status !== "starting"; + if (!isDone) { + prediction = await this.wait( + prediction, + { interval: wait.mode === "poll" ? wait.interval : undefined }, + async (updatedPrediction) => { + // Call progress callback with the updated prediction object + if (progress) { + progress(updatedPrediction); + } + + // We handle the cancel later in the function. + if (signal && signal.aborted) { + return true; // stop polling + } + + return false; // continue polling } + ); + } - return false; // continue polling - } - ); + if (signal && signal.aborted) { + prediction = await this.predictions.cancel(prediction.id); + } // Call progress callback with the completed prediction object if (progress) { @@ -172,7 +203,17 @@ class Replicate { throw new Error(`Prediction failed: ${prediction.error}`); } - return prediction.output; + return transform(prediction.output, (value) => { + if ( + typeof value === "string" && + (value.startsWith("https:") || value.startsWith("data:")) + ) { + return this.useFileOutput + ? createFileOutput({ url: value, fetch: this.fetch }) + : value; + } + return value; + }); } /** @@ -184,6 +225,7 @@ class Replicate { * @param {object} [options.params] - Query parameters * @param {object|Headers} [options.headers] - HTTP headers * @param {object} [options.data] - Body parameters + * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request * @returns {Promise} - Resolves with the response object * @throws {ApiError} If the request failed */ @@ -200,28 +242,39 @@ class Replicate { ); } - const { method = "GET", params = {}, data } = options; + const { method = "GET", params = {}, data, signal } = options; for (const [key, value] of Object.entries(params)) { url.searchParams.append(key, value); } - const headers = {}; + const headers = { + "Content-Type": "application/json", + "User-Agent": userAgent, + }; if (auth) { - headers["Authorization"] = `Token ${auth}`; + headers["Authorization"] = `Bearer ${auth}`; } - headers["Content-Type"] = "application/json"; - headers["User-Agent"] = userAgent; if (options.headers) { for (const [key, value] of Object.entries(options.headers)) { headers[key] = value; } } + let body = undefined; + if (data instanceof FormData) { + body = data; + // biome-ignore lint/performance/noDelete: + delete headers["Content-Type"]; // Use automatic content type header + } else if (data) { + body = JSON.stringify(data); + } + const init = { method, headers, - body: data ? JSON.stringify(data) : undefined, + body, + signal, }; const shouldRetry = @@ -262,7 +315,7 @@ class Replicate { * @yields {ServerSentEvent} Each streamed event from the prediction */ async *stream(ref, options) { - const { wait, ...data } = options; + const { wait, signal, ...data } = options; const identifier = ModelVersionIdentifier.parse(ref); @@ -271,22 +324,24 @@ class Replicate { prediction = await this.predictions.create({ ...data, version: identifier.version, - stream: true, }); } else if (identifier.owner && identifier.name) { prediction = await this.predictions.create({ ...data, model: `${identifier.owner}/${identifier.name}`, - stream: true, }); } else { throw new Error("Invalid model version identifier"); } if (prediction.urls && prediction.urls.stream) { - const { signal } = options; - const stream = new Stream(prediction.urls.stream, { signal }); - yield* stream; + const stream = createReadableStream({ + url: prediction.urls.stream, + fetch: this.fetch, + ...(signal ? { options: { signal } } : {}), + }); + + yield* streamAsyncIterator(stream); } else { throw new Error("Prediction does not support streaming"); } @@ -301,15 +356,20 @@ class Replicate { * console.log(page); * } * @param {Function} endpoint - Function that returns a promise for the next page of results + * @param {object} [options] + * @param {AbortSignal} [options.signal] - AbortSignal to cancel the request. * @yields {object[]} Each page of results */ - async *paginate(endpoint) { + async *paginate(endpoint, options = {}) { const response = await endpoint(); yield response.results; - if (response.next) { + if (response.next && !(options.signal && options.signal.aborted)) { const nextPage = () => - this.request(response.next, { method: "GET" }).then((r) => r.json()); - yield* this.paginate(nextPage); + this.request(response.next, { + method: "GET", + signal: options.signal, + }).then((r) => r.json()); + yield* this.paginate(nextPage, options); } } @@ -375,3 +435,4 @@ class Replicate { module.exports = Replicate; module.exports.validateWebhook = validateWebhook; +module.exports.parseProgressFromLogs = parseProgressFromLogs; diff --git a/index.test.ts b/index.test.ts index f00a7e68..4905908a 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1,18 +1,46 @@ import { expect, jest, test } from "@jest/globals"; import Replicate, { ApiError, + FileOutput, Model, Prediction, validateWebhook, + parseProgressFromLogs, } from "replicate"; import nock from "nock"; -import fetch from "cross-fetch"; +import { createReadableStream } from "./lib/stream"; +import { webcrypto } from "node:crypto"; let client: Replicate; const BASE_URL = "https://api.replicate.com/v1"; nock.disableNetConnect(); +const fileTestCases = [ + // Skip test case if File type is not available + ...(typeof File !== "undefined" + ? [ + { + type: "file", + value: new File(["hello world"], "file_hello.txt", { + type: "text/plain", + }), + expected: "data:text/plain;base64,aGVsbG8gd29ybGQ=", + }, + ] + : []), + { + type: "blob", + value: new Blob(["hello world"], { type: "text/plain" }), + expected: "data:text/plain;base64,aGVsbG8gd29ybGQ=", + }, + { + type: "buffer", + value: Buffer.from("hello world"), + expected: "data:application/octet-stream;base64,aGVsbG8gd29ybGQ=", + }, +]; + describe("Replicate client", () => { let unmatched: any[] = []; const handleNoMatch = (req: unknown, options: any, body: string) => @@ -20,7 +48,6 @@ describe("Replicate client", () => { beforeEach(() => { client = new Replicate({ auth: "test-token" }); - client.fetch = fetch; unmatched = []; nock.emitter.on("no match", handleNoMatch); @@ -72,6 +99,90 @@ describe("Replicate client", () => { }); }); + describe("paginate", () => { + test("pages through results", async () => { + nock(BASE_URL) + .get("/collections") + .reply(200, { + results: [ + { + name: "Super resolution", + slug: "super-resolution", + description: + "Upscaling models that create high-quality images from low-quality images.", + }, + ], + next: `${BASE_URL}/collections?page=2`, + previous: null, + }); + nock(BASE_URL) + .get("/collections?page=2") + .reply(200, { + results: [ + { + name: "Image classification", + slug: "image-classification", + description: "Models that classify images.", + }, + ], + next: null, + previous: null, + }); + + const iterator = client.paginate(client.collections.list); + + const firstPage = (await iterator.next()).value; + expect(firstPage.length).toBe(1); + + const secondPage = (await iterator.next()).value; + expect(secondPage.length).toBe(1); + }); + + test("accepts an abort signal", async () => { + nock(BASE_URL) + .get("/collections") + .reply(200, { + results: [ + { + name: "Super resolution", + slug: "super-resolution", + description: + "Upscaling models that create high-quality images from low-quality images.", + }, + ], + next: `${BASE_URL}/collections?page=2`, + previous: null, + }); + nock(BASE_URL) + .get("/collections?page=2") + .reply(200, { + results: [ + { + name: "Image classification", + slug: "image-classification", + description: "Models that classify images.", + }, + ], + next: null, + previous: null, + }); + + const controller = new AbortController(); + const iterator = client.paginate(client.collections.list, { + signal: controller.signal, + }); + + const firstIteration = await iterator.next(); + expect(firstIteration.value.length).toBe(1); + + controller.abort(); + + const secondIteration = await iterator.next(); + expect(secondIteration.value).toBeUndefined(); + expect(secondIteration.done).toBe(true); + }); + }); + describe("account.get", () => { test("Calls the correct API route", async () => { nock(BASE_URL).get("/account").reply(200, { @@ -184,73 +295,142 @@ describe("Replicate client", () => { }); describe("predictions.create", () => { - test("Calls the correct API route with the correct payload", async () => { - nock(BASE_URL) - .post("/predictions") - .reply(200, { - id: "ufawqhfynnddngldkgtslldrkq", - model: "replicate/hello-world", - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", - urls: { - get: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq", - cancel: - "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", - }, - created_at: "2022-04-26T22:13:06.224088Z", - started_at: null, - completed_at: null, - status: "starting", - input: { - text: "Alice", - }, - output: null, - error: null, - logs: null, - metrics: {}, - }); - const prediction = await client.predictions.create({ - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + const predictionTestCases = [ + { + description: "String input", input: { text: "Alice", }, - webhook: "http://test.host/webhook", - webhook_events_filter: ["output", "completed"], - }); - expect(prediction.id).toBe("ufawqhfynnddngldkgtslldrkq"); - }); - - test.each([ - // Skip test case if File type is not available - ...(typeof File !== "undefined" - ? [ - { - type: "file", - value: new File(["hello world"], "hello.txt", { - type: "text/plain", - }), - expected: "data:text/plain;base64,aGVsbG8gd29ybGQ=", - }, - ] - : []), + }, + { + description: "Number input", + input: { + text: 123, + }, + }, + { + description: "Boolean input", + input: { + text: true, + }, + }, { - type: "blob", - value: new Blob(["hello world"], { type: "text/plain" }), - expected: "data:text/plain;base64,aGVsbG8gd29ybGQ=", + description: "Array input", + input: { + text: ["Alice", "Bob", "Charlie"], + }, }, { - type: "buffer", - value: Buffer.from("hello world"), - expected: "data:application/octet-stream;base64,aGVsbG8gd29ybGQ=", + description: "Object input", + input: { + text: { + name: "Alice", + }, + }, + }, + ].map((testCase) => ({ + ...testCase, + expectedResponse: { + id: "ufawqhfynnddngldkgtslldrkq", + model: "replicate/hello-world", + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + urls: { + get: "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq", + cancel: + "https://api.replicate.com/v1/predictions/ufawqhfynnddngldkgtslldrkq/cancel", + }, + input: testCase.input, + created_at: "2022-04-26T22:13:06.224088Z", + started_at: null, + completed_at: null, + status: "starting", }, - ])( + })); + + test.each(predictionTestCases)( + "$description", + async ({ input, expectedResponse }) => { + nock(BASE_URL) + .post("/predictions", { + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + input: input as Record, + webhook: "http://test.host/webhook", + webhook_events_filter: ["output", "completed"], + }) + .reply(200, expectedResponse); + + const response = await client.predictions.create({ + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + input: input as Record, + webhook: "http://test.host/webhook", + webhook_events_filter: ["output", "completed"], + }); + + expect(response.input).toEqual(input); + expect(response.status).toBe(expectedResponse.status); + } + ); + + test.each(fileTestCases)( + "converts a $type input into a Replicate file URL", + async ({ value: data, type }) => { + const mockedFetch = jest.spyOn(client, "fetch"); + + nock(BASE_URL) + .post("/files") + .reply(201, { + urls: { + get: "https://replicate.com/api/files/123", + }, + }) + .post( + "/predictions", + (body) => body.input.data === "https://replicate.com/api/files/123" + ) + .reply(201, (_uri: string, body: Record) => { + return body; + }); + + const prediction = await client.predictions.create({ + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + input: { + prompt: "Tell me a story", + data, + }, + }); + + expect(client.fetch).toHaveBeenCalledWith( + new URL("https://api.replicate.com/v1/files"), + { + method: "POST", + body: expect.any(FormData), + headers: expect.any(Object), + } + ); + const form = mockedFetch.mock.calls[0][1]?.body as FormData; + // @ts-ignore + expect(form?.get("content")?.name).toMatch(new RegExp(`^${type}_`)); + + expect(prediction.input).toEqual({ + prompt: "Tell me a story", + data: "https://replicate.com/api/files/123", + }); + } + ); + + test.each(fileTestCases)( "converts a $type input into a base64 encoded string", async ({ value: data, expected }) => { let actual: Record | undefined; nock(BASE_URL) + .post("/files") + .reply(503, "Service Unavailable") .post("/predictions") - .reply(201, (uri: string, body: Record) => { + .reply(201, (_uri: string, body: Record) => { actual = body; return body; }); @@ -262,30 +442,42 @@ describe("Replicate client", () => { prompt: "Tell me a story", data, }, - stream: true, }); expect(actual?.input.data).toEqual(expected); } ); - test("Passes stream parameter to API endpoint", async () => { - nock(BASE_URL) - .post("/predictions") - .reply(201, (_uri, body) => { - expect((body as any).stream).toBe(true); - return body; - }); + test.each(fileTestCases)( + "raises an error when the file upload fails with 4xx error for a $type input", + async ({ value: data, expected }) => { + let actual: Record | undefined; + nock(BASE_URL) + .post("/files") + .reply(401, "Unauthorized") + .post("/predictions") + .reply(201, (_uri: string, body: Record) => { + actual = body; + return body; + }); - await client.predictions.create({ - version: - "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", - input: { - prompt: "Tell me a story", - }, - stream: true, - }); - }); + await expect(async () => { + await client.predictions.create({ + version: + "5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + input: { + prompt: "Tell me a story", + data, + }, + }); + }).rejects.toThrowError( + expect.objectContaining({ + name: "ApiError", + message: expect.stringContaining("401"), + }) + ); + } + ); test("Throws an error if webhook URL is invalid", async () => { await expect(async () => { @@ -790,10 +982,8 @@ describe("Replicate client", () => { }, configuration: { hardware: "gpu-t4", - scaling: { - min_instances: 1, - max_instances: 5, - }, + min_instances: 1, + max_instances: 5, }, }, }); @@ -810,70 +1000,295 @@ describe("Replicate client", () => { // Add more tests for error handling, edge cases, etc. }); - describe("predictions.create with model", () => { + describe("deployments.create", () => { test("Calls the correct API route with the correct payload", async () => { nock(BASE_URL) - .post("/models/meta/llama-2-70b-chat/predictions") + .post("/deployments") .reply(200, { - id: "heat2o3bzn3ahtr6bjfftvbaci", - model: "replicate/lifeboat-70b", - version: "d-c6559c5791b50af57b69f4a73f8e021c", - input: { - prompt: "Please write a haiku about llamas.", - }, - logs: "", - error: null, - status: "starting", - created_at: "2023-11-27T13:35:45.99397566Z", - urls: { - cancel: - "https://api.replicate.com/v1/predictions/heat2o3bzn3ahtr6bjfftvbaci/cancel", - get: "https://api.replicate.com/v1/predictions/heat2o3bzn3ahtr6bjfftvbaci", + owner: "acme", + name: "my-app-image-generator", + current_release: { + number: 1, + model: "stability-ai/sdxl", + version: + "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", + created_at: "2024-02-15T16:32:57.018467Z", + created_by: { + type: "organization", + username: "acme", + name: "Acme Corp, Inc.", + github_url: "https://github.com/acme", + }, + configuration: { + hardware: "gpu-t4", + min_instances: 1, + max_instances: 5, + }, }, }); - const prediction = await client.predictions.create({ - model: "meta/llama-2-70b-chat", - input: { - prompt: "Please write a haiku about llamas.", - }, - webhook: "http://test.host/webhook", - webhook_events_filter: ["output", "completed"], - }); - expect(prediction.id).toBe("heat2o3bzn3ahtr6bjfftvbaci"); - }); - // Add more tests for error handling, edge cases, etc. - }); - describe("hardware.list", () => { - test("Calls the correct API route", async () => { - nock(BASE_URL) - .get("/hardware") - .reply(200, [ - { name: "CPU", sku: "cpu" }, - { name: "Nvidia T4 GPU", sku: "gpu-t4" }, - { name: "Nvidia A40 GPU", sku: "gpu-a40-small" }, - { name: "Nvidia A40 (Large) GPU", sku: "gpu-a40-large" }, - ]); + const deployment = await client.deployments.create({ + name: "my-app-image-generator", + model: "stability-ai/sdxl", + version: + "da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf", + hardware: "gpu-t4", + min_instances: 1, + max_instances: 5, + }); - const hardware = await client.hardware.list(); - expect(hardware.length).toBe(4); - expect(hardware[0].name).toBe("CPU"); - expect(hardware[0].sku).toBe("cpu"); + expect(deployment.owner).toBe("acme"); + expect(deployment.name).toBe("my-app-image-generator"); + expect(deployment.current_release.model).toBe("stability-ai/sdxl"); }); // Add more tests for error handling, edge cases, etc. }); - describe("models.create", () => { + describe("deployments.update", () => { test("Calls the correct API route with the correct payload", async () => { - nock(BASE_URL).post("/models").reply(200, { - owner: "test-owner", - name: "test-model", - visibility: "public", - hardware: "cpu", - description: "A test model", - }); - - const model = await client.models.create("test-owner", "test-model", { + nock(BASE_URL) + .patch("/deployments/acme/my-app-image-generator") + .reply(200, { + owner: "acme", + name: "my-app-image-generator", + current_release: { + number: 2, + model: "stability-ai/sdxl", + version: + "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", + created_at: "2024-02-16T08:14:22.345678Z", + created_by: { + type: "organization", + username: "acme", + name: "Acme Corp, Inc.", + github_url: "https://github.com/acme", + }, + configuration: { + hardware: "gpu-a40-large", + min_instances: 3, + max_instances: 10, + }, + }, + }); + + const deployment = await client.deployments.update( + "acme", + "my-app-image-generator", + { + version: + "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532", + hardware: "gpu-a40-large", + min_instances: 3, + max_instances: 10, + } + ); + + expect(deployment.current_release.number).toBe(2); + expect(deployment.current_release.version).toBe( + "632231d0d49d34d5c4633bd838aee3d81d936e59a886fbf28524702003b4c532" + ); + expect(deployment.current_release.configuration.hardware).toBe( + "gpu-a40-large" + ); + expect(deployment.current_release.configuration.min_instances).toBe(3); + expect(deployment.current_release.configuration.max_instances).toBe(10); + }); + // Add more tests for error handling, edge cases, etc. + }); + + describe("deployments.delete", () => { + test("Calls the correct API route with the correct payload", async () => { + nock(BASE_URL) + .delete("/deployments/acme/my-app-image-generator") + .reply(204); + + const success = await client.deployments.delete( + "acme", + "my-app-image-generator" + ); + expect(success).toBe(true); + }); + }); + + describe("deployments.list", () => { + test("Calls the correct API route", async () => { + nock(BASE_URL) + .get("/deployments") + .reply(200, { + next: null, + previous: null, + results: [ + { + owner: "acme", + name: "my-app-image-generator", + current_release: { + // ... + }, + }, + // ... + ], + }); + + const deployments = await client.deployments.list(); + expect(deployments.results.length).toBe(1); + }); + // Add more tests for pagination, error handling, edge cases, etc. + }); + + describe("predictions.create with model", () => { + test("Calls the correct API route with the correct payload", async () => { + nock(BASE_URL) + .post("/models/meta/meta-llama-3-70b-instruct/predictions") + .reply(200, { + id: "heat2o3bzn3ahtr6bjfftvbaci", + model: "replicate/lifeboat-70b", + version: "d-c6559c5791b50af57b69f4a73f8e021c", + input: { + prompt: "Please write a haiku about llamas.", + }, + logs: "", + error: null, + status: "starting", + created_at: "2023-11-27T13:35:45.99397566Z", + urls: { + cancel: + "https://api.replicate.com/v1/predictions/heat2o3bzn3ahtr6bjfftvbaci/cancel", + get: "https://api.replicate.com/v1/predictions/heat2o3bzn3ahtr6bjfftvbaci", + }, + }); + const prediction = await client.predictions.create({ + model: "meta/meta-llama-3-70b-instruct", + input: { + prompt: "Please write a haiku about llamas.", + }, + webhook: "http://test.host/webhook", + webhook_events_filter: ["output", "completed"], + }); + expect(prediction.id).toBe("heat2o3bzn3ahtr6bjfftvbaci"); + }); + // Add more tests for error handling, edge cases, etc. + }); + + describe("files.create", () => { + test("Calls the correct API route with the correct payload", async () => { + for (const testCase of fileTestCases) { + nock(BASE_URL) + .post("/files") + .reply(200, { + id: "123", + name: "test-file", + content_type: "application/octet-stream", + size: 1024, + etag: "abc123", + checksum: "sha256:1234567890abcdef", + metadata: {}, + created_at: "2023-01-01T00:00:00Z", + expires_at: null, + urls: { + get: "https://api.replicate.com/v1/files/123", + }, + }); + const file = await client.files.create(testCase.value); + expect(file.id).toBe("123"); + expect(file.name).toBe("test-file"); + } + }); + }); + + describe("files.get", () => { + test("Calls the correct API route", async () => { + nock(BASE_URL) + .get("/files/123") + .reply(200, { + id: "123", + name: "test-file", + content_type: "application/octet-stream", + size: 1024, + etag: "abc123", + checksum: "sha256:1234567890abcdef", + metadata: {}, + created_at: "2023-01-01T00:00:00Z", + expires_at: null, + urls: { + get: "https://api.replicate.com/v1/files/123", + }, + }); + + const file = await client.files.get("123"); + expect(file.id).toBe("123"); + expect(file.name).toBe("test-file"); + }); + }); + + describe("files.list", () => { + test("Calls the correct API route", async () => { + nock(BASE_URL) + .get("/files") + .reply(200, { + next: null, + previous: null, + results: [ + { + id: "123", + name: "test-file", + content_type: "application/octet-stream", + size: 1024, + etag: "abc123", + checksum: "sha256:1234567890abcdef", + metadata: {}, + created_at: "2023-01-01T00:00:00Z", + expires_at: null, + urls: { + get: "https://api.replicate.com/v1/files/123", + }, + }, + ], + }); + + const files = await client.files.list(); + expect(files.results.length).toBe(1); + expect(files.results[0].id).toBe("123"); + }); + }); + + describe("files.delete", () => { + test("Calls the correct API route", async () => { + nock(BASE_URL).delete("/files/123").reply(204); + const success = await client.files.delete("123"); + expect(success).toBe(true); + }); + }); + + describe("hardware.list", () => { + test("Calls the correct API route", async () => { + nock(BASE_URL) + .get("/hardware") + .reply(200, [ + { name: "CPU", sku: "cpu" }, + { name: "Nvidia T4 GPU", sku: "gpu-t4" }, + { name: "Nvidia A40 GPU", sku: "gpu-a40-small" }, + { name: "Nvidia A40 (Large) GPU", sku: "gpu-a40-large" }, + ]); + + const hardware = await client.hardware.list(); + expect(hardware.length).toBe(4); + expect(hardware[0].name).toBe("CPU"); + expect(hardware[0].sku).toBe("cpu"); + }); + // Add more tests for error handling, edge cases, etc. + }); + + describe("models.create", () => { + test("Calls the correct API route with the correct payload", async () => { + nock(BASE_URL).post("/models").reply(200, { + owner: "test-owner", + name: "test-model", + visibility: "public", + hardware: "cpu", + description: "A test model", + }); + + const model = await client.models.create("test-owner", "test-model", { visibility: "public", hardware: "cpu", description: "A test model", @@ -887,69 +1302,166 @@ describe("Replicate client", () => { }); }); - describe("run", () => { - test("Calls the correct API routes for a version", async () => { - const firstPollingRequest = true; + describe("models.search", () => { + test("Calls the correct API route with the correct payload", async () => { + nock(BASE_URL) + .intercept("/models", "QUERY") + .reply(200, { + results: [ + { + url: "https://replicate.com/meta/meta-llama-3-70b-instruct", + owner: "meta", + name: "meta-llama-3-70b-instruct", + description: + "Llama 2 is a collection of pretrained and fine-tuned generative text models ranging in scale from 7 billion to 70 billion parameters.", + visibility: "public", + github_url: null, + paper_url: + "https://ai.meta.com/research/publications/llama-2-open-foundation-and-fine-tuned-chat-models/", + license_url: "https://ai.meta.com/llama/license/", + run_count: 1000000, + cover_image_url: + "https://replicate.delivery/pbxt/IJqFrnAKEDiCBnlXyndzVVxkZvfQ7kLjGVEZZPXTRXxOOPkQA/llama2.png", + default_example: null, + latest_version: null, + }, + // ... more results ... + ], + next: null, + previous: null, + }); + + const searchResults = await client.models.search("llama"); + expect(searchResults.results.length).toBeGreaterThan(0); + expect(searchResults.results[0].owner).toBe("meta"); + expect(searchResults.results[0].name).toBe("meta-llama-3-70b-instruct"); + }); + // Add more tests for error handling, edge cases, etc. + }); + + describe("run", () => { + test("Calls the correct API routes", async () => { nock(BASE_URL) .post("/predictions") .reply(201, { id: "ufawqhfynnddngldkgtslldrkq", status: "starting", + logs: null, + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [ + "Using seed: 12345", + "0%| | 0/5 [00:00 { + const progress = parseProgressFromLogs(prediction); + callback(prediction, progress); + } ); expect(output).toBe("Goodbye!"); - expect(progress).toHaveBeenNthCalledWith(1, { - id: "ufawqhfynnddngldkgtslldrkq", - status: "starting", - }); + expect(callback).toHaveBeenNthCalledWith( + 1, + { + id: "ufawqhfynnddngldkgtslldrkq", + status: "starting", + logs: null, + }, + null + ); - expect(progress).toHaveBeenNthCalledWith(2, { - id: "ufawqhfynnddngldkgtslldrkq", - status: "processing", - }); + expect(callback).toHaveBeenNthCalledWith( + 2, + { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: expect.any(String), + }, + { + percentage: 0.4, + current: 2, + total: 5, + } + ); - expect(progress).toHaveBeenNthCalledWith(3, { - id: "ufawqhfynnddngldkgtslldrkq", - status: "processing", - }); + expect(callback).toHaveBeenNthCalledWith( + 3, + { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: expect.any(String), + }, + { + percentage: 0.8, + current: 4, + total: 5, + } + ); - expect(progress).toHaveBeenNthCalledWith(4, { - id: "ufawqhfynnddngldkgtslldrkq", - status: "succeeded", - output: "Goodbye!", - }); + expect(callback).toHaveBeenNthCalledWith( + 4, + { + id: "ufawqhfynnddngldkgtslldrkq", + status: "succeeded", + logs: expect.any(String), + output: "Goodbye!", + }, + { + percentage: 1.0, + current: 5, + total: 5, + } + ); - expect(progress).toHaveBeenCalledTimes(4); + expect(callback).toHaveBeenCalledTimes(4); }); test("Calls the correct API routes for a model", async () => { - const firstPollingRequest = true; - nock(BASE_URL) .post("/models/replicate/hello-world/predictions") .reply(201, { @@ -975,7 +1487,7 @@ describe("Replicate client", () => { "replicate/hello-world", { input: { text: "Hello, world!" }, - wait: { interval: 1 }, + wait: { mode: "poll", interval: 1 }, }, progress ); @@ -1021,12 +1533,18 @@ describe("Replicate client", () => { }); await expect( - client.run("a/b-1.0:abc123", { input: { text: "Hello, world!" } }) + client.run("a/b-1.0:abc123", { + wait: { mode: "poll" }, + input: { text: "Hello, world!" }, + }) ).resolves.not.toThrow(); }); test("Throws an error for invalid identifiers", async () => { - const options = { input: { text: "Hello, world!" } }; + const options = { + wait: { mode: "poll" } as { mode: "poll" }, + input: { text: "Hello, world!" }, + }; // @ts-expect-error await expect(client.run("owner:abc123", options)).rejects.toThrow(); @@ -1042,6 +1560,7 @@ describe("Replicate client", () => { await client.run( "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", { + wait: { mode: "poll" }, input: { text: "Alice", }, @@ -1054,15 +1573,18 @@ describe("Replicate client", () => { test("Aborts the operation when abort signal is invoked", async () => { const controller = new AbortController(); const { signal } = controller; + let body: Record | undefined; const scope = nock(BASE_URL) - .post("/predictions", (body) => { + .post("/predictions", (_body) => { + // Should not pass the signal object in the body. + body = _body; controller.abort(); - return body; + return _body; }) .reply(201, { id: "ufawqhfynnddngldkgtslldrkq", - status: "processing", + status: "starting", }) .persist() .get("/predictions/ufawqhfynnddngldkgtslldrkq") @@ -1076,18 +1598,244 @@ describe("Replicate client", () => { status: "canceled", }); - await client.run( + const onProgress = jest.fn(); + const output = await client.run( "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", { + wait: { mode: "poll" }, input: { text: "Hello, world!" }, signal, - } + }, + onProgress ); + expect(body).toBeDefined(); + expect(body?.["signal"]).toBeUndefined(); expect(signal.aborted).toBe(true); + expect(output).toBeUndefined(); + + expect(onProgress).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + status: "starting", + }) + ); + expect(onProgress).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + status: "processing", + }) + ); + expect(onProgress).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + status: "canceled", + }) + ); scope.done(); }); + + test("returns FileOutput for URLs when useFileOutput is true", async () => { + client = new Replicate({ auth: "foo", useFileOutput: true }); + + nock(BASE_URL) + .post("/predictions") + .reply(201, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "starting", + logs: null, + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "succeeded", + output: "https://example.com", + logs: [].join("\n"), + }); + + nock("https://example.com") + .get("/") + .reply(200, "hello world", { "Content-Type": "text/plain" }); + + const output = (await client.run( + "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + { + wait: { mode: "poll" }, + input: { text: "Hello, world!" }, + } + )) as FileOutput; + + expect(output).toBeInstanceOf(ReadableStream); + expect(output.url()).toEqual(new URL("https://example.com")); + + const blob = await output.blob(); + expect(blob.type).toEqual("text/plain"); + expect(blob.arrayBuffer()).toEqual( + new Blob(["Hello, world!"]).arrayBuffer() + ); + }); + + test("returns FileOutput for URLs when useFileOutput is true - acts like string", async () => { + client = new Replicate({ auth: "foo", useFileOutput: true }); + + nock(BASE_URL) + .post("/predictions") + .reply(201, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "starting", + logs: null, + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "succeeded", + output: "https://example.com", + logs: [].join("\n"), + }); + + nock("https://example.com") + .get("/") + .reply(200, "hello world", { "Content-Type": "text/plain" }); + + const output = (await client.run( + "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + { + wait: { mode: "poll" }, + input: { text: "Hello, world!" }, + } + )) as unknown as string; + + expect(fetch(output).then((r) => r.text())).resolves.toEqual( + "hello world" + ); + }); + + test("returns FileOutput for URLs when useFileOutput is true - array output", async () => { + client = new Replicate({ auth: "foo", useFileOutput: true }); + + nock(BASE_URL) + .post("/predictions") + .reply(201, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "starting", + logs: null, + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "succeeded", + output: ["https://example.com"], + logs: [].join("\n"), + }); + + nock("https://example.com") + .get("/") + .reply(200, "hello world", { "Content-Type": "text/plain" }); + + const [output] = (await client.run( + "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + { + wait: { mode: "poll" }, + input: { text: "Hello, world!" }, + } + )) as FileOutput[]; + + expect(output).toBeInstanceOf(ReadableStream); + expect(output.url()).toEqual(new URL("https://example.com")); + + const blob = await output.blob(); + expect(blob.type).toEqual("text/plain"); + expect(blob.arrayBuffer()).toEqual( + new Blob(["Hello, world!"]).arrayBuffer() + ); + }); + + test("returns FileOutput for URLs when useFileOutput is true - data uri", async () => { + client = new Replicate({ auth: "foo", useFileOutput: true }); + + nock(BASE_URL) + .post("/predictions") + .reply(201, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "starting", + logs: null, + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "processing", + logs: [].join("\n"), + }) + .get("/predictions/ufawqhfynnddngldkgtslldrkq") + .reply(200, { + id: "ufawqhfynnddngldkgtslldrkq", + status: "succeeded", + output: "data:text/plain;base64,SGVsbG8sIHdvcmxkIQ==", + logs: [].join("\n"), + }); + + const output = (await client.run( + "owner/model:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + { + wait: { mode: "poll" }, + input: { text: "Hello, world!" }, + } + )) as FileOutput; + + expect(output).toBeInstanceOf(ReadableStream); + expect(output.url()).toEqual( + new URL("data:text/plain;base64,SGVsbG8sIHdvcmxkIQ==") + ); + + const blob = await output.blob(); + expect(blob.type).toEqual("text/plain"); + expect(blob.arrayBuffer()).toEqual( + new Blob(["Hello, world!"]).arrayBuffer() + ); + }); }); describe("webhooks.default.secret.get", () => { @@ -1116,13 +1864,334 @@ describe("Replicate client", () => { // This is a test secret and should not be used in production const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"; + if (globalThis.crypto) { + const isValid = await validateWebhook(request, secret); + expect(isValid).toBe(true); + } else { + const isValid = await validateWebhook( + request, + secret, + webcrypto as Crypto // Node 18 requires this to be passed manually + ); + expect(isValid).toBe(true); + } + }); - const isValid = await validateWebhook(request, secret); - expect(isValid).toBe(true); + test("Can be used to validate webhook", async () => { + // Test case from https://github.com/svix/svix-webhooks/blob/b41728cd98a7e7004a6407a623f43977b82fcba4/javascript/src/webhook.test.ts#L190-L200 + const requestData = { + id: "msg_p5jXN8AQM9LWM0D4loKWxJek", + timestamp: "1614265330", + signature: "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=", + body: `{"test": 2432232314}`, + secret: "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw", + }; + + // This is a test secret and should not be used in production + if (globalThis.crypto) { + const isValid = await validateWebhook(requestData); + expect(isValid).toBe(true); + } else { + const isValid = await validateWebhook( + requestData, + webcrypto as Crypto // Node 18 requires this to be passed manually + ); + expect(isValid).toBe(true); + } }); // Add more tests for error handling, edge cases, etc. }); // Continue with tests for other methods + + describe("createReadableStream", () => { + function createStream(body: string | ReadableStream, status = 200) { + const streamEndpoint = "https://stream.replicate.com/fake_stream"; + const fetch = jest.fn((url) => { + if (url !== streamEndpoint) { + throw new Error(`Unmocked call to fetch() with url: ${url}`); + } + return new Response(body, { status }); + }); + return createReadableStream({ + url: streamEndpoint, + fetch: fetch as any, + }); + } + + test("consumes a server sent event stream", async () => { + const stream = createStream( + ` + event: output + id: EVENT_1 + data: hello world + + event: done + id: EVENT_2 + data: {} + + `.replace(/^[ ]+/gm, "") + ); + + const iterator = stream[Symbol.asyncIterator](); + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "output", id: "EVENT_1", data: "hello world" }, + }); + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "done", id: "EVENT_2", data: "{}" }, + }); + expect(await iterator.next()).toEqual({ done: true }); + expect(await iterator.next()).toEqual({ done: true }); + }); + + test("consumes multiple events", async () => { + const stream = createStream( + ` + event: output + id: EVENT_1 + data: hello world + + event: output + id: EVENT_2 + data: hello dave + + event: done + id: EVENT_3 + data: {} + + `.replace(/^[ ]+/gm, "") + ); + + const iterator = stream[Symbol.asyncIterator](); + + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "output", id: "EVENT_1", data: "hello world" }, + }); + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "output", id: "EVENT_2", data: "hello dave" }, + }); + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "done", id: "EVENT_3", data: "{}" }, + }); + expect(await iterator.next()).toEqual({ done: true }); + expect(await iterator.next()).toEqual({ done: true }); + }); + + test("ignores unexpected characters", async () => { + const stream = createStream( + ` + : hi + + event: output + id: EVENT_1 + data: hello world + + event: done + id: EVENT_2 + data: {} + + `.replace(/^[ ]+/gm, "") + ); + + const iterator = stream[Symbol.asyncIterator](); + + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "output", id: "EVENT_1", data: "hello world" }, + }); + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "done", id: "EVENT_2", data: "{}" }, + }); + expect(await iterator.next()).toEqual({ done: true }); + expect(await iterator.next()).toEqual({ done: true }); + }); + + test("supports multiple lines of output in a single event", async () => { + const stream = createStream( + ` + : hi + + event: output + id: EVENT_1 + data: hello, + data: this is a new line, + data: and this is a new line too + + event: done + id: EVENT_2 + data: {} + + `.replace(/^[ ]+/gm, "") + ); + + const iterator = stream[Symbol.asyncIterator](); + + expect(await iterator.next()).toEqual({ + done: false, + value: { + event: "output", + id: "EVENT_1", + data: "hello,\nthis is a new line,\nand this is a new line too", + }, + }); + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "done", id: "EVENT_2", data: "{}" }, + }); + expect(await iterator.next()).toEqual({ done: true }); + expect(await iterator.next()).toEqual({ done: true }); + }); + + test("supports the server writing data lines in multiple chunks", async () => { + // Create a stream of data chunks split on the pipe character for readability. + const data = ` + event: output + id: EVENT_1 + data: hello,| + data: this is a new line,| + data: and this is a new line too + + event: done + id: EVENT_2 + data: {} + + `.replace(/^[ ]+/gm, ""); + + const chunks = data.split("|"); + const body = new ReadableStream({ + async pull(controller) { + if (chunks.length) { + await new Promise((resolve) => setTimeout(resolve, 1)); + const chunk = chunks.shift(); + controller.enqueue(new TextEncoder().encode(chunk)); + } + }, + }); + + const stream = createStream(body); + + // Consume the iterator in parallel to writing it. + const iterator = stream[Symbol.asyncIterator](); + expect(await iterator.next()).toEqual({ + done: false, + value: { + event: "output", + id: "EVENT_1", + data: "hello,\nthis is a new line,\nand this is a new line too", + }, + }); + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "done", id: "EVENT_2", data: "{}" }, + }); + expect(await iterator.next()).toEqual({ done: true }); + + // Wait for both promises to resolve. + }); + + test("supports the server writing data in a complete mess", async () => { + const body = new ReadableStream({ + async pull(controller) { + if (chunks.length) { + await new Promise((resolve) => setTimeout(resolve, 1)); + const chunk = chunks.shift(); + controller.enqueue(new TextEncoder().encode(chunk)); + } + }, + }); + const stream = createStream(body); + + // Create a stream of data chunks split on the pipe character for readability. + const data = ` + : hi + + ev|ent: output + id: EVENT_1 + data: hello, + data: this |is a new line,| + data: and this is |a new line too + + event: d|one + id: EVENT|_2 + data: {} + + `.replace(/^[ ]+/gm, ""); + + const chunks = data.split("|"); + + const iterator = stream[Symbol.asyncIterator](); + expect(await iterator.next()).toEqual({ + done: false, + value: { + event: "output", + id: "EVENT_1", + data: "hello,\nthis is a new line,\nand this is a new line too", + }, + }); + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "done", id: "EVENT_2", data: "{}" }, + }); + expect(await iterator.next()).toEqual({ done: true }); + }); + + test("supports ending without a done", async () => { + const stream = createStream( + ` + event: output + id: EVENT_1 + data: hello world + + `.replace(/^[ ]+/gm, "") + ); + + const iterator = stream[Symbol.asyncIterator](); + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "output", id: "EVENT_1", data: "hello world" }, + }); + expect(await iterator.next()).toEqual({ done: true }); + }); + + test("an error event in the stream raises an exception", async () => { + const stream = createStream( + ` + event: output + id: EVENT_1 + data: hello world + + event: error + id: EVENT_2 + data: An unexpected error occurred + + `.replace(/^[ ]+/gm, "") + ); + + const iterator = stream[Symbol.asyncIterator](); + expect(await iterator.next()).toEqual({ + done: false, + value: { event: "output", id: "EVENT_1", data: "hello world" }, + }); + await expect(iterator.next()).rejects.toThrowError( + "An unexpected error occurred" + ); + expect(await iterator.next()).toEqual({ done: true }); + }); + + test("an error when fetching the stream raises an exception", async () => { + const stream = createStream("{}", 500); + const iterator = stream[Symbol.asyncIterator](); + await expect(iterator.next()).rejects.toThrowError( + "Request to https://stream.replicate.com/fake_stream failed with status 500" + ); + expect(await iterator.next()).toEqual({ done: true }); + }); + }); }); diff --git a/integration/browser/.npmrc b/integration/browser/.npmrc new file mode 100644 index 00000000..77750405 --- /dev/null +++ b/integration/browser/.npmrc @@ -0,0 +1,3 @@ +package-lock=false +audit=false +fund=false diff --git a/integration/browser/README.md b/integration/browser/README.md new file mode 100644 index 00000000..575d779a --- /dev/null +++ b/integration/browser/README.md @@ -0,0 +1,39 @@ +# Browser integration tests + +Uses [`playwright`](https://playwright.dev/docs) to run a basic integration test against the three most common browser engines, Firefox, Chromium and WebKit. + +It uses the `replicate/canary` model for the moment, which requires a Replicate API token available in the environment under `REPLICATE_API_TOKEN`. + +The entire suite is a single `main()` function that calls a single model exercising the streaming API. + +The test uses `esbuild` within the test generate a browser friendly version of the `index.js` file which is loaded into the given browser and calls the `main()` function asserting the response content. + +## CORS + +The Replicate API doesn't support Cross Origin Resource Sharing at this time. We work around this in Playwright by intercepting the request in a `page.route` handler. We don't modify the request/response, but this seems to work around the restriction. + +## Setup + + npm install + +## Local + +The following command will run the tests across all browsers. + + npm test + +To run against the default browser (chromium) run: + + npm exec playwright test + +Or, specify a browser with: + + npm exec playwright test --browser firefox + +## Debugging + +Running `playwright test` with the `--debug` flag opens a browser window with a debugging interface, and a breakpoint set at the start of the test. It can also be connected directly to VSCode. + + npm exec playwright test --debug + +The browser.js file is injected into the page via a script tag, to be able to set breakpoints in this file you'll need to use a `debugger` statement and open the devtools in the spawned browser window before continuing the test suite. diff --git a/integration/browser/index.js b/integration/browser/index.js new file mode 100644 index 00000000..9c0ae654 --- /dev/null +++ b/integration/browser/index.js @@ -0,0 +1,22 @@ +import Replicate from "replicate"; + +/** + * @param {string} - token the REPLICATE_API_TOKEN + */ +window.main = async (token) => { + const replicate = new Replicate({ auth: token }); + const stream = replicate.stream( + "replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272", + { + input: { + text: "Betty Browser", + }, + } + ); + + const output = []; + for await (const event of stream) { + output.push(String(event)); + } + return output.join(""); +}; diff --git a/integration/browser/index.test.js b/integration/browser/index.test.js new file mode 100644 index 00000000..380d8138 --- /dev/null +++ b/integration/browser/index.test.js @@ -0,0 +1,34 @@ +import { test, expect } from "@playwright/test"; +import { build } from "esbuild"; + +// Convert the source file from commonjs to a browser script. +const result = await build({ + entryPoints: ["index.js"], + bundle: true, + platform: "browser", + external: ["node:crypto"], + write: false, +}); +const source = new TextDecoder().decode(result.outputFiles[0].contents); + +// https://playwright.dev/docs/network#modify-requests + +test("browser", async ({ page }) => { + // Patch the API endpoint to work around CORS for now. + await page.route( + "https://api.replicate.com/v1/predictions", + async (route) => { + // Fetch original response. + const response = await route.fetch(); + // Add a prefix to the title. + return route.fulfill({ response }); + } + ); + + await page.addScriptTag({ content: source }); + const result = await page.evaluate( + (token) => window.main(token), + [process.env.REPLICATE_API_TOKEN] + ); + expect(result).toBe("hello there, Betty Browser"); +}); diff --git a/integration/browser/package.json b/integration/browser/package.json new file mode 100644 index 00000000..91ba1791 --- /dev/null +++ b/integration/browser/package.json @@ -0,0 +1,19 @@ +{ + "name": "replicate-app-browser", + "private": true, + "version": "0.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "playwright test --browser all" + }, + "license": "ISC", + "dependencies": { + "replicate": "../../" + }, + "devDependencies": { + "@playwright/test": "^1.42.1", + "esbuild": "^0.20.1" + } +} diff --git a/integration/browser/playwright.config.ts b/integration/browser/playwright.config.ts new file mode 100644 index 00000000..142a1777 --- /dev/null +++ b/integration/browser/playwright.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({}); diff --git a/integration/bun/.gitignore b/integration/bun/.gitignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/integration/bun/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/integration/bun/bun.lockb b/integration/bun/bun.lockb new file mode 100755 index 00000000..dd45eaf0 Binary files /dev/null and b/integration/bun/bun.lockb differ diff --git a/integration/bun/index.test.ts b/integration/bun/index.test.ts new file mode 100644 index 00000000..f0665a6d --- /dev/null +++ b/integration/bun/index.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from "bun:test"; +import main from "./index.js"; + +// Verify exported types. +import type { + Status, + Visibility, + WebhookEventType, + ApiError, + Collection, + Hardware, + Model, + ModelVersion, + Prediction, + Training, + Page, + ServerSentEvent, +} from "replicate"; + +test("main", async () => { + const output = await main(); + expect(output).toEqual("hello there, Brünnhilde Bun"); +}); diff --git a/integration/bun/index.ts b/integration/bun/index.ts new file mode 100644 index 00000000..853cc286 --- /dev/null +++ b/integration/bun/index.ts @@ -0,0 +1,25 @@ +import Replicate from "replicate"; + +const replicate = new Replicate({ + auth: process.env.REPLICATE_API_TOKEN, +}); + +export default async function main() { + const model = + "replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272"; + const options = { + input: { + text: "Brünnhilde Bun", + }, + }; + const output = []; + + for await (const { event, data } of replicate.stream(model, options)) { + console.log({ event, data }); + if (event === "output") { + output.push(data); + } + } + + return output.join("").trim(); +} diff --git a/integration/bun/package.json b/integration/bun/package.json new file mode 100644 index 00000000..f4ba2029 --- /dev/null +++ b/integration/bun/package.json @@ -0,0 +1,14 @@ +{ + "name": "replicate-app-bun", + "version": "0.0.0", + "private": true, + "description": "Bun integration tests", + "main": "index.js", + "type": "module", + "dependencies": { + "replicate": "file:../../" + }, + "devDependencies": { + "@types/bun": "latest" + } +} diff --git a/integration/bun/tsconfig.json b/integration/bun/tsconfig.json new file mode 100644 index 00000000..2ffa937f --- /dev/null +++ b/integration/bun/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "nodenext", + "inlineSourceMap": true, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true + } +} diff --git a/integration/cloudflare-worker/.gitignore b/integration/cloudflare-worker/.gitignore new file mode 100644 index 00000000..8394e05f --- /dev/null +++ b/integration/cloudflare-worker/.gitignore @@ -0,0 +1,4 @@ +.wrangler +node_modules +.dev.vars + diff --git a/integration/cloudflare-worker/.npmrc b/integration/cloudflare-worker/.npmrc new file mode 100644 index 00000000..77750405 --- /dev/null +++ b/integration/cloudflare-worker/.npmrc @@ -0,0 +1,3 @@ +package-lock=false +audit=false +fund=false diff --git a/integration/cloudflare-worker/index.js b/integration/cloudflare-worker/index.js new file mode 100644 index 00000000..32ec9fc8 --- /dev/null +++ b/integration/cloudflare-worker/index.js @@ -0,0 +1,33 @@ +import Replicate from "replicate"; + +export default { + async fetch(_request, env, _ctx) { + const replicate = new Replicate({ auth: env.REPLICATE_API_TOKEN }); + + try { + const controller = new AbortController(); + const output = replicate.stream( + "replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272", + { + input: { + text: "Colin CloudFlare", + }, + signal: controller.signal, + } + ); + const stream = new ReadableStream({ + async start(controller) { + for await (const event of output) { + controller.enqueue(new TextEncoder().encode(`${event}`)); + } + controller.enqueue(new TextEncoder().encode("\n")); + controller.close(); + }, + }); + + return new Response(stream); + } catch (err) { + return Response("", { status: 500 }); + } + }, +}; diff --git a/integration/cloudflare-worker/index.test.js b/integration/cloudflare-worker/index.test.js new file mode 100644 index 00000000..932d8f5d --- /dev/null +++ b/integration/cloudflare-worker/index.test.js @@ -0,0 +1,41 @@ +// https://developers.cloudflare.com/workers/wrangler/api/#unstable_dev +import { unstable_dev as dev } from "wrangler"; +import { test, after, before, describe } from "node:test"; +import assert from "node:assert"; + +describe("CloudFlare Worker", () => { + /** @type {import("wrangler").UnstableDevWorker} */ + let worker; + + before(async () => { + worker = await dev("./index.js", { + port: 3000, + experimental: { disableExperimentalWarning: true }, + }); + }); + + after(async () => { + if (!worker) { + // If no worker the before hook failed to run and the process will hang. + process.exit(1); + } + await worker.stop(); + }); + + test("worker streams back a response", { timeout: 5000 }, async () => { + const resp = await worker.fetch(); + const text = await resp.text(); + + assert.ok(resp.ok, `expected status to be 2xx but got ${resp.status}`); + assert( + text.length > 0, + "expected body to have content but got body.length of 0" + ); + assert( + text.includes("Colin CloudFlare"), + `expected body to include "Colin CloudFlare" but got ${JSON.stringify( + text + )}` + ); + }); +}); diff --git a/integration/cloudflare-worker/package.json b/integration/cloudflare-worker/package.json new file mode 100644 index 00000000..94ae94c1 --- /dev/null +++ b/integration/cloudflare-worker/package.json @@ -0,0 +1,18 @@ +{ + "name": "replicate-app-cloudflare", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "start": "wrangler dev", + "test": "node --test index.test.js" + }, + "dependencies": { + "replicate": "file:../../" + }, + "devDependencies": { + "wrangler": "^3.32.0" + } +} diff --git a/integration/cloudflare-worker/wrangler.toml b/integration/cloudflare-worker/wrangler.toml new file mode 100644 index 00000000..4fbf2093 --- /dev/null +++ b/integration/cloudflare-worker/wrangler.toml @@ -0,0 +1,4 @@ +name = "cloudflare-worker" +main = "index.js" +compatibility_date = "2024-03-04" +compatibility_flags = [ "nodejs_compat" ] diff --git a/integration/commonjs/.npmrc b/integration/commonjs/.npmrc new file mode 100644 index 00000000..77750405 --- /dev/null +++ b/integration/commonjs/.npmrc @@ -0,0 +1,3 @@ +package-lock=false +audit=false +fund=false diff --git a/integration/commonjs/index.js b/integration/commonjs/index.js index abe859a2..6d8c436b 100644 --- a/integration/commonjs/index.js +++ b/integration/commonjs/index.js @@ -5,12 +5,13 @@ const replicate = new Replicate({ }); module.exports = async function main() { - return await replicate.run( - "replicate/hello-world:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + const output = await replicate.run( + "replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272", { input: { - text: "Claire CommonJS" - } + text: "Claire CommonJS", + }, } ); + return output.join("").trim(); }; diff --git a/integration/commonjs/index.test.js b/integration/commonjs/index.test.js index 5ef7b63c..afc3c014 100644 --- a/integration/commonjs/index.test.js +++ b/integration/commonjs/index.test.js @@ -1,8 +1,8 @@ -const { test } = require('node:test'); -const assert = require('node:assert'); -const main = require('./index'); +const { test } = require("node:test"); +const assert = require("node:assert"); +const main = require("./index"); -test('main', async () => { +test("main", async () => { const output = await main(); - assert.equal(output, "hello Claire CommonJS"); + assert.equal(output, "hello there, Claire CommonJS"); }); diff --git a/integration/commonjs/package-lock.json b/integration/commonjs/package-lock.json deleted file mode 100644 index 1584af5e..00000000 --- a/integration/commonjs/package-lock.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "replicate-app-commonjs", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "replicate-app-commonjs", - "version": "0.0.0", - "dependencies": { - "replicate": "file:../../" - } - }, - "../..": { - "version": "0.25.2", - "license": "Apache-2.0", - "devDependencies": { - "@biomejs/biome": "^1.4.1", - "@types/jest": "^29.5.3", - "@typescript-eslint/eslint-plugin": "^5.56.0", - "cross-fetch": "^3.1.5", - "jest": "^29.6.2", - "nock": "^13.3.0", - "ts-jest": "^29.1.0", - "typescript": "^5.0.2" - }, - "engines": { - "git": ">=2.11.0", - "node": ">=18.0.0", - "npm": ">=7.19.0", - "yarn": ">=1.7.0" - }, - "optionalDependencies": { - "readable-stream": ">=4.0.0" - } - }, - "node_modules/replicate": { - "resolved": "../..", - "link": true - } - } -} diff --git a/integration/deno/deno.json b/integration/deno/deno.json new file mode 100644 index 00000000..6b3e0a05 --- /dev/null +++ b/integration/deno/deno.json @@ -0,0 +1,5 @@ +{ + "imports": { + "replicate": "npm:replicate" + } +} diff --git a/integration/deno/deno.lock b/integration/deno/deno.lock new file mode 100644 index 00000000..057d6122 --- /dev/null +++ b/integration/deno/deno.lock @@ -0,0 +1,88 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@std/assert": "jsr:@std/assert@0.226.0", + "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.1", + "npm:replicate": "npm:replicate@0.31.0" + }, + "jsr": { + "@std/assert@0.226.0": { + "integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3", + "dependencies": [ + "jsr:@std/internal@^1.0.0" + ] + }, + "@std/internal@1.0.1": { + "integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6" + } + }, + "npm": { + "abort-controller@3.0.0": { + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "event-target-shim@5.0.1" + } + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dependencies": {} + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": { + "base64-js": "base64-js@1.5.1", + "ieee754": "ieee754@1.2.1" + } + }, + "event-target-shim@5.0.1": { + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dependencies": {} + }, + "events@3.3.0": { + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dependencies": {} + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dependencies": {} + }, + "process@0.11.10": { + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dependencies": {} + }, + "readable-stream@4.5.2": { + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "abort-controller@3.0.0", + "buffer": "buffer@6.0.3", + "events": "events@3.3.0", + "process": "process@0.11.10", + "string_decoder": "string_decoder@1.3.0" + } + }, + "replicate@0.31.0": { + "integrity": "sha512-BQl52LqndfY2sLQ384jyspaJI5ia301+IN1zOBbKZa2dB5EnayUxS0ynFueOdwo/4qRfQTR0GKJwpKFK/mb3zw==", + "dependencies": { + "readable-stream": "readable-stream@4.5.2" + } + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dependencies": {} + }, + "string_decoder@1.3.0": { + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + } + } + }, + "remote": {}, + "workspace": { + "dependencies": [ + "npm:replicate" + ] + } +} diff --git a/integration/deno/index.test.ts b/integration/deno/index.test.ts new file mode 100644 index 00000000..e944d770 --- /dev/null +++ b/integration/deno/index.test.ts @@ -0,0 +1,26 @@ +import { assertEquals } from "jsr:@std/assert"; +import main from "./index.ts"; + +// Verify exported types. +import type { + Status, + Visibility, + WebhookEventType, + ApiError, + Collection, + Hardware, + Model, + ModelVersion, + Prediction, + Training, + Page, + ServerSentEvent, +} from "replicate"; + +Deno.test({ + name: "main", + async fn() { + const output = await main(); + assertEquals({ output }, { output: "hello there, Deno the dinosaur" }); + }, +}); diff --git a/integration/deno/index.ts b/integration/deno/index.ts new file mode 100644 index 00000000..a235699b --- /dev/null +++ b/integration/deno/index.ts @@ -0,0 +1,17 @@ +import Replicate from "replicate"; + +const replicate = new Replicate({ + auth: Deno.env.get("REPLICATE_API_TOKEN"), +}); + +export default async function main() { + const output = (await replicate.run( + "replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272", + { + input: { + text: "Deno the dinosaur", + }, + } + )) as string[]; + return output.join("").trim(); +} diff --git a/integration/deno/package.json b/integration/deno/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/integration/deno/package.json @@ -0,0 +1 @@ +{} diff --git a/integration/esm/.npmrc b/integration/esm/.npmrc new file mode 100644 index 00000000..77750405 --- /dev/null +++ b/integration/esm/.npmrc @@ -0,0 +1,3 @@ +package-lock=false +audit=false +fund=false diff --git a/integration/esm/index.js b/integration/esm/index.js index 547b726f..48feccb7 100644 --- a/integration/esm/index.js +++ b/integration/esm/index.js @@ -5,12 +5,13 @@ const replicate = new Replicate({ }); export default async function main() { - return await replicate.run( - "replicate/hello-world:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + const output = await replicate.run( + "replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272", { input: { - text: "Evelyn ESM" - } + text: "Evelyn ESM", + }, } ); -}; + return Array.isArray(output) ? output.join("").trim() : String(output).trim(); +} diff --git a/integration/esm/index.test.js b/integration/esm/index.test.js index 2bd276fd..c95ab8b0 100644 --- a/integration/esm/index.test.js +++ b/integration/esm/index.test.js @@ -1,8 +1,8 @@ -import { test } from 'node:test'; -import assert from 'node:assert'; -import main from './index.js'; +import { test } from "node:test"; +import assert from "node:assert"; +import main from "./index.js"; -test('main', async () => { +test("main", async () => { const output = await main(); - assert.equal(output, "hello Evelyn ESM"); + assert.equal(output, "hello there, Evelyn ESM"); }); diff --git a/integration/esm/package-lock.json b/integration/esm/package-lock.json deleted file mode 100644 index 2a17c888..00000000 --- a/integration/esm/package-lock.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "replicate-app-esm", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "replicate-app-esm", - "version": "0.0.0", - "dependencies": { - "replicate": "file:../../" - } - }, - "../..": { - "name": "replicate", - "version": "0.25.2", - "license": "Apache-2.0", - "devDependencies": { - "@biomejs/biome": "^1.4.1", - "@types/jest": "^29.5.3", - "@typescript-eslint/eslint-plugin": "^5.56.0", - "cross-fetch": "^3.1.5", - "jest": "^29.6.2", - "nock": "^13.3.0", - "ts-jest": "^29.1.0", - "typescript": "^5.0.2" - }, - "engines": { - "git": ">=2.11.0", - "node": ">=18.0.0", - "npm": ">=7.19.0", - "yarn": ">=1.7.0" - }, - "optionalDependencies": { - "readable-stream": ">=4.0.0" - } - }, - "node_modules/replicate": { - "resolved": "../..", - "link": true - } - } -} diff --git a/integration/next/.npmrc b/integration/next/.npmrc new file mode 100644 index 00000000..77750405 --- /dev/null +++ b/integration/next/.npmrc @@ -0,0 +1,3 @@ +package-lock=false +audit=false +fund=false diff --git a/integration/next/middleware.ts b/integration/next/middleware.ts new file mode 100644 index 00000000..090adfd4 --- /dev/null +++ b/integration/next/middleware.ts @@ -0,0 +1,17 @@ +// NOTE: This file currently doesn't do anything other than +// validate that `next build` works as expected. We can +// extend it in future to support actual middleware tests. +import { NextRequest } from "next/server"; +import Replicate from "replicate"; + +// Limit the middleware to paths starting with `/api/` +export const config = { + matcher: "/api/:function*", +}; + +const replicate = new Replicate(); + +export function middleware(request: NextRequest) { + const output = replicate.run("foo/bar"); + return Response.json({ output }, 200); +} diff --git a/integration/next/package.json b/integration/next/package.json new file mode 100644 index 00000000..6101a71b --- /dev/null +++ b/integration/next/package.json @@ -0,0 +1,14 @@ +{ + "name": "replicate-next", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "next", + "build": "rm -rf .next && next build", + "start": "next start" + }, + "dependencies": { + "next": "^14.2.3", + "replicate": "../../" + } +} diff --git a/integration/next/pages/index.js b/integration/next/pages/index.js new file mode 100644 index 00000000..09124388 --- /dev/null +++ b/integration/next/pages/index.js @@ -0,0 +1,5 @@ +export default () => ( +
+

Welcome to Next.js

+
+); diff --git a/integration/typescript/.npmrc b/integration/typescript/.npmrc new file mode 100644 index 00000000..77750405 --- /dev/null +++ b/integration/typescript/.npmrc @@ -0,0 +1,3 @@ +package-lock=false +audit=false +fund=false diff --git a/integration/typescript/index.test.ts b/integration/typescript/index.test.ts index be4ab90c..0f0f356b 100644 --- a/integration/typescript/index.test.ts +++ b/integration/typescript/index.test.ts @@ -1,9 +1,9 @@ -import { test } from 'node:test'; -import assert from 'node:assert'; -import main from './index.js'; +import { test } from "node:test"; +import assert from "node:assert"; +import main from "./index.js"; // Verify exported types. -import type { +import type { Status, Visibility, WebhookEventType, @@ -18,7 +18,7 @@ import type { ServerSentEvent, } from "replicate"; -test('main', async () => { +test("main", async () => { const output = await main(); - assert.equal(output, "hello Tracy TypeScript"); -}); + assert.equal(output, "hello there, Tracy TypeScript"); +}); diff --git a/integration/typescript/index.ts b/integration/typescript/index.ts index 8e27a3ba..1b276bc9 100644 --- a/integration/typescript/index.ts +++ b/integration/typescript/index.ts @@ -5,12 +5,13 @@ const replicate = new Replicate({ }); export default async function main() { - return await replicate.run( - "replicate/hello-world:5c7d5dc6dd8bf75c1acaa8565735e7986bc5b66206b55cca93cb72c9bf15ccaa", + const output = (await replicate.run( + "replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272", { input: { - text: "Tracy TypeScript" - } + text: "Tracy TypeScript", + }, } - ); -}; + )) as string[]; + return output.join("").trim(); +} diff --git a/integration/typescript/package-lock.json b/integration/typescript/package-lock.json deleted file mode 100644 index f309b1b1..00000000 --- a/integration/typescript/package-lock.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "name": "replicate-app-esm", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "replicate-app-esm", - "version": "0.0.0", - "dependencies": { - "@types/node": "^20.11.0", - "replicate": "file:../../", - "typescript": "^5.3.3" - } - }, - "../..": { - "name": "replicate", - "version": "0.25.2", - "license": "Apache-2.0", - "devDependencies": { - "@biomejs/biome": "^1.4.1", - "@types/jest": "^29.5.3", - "@typescript-eslint/eslint-plugin": "^5.56.0", - "cross-fetch": "^3.1.5", - "jest": "^29.6.2", - "nock": "^13.3.0", - "ts-jest": "^29.1.0", - "typescript": "^5.0.2" - }, - "engines": { - "git": ">=2.11.0", - "node": ">=18.0.0", - "npm": ">=7.19.0", - "yarn": ">=1.7.0" - }, - "optionalDependencies": { - "readable-stream": ">=4.0.0" - } - }, - "node_modules/@types/node": { - "version": "20.11.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz", - "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/replicate": { - "resolved": "../..", - "link": true - }, - "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - } - } -} diff --git a/lib/accounts.js b/lib/accounts.js index b3bbd9fe..72a94afa 100644 --- a/lib/accounts.js +++ b/lib/accounts.js @@ -1,11 +1,14 @@ /** * Get the current account * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the current account */ -async function getCurrentAccount() { +async function getCurrentAccount({ signal } = {}) { const response = await this.request("/account", { method: "GET", + signal, }); return response.json(); diff --git a/lib/collections.js b/lib/collections.js index 9332aaab..7b8e8f11 100644 --- a/lib/collections.js +++ b/lib/collections.js @@ -2,11 +2,14 @@ * Fetch a model collection * * @param {string} collection_slug - Required. The slug of the collection. See http://replicate.com/collections + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with the collection data */ -async function getCollection(collection_slug) { +async function getCollection(collection_slug, { signal } = {}) { const response = await this.request(`/collections/${collection_slug}`, { method: "GET", + signal, }); return response.json(); @@ -15,11 +18,14 @@ async function getCollection(collection_slug) { /** * Fetch a list of model collections * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with the collections data */ -async function listCollections() { +async function listCollections({ signal } = {}) { const response = await this.request("/collections", { method: "GET", + signal, }); return response.json(); diff --git a/lib/deployments.js b/lib/deployments.js index 3e1ceebd..d45c4f39 100644 --- a/lib/deployments.js +++ b/lib/deployments.js @@ -7,13 +7,14 @@ const { transformFileInputs } = require("./util"); * @param {string} deployment_name - Required. The name of the deployment * @param {object} options * @param {object} options.input - Required. An object with the model inputs - * @param {boolean} [options.stream] - Whether to stream the prediction output. Defaults to false * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the prediction has new output * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) + * @param {boolean|integer} [options.wait] - Whether to wait until the prediction is completed before returning. If an integer is provided, it will wait for that many seconds. Defaults to false + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the created prediction data */ async function createPrediction(deployment_owner, deployment_name, options) { - const { stream, input, ...data } = options; + const { input, wait, signal, ...data } = options; if (data.webhook) { try { @@ -24,15 +25,30 @@ async function createPrediction(deployment_owner, deployment_name, options) { } } + const headers = {}; + if (wait) { + if (typeof wait === "number") { + const n = Math.max(1, Math.ceil(Number(wait)) || 1); + headers["Prefer"] = `wait=${n}`; + } else { + headers["Prefer"] = "wait"; + } + } + const response = await this.request( `/deployments/${deployment_owner}/${deployment_name}/predictions`, { method: "POST", + headers, data: { ...data, - input: await transformFileInputs(input), - stream, + input: await transformFileInputs( + this, + input, + this.fileEncodingStrategy + ), }, + signal, } ); @@ -44,16 +60,128 @@ async function createPrediction(deployment_owner, deployment_name, options) { * * @param {string} deployment_owner - Required. The username of the user or organization who owns the deployment * @param {string} deployment_name - Required. The name of the deployment + * @param {object] [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the deployment data */ -async function getDeployment(deployment_owner, deployment_name) { +async function getDeployment( + deployment_owner, + deployment_name, + { signal } = {} +) { const response = await this.request( `/deployments/${deployment_owner}/${deployment_name}`, { method: "GET", + signal, + } + ); + + return response.json(); +} + +/** + * @typedef {Object} DeploymentCreateRequest - Request body for `deployments.create` + * @property {string} name - the name of the deployment + * @property {string} model - the full name of the model that you want to deploy e.g. stability-ai/sdxl + * @property {string} version - the 64-character string ID of the model version that you want to deploy + * @property {string} hardware - the SKU for the hardware used to run the model, via `replicate.hardware.list()` + * @property {number} min_instances - the minimum number of instances for scaling + * @property {number} max_instances - the maximum number of instances for scaling + */ + +/** + * Create a deployment + * + * @param {DeploymentCreateRequest} deployment_config - Required. The deployment config. + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal + * @returns {Promise} Resolves with the deployment data + */ +async function createDeployment(deployment_config, { signal } = {}) { + const response = await this.request("/deployments", { + method: "POST", + data: deployment_config, + signal, + }); + + return response.json(); +} + +/** + * @typedef {Object} DeploymentUpdateRequest - Request body for `deployments.update` + * @property {string} version - the 64-character string ID of the model version that you want to deploy + * @property {string} hardware - the SKU for the hardware used to run the model, via `replicate.hardware.list()` + * @property {number} min_instances - the minimum number of instances for scaling + * @property {number} max_instances - the maximum number of instances for scaling + */ + +/** + * Update an existing deployment + * + * @param {string} deployment_owner - Required. The username of the user or organization who owns the deployment + * @param {string} deployment_name - Required. The name of the deployment + * @param {DeploymentUpdateRequest} deployment_config - Required. The deployment changes. + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal + * @returns {Promise} Resolves with the deployment data + */ +async function updateDeployment( + deployment_owner, + deployment_name, + deployment_config, + { signal } = {} +) { + const response = await this.request( + `/deployments/${deployment_owner}/${deployment_name}`, + { + method: "PATCH", + data: deployment_config, + signal, + } + ); + + return response.json(); +} + +/** + * Delete a deployment + * + * @param {string} deployment_owner - Required. The username of the user or organization who owns the deployment + * @param {string} deployment_name - Required. The name of the deployment + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal + * @returns {Promise} Resolves with true if the deployment was deleted + */ +async function deleteDeployment( + deployment_owner, + deployment_name, + { signal } = {} +) { + const response = await this.request( + `/deployments/${deployment_owner}/${deployment_name}`, + { + method: "DELETE", + signal, } ); + return response.status === 204; +} + +/** + * List all deployments + * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal + * @returns {Promise} - Resolves with a page of deployments + */ +async function listDeployments({ signal } = {}) { + const response = await this.request("/deployments", { + method: "GET", + signal, + }); + return response.json(); } @@ -62,4 +190,8 @@ module.exports = { create: createPrediction, }, get: getDeployment, + create: createDeployment, + update: updateDeployment, + list: listDeployments, + delete: deleteDeployment, }; diff --git a/lib/files.js b/lib/files.js new file mode 100644 index 00000000..c8101398 --- /dev/null +++ b/lib/files.js @@ -0,0 +1,102 @@ +/** + * Create a file + * + * @param {object} file - Required. The file object. + * @param {object} metadata - Optional. User-provided metadata associated with the file. + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal + * @returns {Promise} - Resolves with the file data + */ +async function createFile(file, metadata = {}, { signal } = {}) { + const form = new FormData(); + + let filename; + let blob; + if (file instanceof Blob) { + filename = file.name || `blob_${Date.now()}`; + blob = file; + } else if (Buffer.isBuffer(file)) { + filename = `buffer_${Date.now()}`; + const bytes = new Uint8Array(file); + blob = new Blob([bytes], { + type: "application/octet-stream", + name: filename, + }); + } else { + throw new Error("Invalid file argument, must be a Blob, File or Buffer"); + } + + form.append("content", blob, filename); + form.append( + "metadata", + new Blob([JSON.stringify(metadata)], { type: "application/json" }) + ); + + const response = await this.request("/files", { + method: "POST", + data: form, + headers: { + "Content-Type": "multipart/form-data", + }, + signal, + }); + + return response.json(); +} + +/** + * List all files + * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal + * @returns {Promise} - Resolves with the files data + */ +async function listFiles({ signal } = {}) { + const response = await this.request("/files", { + method: "GET", + signal, + }); + + return response.json(); +} + +/** + * Get a file + * + * @param {string} file_id - Required. The ID of the file. + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal + * @returns {Promise} - Resolves with the file data + */ +async function getFile(file_id, { signal } = {}) { + const response = await this.request(`/files/${file_id}`, { + method: "GET", + signal, + }); + + return response.json(); +} + +/** + * Delete a file + * + * @param {string} file_id - Required. The ID of the file. + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal + * @returns {Promise} - Resolves with true if the file was deleted + */ +async function deleteFile(file_id, { signal } = {}) { + const response = await this.request(`/files/${file_id}`, { + method: "DELETE", + signal, + }); + + return response.status === 204; +} + +module.exports = { + create: createFile, + list: listFiles, + get: getFile, + delete: deleteFile, +}; diff --git a/lib/hardware.js b/lib/hardware.js index d717548f..e981b1fa 100644 --- a/lib/hardware.js +++ b/lib/hardware.js @@ -1,11 +1,14 @@ /** * List hardware * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the array of hardware */ -async function listHardware() { +async function listHardware({ signal } = {}) { const response = await this.request("/hardware", { method: "GET", + signal, }); return response.json(); diff --git a/lib/models.js b/lib/models.js index c6a02fca..4a3fcdde 100644 --- a/lib/models.js +++ b/lib/models.js @@ -3,11 +3,14 @@ * * @param {string} model_owner - Required. The name of the user or organization that owns the model * @param {string} model_name - Required. The name of the model + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the model data */ -async function getModel(model_owner, model_name) { +async function getModel(model_owner, model_name, { signal } = {}) { const response = await this.request(`/models/${model_owner}/${model_name}`, { method: "GET", + signal, }); return response.json(); @@ -18,13 +21,16 @@ async function getModel(model_owner, model_name) { * * @param {string} model_owner - Required. The name of the user or organization that owns the model * @param {string} model_name - Required. The name of the model + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the list of model versions */ -async function listModelVersions(model_owner, model_name) { +async function listModelVersions(model_owner, model_name, { signal } = {}) { const response = await this.request( `/models/${model_owner}/${model_name}/versions`, { method: "GET", + signal, } ); @@ -37,13 +43,21 @@ async function listModelVersions(model_owner, model_name) { * @param {string} model_owner - Required. The name of the user or organization that owns the model * @param {string} model_name - Required. The name of the model * @param {string} version_id - Required. The model version + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the model version data */ -async function getModelVersion(model_owner, model_name, version_id) { +async function getModelVersion( + model_owner, + model_name, + version_id, + { signal } = {} +) { const response = await this.request( `/models/${model_owner}/${model_name}/versions/${version_id}`, { method: "GET", + signal, } ); @@ -53,11 +67,14 @@ async function getModelVersion(model_owner, model_name, version_id) { /** * List all public models * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the model version data */ -async function listModels() { +async function listModels({ signal } = {}) { const response = await this.request("/models", { method: "GET", + signal, }); return response.json(); @@ -76,14 +93,38 @@ async function listModels() { * @param {string} options.paper_url - A URL for the model's paper. * @param {string} options.license_url - A URL for the model's license. * @param {string} options.cover_image_url - A URL for the model's cover image. This should be an image file. + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the model version data */ async function createModel(model_owner, model_name, options) { - const data = { owner: model_owner, name: model_name, ...options }; + const { signal, ...rest } = options; + const data = { owner: model_owner, name: model_name, ...rest }; const response = await this.request("/models", { method: "POST", data, + signal, + }); + + return response.json(); +} + +/** + * Search for public models + * + * @param {string} query - The search query + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal + * @returns {Promise} Resolves with a page of models matching the search query + */ +async function search(query, { signal } = {}) { + const response = await this.request("/models", { + method: "QUERY", + headers: { + "Content-Type": "text/plain", + }, + data: query, + signal, }); return response.json(); @@ -94,4 +135,5 @@ module.exports = { list: listModels, create: createModel, versions: { list: listModelVersions, get: getModelVersion }, + search, }; diff --git a/lib/predictions.js b/lib/predictions.js index 5b0370e5..708d04b3 100644 --- a/lib/predictions.js +++ b/lib/predictions.js @@ -9,11 +9,12 @@ const { transformFileInputs } = require("./util"); * @param {object} options.input - Required. An object with the model inputs * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the prediction has new output * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) - * @param {boolean} [options.stream] - Whether to stream the prediction output. Defaults to false + * @param {boolean|integer} [options.wait] - Whether to wait until the prediction is completed before returning. If an integer is provided, it will wait for that many seconds. Defaults to false + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the created prediction */ async function createPrediction(options) { - const { model, version, stream, input, ...data } = options; + const { model, version, input, wait, signal, ...data } = options; if (data.webhook) { try { @@ -24,25 +25,45 @@ async function createPrediction(options) { } } + const headers = {}; + if (wait) { + if (typeof wait === "number") { + const n = Math.max(1, Math.ceil(Number(wait)) || 1); + headers["Prefer"] = `wait=${n}`; + } else { + headers["Prefer"] = "wait"; + } + } + let response; if (version) { response = await this.request("/predictions", { method: "POST", + headers, data: { ...data, - input: await transformFileInputs(input), + input: await transformFileInputs( + this, + input, + this.fileEncodingStrategy + ), version, - stream, }, + signal, }); } else if (model) { response = await this.request(`/models/${model}/predictions`, { method: "POST", + headers, data: { ...data, - input: await transformFileInputs(input), - stream, + input: await transformFileInputs( + this, + input, + this.fileEncodingStrategy + ), }, + signal, }); } else { throw new Error("Either model or version must be specified"); @@ -55,11 +76,14 @@ async function createPrediction(options) { * Fetch a prediction by ID * * @param {number} prediction_id - Required. The prediction ID + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the prediction data */ -async function getPrediction(prediction_id) { +async function getPrediction(prediction_id, { signal } = {}) { const response = await this.request(`/predictions/${prediction_id}`, { method: "GET", + signal, }); return response.json(); @@ -69,11 +93,14 @@ async function getPrediction(prediction_id) { * Cancel a prediction by ID * * @param {string} prediction_id - Required. The training ID + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the data for the training */ -async function cancelPrediction(prediction_id) { +async function cancelPrediction(prediction_id, { signal } = {}) { const response = await this.request(`/predictions/${prediction_id}/cancel`, { method: "POST", + signal, }); return response.json(); @@ -82,11 +109,14 @@ async function cancelPrediction(prediction_id) { /** * List all predictions * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with a page of predictions */ -async function listPredictions() { +async function listPredictions({ signal } = {}) { const response = await this.request("/predictions", { method: "GET", + signal, }); return response.json(); diff --git a/lib/stream.js b/lib/stream.js index 012d6d03..2c899bd9 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -1,14 +1,14 @@ // Attempt to use readable-stream if available, attempt to use the built-in stream module. -let Readable; -try { - Readable = require("readable-stream").Readable; -} catch (e) { - try { - Readable = require("stream").Readable; - } catch (e) { - Readable = null; - } -} + +const ApiError = require("./error"); +const { streamAsyncIterator } = require("./util"); +const { + EventSourceParserStream, +} = require("../vendor/eventsource-parser/stream"); +const { TextDecoderStream } = + typeof globalThis.TextDecoderStream === "undefined" + ? require("../vendor/streams-text-encoding/text-decoder-stream") + : globalThis; /** * A server-sent event. @@ -42,98 +42,135 @@ class ServerSentEvent { } /** - * A stream of server-sent events. + * Create a new stream of server-sent events. + * + * @param {object} config + * @param {string} config.url The URL to connect to. + * @param {typeof fetch} [config.fetch] The URL to connect to. + * @param {object} [config.options] The EventSource options. + * @param {boolean} [config.options.useFileOutput] Whether to use the file output stream. + * @returns {ReadableStream & AsyncIterable} */ -class Stream extends Readable { - /** - * Create a new stream of server-sent events. - * - * @param {string} url The URL to connect to. - * @param {object} options The fetch options. - */ - constructor(url, options) { - if (!Readable) { - throw new Error( - "Readable streams are not supported. Please use Node.js 18 or later, or install the readable-stream package." - ); - } +function createReadableStream({ url, fetch, options = {} }) { + const { useFileOutput = true, headers = {}, ...initOptions } = options; + + return new ReadableStream({ + async start(controller) { + const init = { + ...initOptions, + headers: { + ...headers, + Accept: "text/event-stream", + }, + }; + const response = await fetch(url, init); + + if (!response.ok) { + const text = await response.text(); + const request = new Request(url, init); + controller.error( + new ApiError( + `Request to ${url} failed with status ${response.status}: ${text}`, + request, + response + ) + ); + } - super(); - this.url = url; - this.options = options; + const stream = response.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new EventSourceParserStream()); - this.event = null; - this.data = []; - this.lastEventId = null; - this.retry = null; - } + for await (const event of streamAsyncIterator(stream)) { + if (event.event === "error") { + controller.error(new Error(event.data)); + break; + } - decode(line) { - if (!line) { - if (!this.event && !this.data.length && !this.lastEventId) { - return null; - } + let data = event.data; + if ( + useFileOutput && + typeof data === "string" && + (data.startsWith("https:") || data.startsWith("data:")) + ) { + data = createFileOutput({ data, fetch }); + } + controller.enqueue(new ServerSentEvent(event.event, data, event.id)); - const sse = new ServerSentEvent( - this.event, - this.data.join("\n"), - this.lastEventId - ); + if (event.event === "done") { + break; + } + } - this.event = null; - this.data = []; - this.retry = null; + controller.close(); + }, + }); +} - return sse; +/** + * Create a new readable stream for an output file + * created by running a Replicate model. + * + * @param {object} config + * @param {string} config.url The URL to connect to. + * @param {typeof fetch} [config.fetch] The fetch function. + * @returns {ReadableStream} + */ +function createFileOutput({ url, fetch }) { + let type = "application/octet-stream"; + + class FileOutput extends ReadableStream { + async blob() { + const chunks = []; + for await (const chunk of this) { + chunks.push(chunk); + } + return new Blob(chunks, { type }); } - if (line.startsWith(":")) { - return null; + url() { + return new URL(url); } - const [field, value] = line.split(": "); - if (field === "event") { - this.event = value; - } else if (field === "data") { - this.data.push(value); - } else if (field === "id") { - this.lastEventId = value; + toString() { + return url; } - - return null; } - async *[Symbol.asyncIterator]() { - const response = await fetch(this.url, { - ...this.options, - headers: { - Accept: "text/event-stream", - }, - }); - - for await (const chunk of response.body) { - const decoder = new TextDecoder("utf-8"); - const text = decoder.decode(chunk); - const lines = text.split("\n"); - for (const line of lines) { - const sse = this.decode(line); - if (sse) { - if (sse.event === "error") { - throw new Error(sse.data); - } - - yield sse; - - if (sse.event === "done") { - return; - } + return new FileOutput({ + async start(controller) { + const response = await fetch(url); + + if (!response.ok) { + const text = await response.text(); + const request = new Request(url, init); + controller.error( + new ApiError( + `Request to ${url} failed with status ${response.status}: ${text}`, + request, + response + ) + ); + } + + if (response.headers.get("Content-Type")) { + type = response.headers.get("Content-Type"); + } + + try { + for await (const chunk of streamAsyncIterator(response.body)) { + controller.enqueue(chunk); } + controller.close(); + } catch (err) { + controller.error(err); } - } - } + }, + }); } module.exports = { - Stream, + createFileOutput, + createReadableStream, ServerSentEvent, }; diff --git a/lib/trainings.js b/lib/trainings.js index 6b13dca2..49640b9e 100644 --- a/lib/trainings.js +++ b/lib/trainings.js @@ -9,10 +9,11 @@ * @param {object} options.input - Required. An object with the model inputs * @param {string} [options.webhook] - An HTTPS URL for receiving a webhook when the training updates * @param {string[]} [options.webhook_events_filter] - You can change which events trigger webhook requests by specifying webhook events (`start`|`output`|`logs`|`completed`) + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the data for the created training */ async function createTraining(model_owner, model_name, version_id, options) { - const { ...data } = options; + const { signal, ...data } = options; if (data.webhook) { try { @@ -28,6 +29,7 @@ async function createTraining(model_owner, model_name, version_id, options) { { method: "POST", data, + signal, } ); @@ -38,11 +40,14 @@ async function createTraining(model_owner, model_name, version_id, options) { * Fetch a training by ID * * @param {string} training_id - Required. The training ID + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the data for the training */ -async function getTraining(training_id) { +async function getTraining(training_id, { signal } = {}) { const response = await this.request(`/trainings/${training_id}`, { method: "GET", + signal, }); return response.json(); @@ -52,11 +57,14 @@ async function getTraining(training_id) { * Cancel a training by ID * * @param {string} training_id - Required. The training ID + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the data for the training */ -async function cancelTraining(training_id) { +async function cancelTraining(training_id, { signal } = {}) { const response = await this.request(`/trainings/${training_id}/cancel`, { method: "POST", + signal, }); return response.json(); @@ -65,11 +73,14 @@ async function cancelTraining(training_id) { /** * List all trainings * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} - Resolves with a page of trainings */ -async function listTrainings() { +async function listTrainings({ signal } = {}) { const response = await this.request("/trainings", { method: "GET", + signal, }); return response.json(); diff --git a/lib/util.js b/lib/util.js index 48d7563c..09665774 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,6 +1,5 @@ -const crypto = require("node:crypto"); - const ApiError = require("./error"); +const { create: createFile } = require("./files"); /** * @see {@link validateWebhook} @@ -11,6 +10,7 @@ const ApiError = require("./error"); * @param {string} requestData.body - The raw body of the incoming webhook request. * @param {string} requestData.secret - The webhook secret, obtained from `replicate.webhooks.defaul.secret` method. * @param {string} requestData.signature - The webhook signature header from the incoming request, comprising one or more space-delimited signatures. + * @param {Crypto} [crypto] - An optional `Crypto` implementation that conforms to the [browser Crypto interface](https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto) */ /** @@ -23,37 +23,67 @@ const ApiError = require("./error"); * @param {string} requestData.headers["webhook-signature"] - The webhook signature header from the incoming request, comprising one or more space-delimited signatures * @param {string} requestData.body - The raw body of the incoming webhook request * @param {string} secret - The webhook secret, obtained from `replicate.webhooks.defaul.secret` method + * @param {Crypto} [crypto] - An optional `Crypto` implementation that conforms to the [browser Crypto interface](https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto) */ /** * Validate a webhook signature * - * @returns {boolean} - True if the signature is valid + * @returns {Promise} - True if the signature is valid * @throws {Error} - If the request is missing required headers, body, or secret */ -async function validateWebhook(requestData, secret) { - let { id, timestamp, body, signature } = requestData; - const signingSecret = secret || requestData.secret; +async function validateWebhook(requestData, secretOrCrypto, customCrypto) { + let id; + let body; + let timestamp; + let signature; + let secret; + let crypto = globalThis.crypto; if (requestData && requestData.headers && requestData.body) { - id = requestData.headers.get("webhook-id"); - timestamp = requestData.headers.get("webhook-timestamp"); - signature = requestData.headers.get("webhook-signature"); + if (typeof requestData.headers.get === "function") { + // Headers object (e.g. Fetch API Headers) + id = requestData.headers.get("webhook-id"); + timestamp = requestData.headers.get("webhook-timestamp"); + signature = requestData.headers.get("webhook-signature"); + } else { + // Plain object with header key-value pairs + id = requestData.headers["webhook-id"]; + timestamp = requestData.headers["webhook-timestamp"]; + signature = requestData.headers["webhook-signature"]; + } + body = requestData.body; + if (typeof secretOrCrypto !== "string") { + throw new Error( + "Unexpected value for secret passed to validateWebhook, expected a string" + ); + } + + secret = secretOrCrypto; + if (customCrypto) { + crypto = customCrypto; + } + } else { + id = requestData.id; body = requestData.body; + timestamp = requestData.timestamp; + signature = requestData.signature; + secret = requestData.secret; + if (secretOrCrypto) { + crypto = secretOrCrypto; + } } if (body instanceof ReadableStream || body.readable) { try { - const chunks = []; - for await (const chunk of body) { - chunks.push(Buffer.from(chunk)); - } - body = Buffer.concat(chunks).toString("utf8"); + body = await new Response(body).text(); } catch (err) { throw new Error(`Error reading body: ${err.message}`); } - } else if (body instanceof Buffer) { - body = body.toString("utf8"); + } else if (isTypedArray(body)) { + body = await new Blob([body]).text(); + } else if (typeof body === "object") { + body = JSON.stringify(body); } else if (typeof body !== "string") { throw new Error("Invalid body type"); } @@ -66,18 +96,23 @@ async function validateWebhook(requestData, secret) { throw new Error("Missing required body"); } - if (!signingSecret) { + if (!secret) { throw new Error("Missing required secret"); } - const signedContent = `${id}.${timestamp}.${body}`; + if (!crypto) { + throw new Error( + 'Missing `crypto` implementation. If using Node 18 pass in require("node:crypto").webcrypto' + ); + } - const secretBytes = Buffer.from(signingSecret.split("_")[1], "base64"); + const signedContent = `${id}.${timestamp}.${body}`; - const computedSignature = crypto - .createHmac("sha256", secretBytes) - .update(signedContent) - .digest("base64"); + const computedSignature = await createHMACSHA256( + secret.split("_").pop(), + signedContent, + crypto + ); const expectedSignatures = signature .split(" ") @@ -88,6 +123,56 @@ async function validateWebhook(requestData, secret) { ); } +/** + * @param {string} secret - base64 encoded string + * @param {string} data - text body of request + * @param {Crypto} crypto - an implementation of the web Crypto api + */ +async function createHMACSHA256(secret, data, crypto) { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + "raw", + base64ToBytes(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"] + ); + + const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data)); + return bytesToBase64(signature); +} + +/** + * Convert a base64 encoded string into bytes. + * + * @param {string} the base64 encoded string + * @return {Uint8Array} + * + * Two functions for encoding/decoding base64 strings using web standards. Not + * intended to be used to encode/decode arbitrary string data. + * See: https://developer.mozilla.org/en-US/docs/Glossary/Base64#javascript_support + * See: https://stackoverflow.com/a/31621532 + * + * Performance might take a hit because of the conversion to string and then to binary, + * if this is the case we might want to look at an alternative solution. + * See: https://jsben.ch/wnaZC + */ +function base64ToBytes(base64) { + return Uint8Array.from(atob(base64), (m) => m.codePointAt(0)); +} + +/** + * Convert a base64 encoded string into bytes. + * + * See {@link base64ToBytes} for caveats. + * + * @param {Uint8Array | ArrayBuffer} the base64 encoded string + * @return {string} + */ +function bytesToBase64(bytes) { + return btoa(String.fromCharCode.apply(null, new Uint8Array(bytes))); +} + /** * Automatically retry a request if it fails with an appropriate status code. * @@ -150,12 +235,65 @@ async function withAutomaticRetries(request, options = {}) { } attempts += 1; } - /* eslint-enable no-await-in-loop */ } while (attempts < maxRetries); return request(); } +/** + * Walks the inputs and, for any File or Blob, tries to upload it to Replicate + * and replaces the input with the URL of the uploaded file. + * + * @param {Replicate} client - The client used to upload the file + * @param {object} inputs - The inputs to transform + * @param {"default" | "upload" | "data-uri"} strategy - Whether to upload files to Replicate, encode as dataURIs or try both. + * @returns {Promise} - The transformed inputs + * @throws {ApiError} If the request to upload the file fails + */ +async function transformFileInputs(client, inputs, strategy) { + switch (strategy) { + case "data-uri": + return await transformFileInputsToBase64EncodedDataURIs(client, inputs); + case "upload": + return await transformFileInputsToReplicateFileURLs(client, inputs); + case "default": + try { + return await transformFileInputsToReplicateFileURLs(client, inputs); + } catch (error) { + if ( + error instanceof ApiError && + error.response.status >= 400 && + error.response.status < 500 + ) { + throw error; + } + return await transformFileInputsToBase64EncodedDataURIs(inputs); + } + default: + throw new Error(`Unexpected file upload strategy: ${strategy}`); + } +} + +/** + * Walks the inputs and, for any File or Blob, tries to upload it to Replicate + * and replaces the input with the URL of the uploaded file. + * + * @param {Replicate} client - The client used to upload the file + * @param {object} inputs - The inputs to transform + * @returns {Promise} - The transformed inputs + * @throws {ApiError} If the request to upload the file fails + */ +async function transformFileInputsToReplicateFileURLs(client, inputs) { + return await transform(inputs, async (value) => { + if (value instanceof Blob || value instanceof Buffer) { + const file = await createFile.call(client, value); + return file.urls.get; + } + + return value; + }); +} + const MAX_DATA_URI_SIZE = 10_000_000; /** @@ -163,25 +301,25 @@ const MAX_DATA_URI_SIZE = 10_000_000; * base64-encoded data URI. * * @param {object} inputs - The inputs to transform - * @returns {object} - The transformed inputs - * @throws {Error} If the size of inputs exceeds a given threshould set by MAX_DATA_URI_SIZE + * @returns {Promise} - The transformed inputs + * @throws {Error} If the size of inputs exceeds a given threshold set by MAX_DATA_URI_SIZE */ -async function transformFileInputs(inputs) { +async function transformFileInputsToBase64EncodedDataURIs(inputs) { let totalBytes = 0; - const result = await transform(inputs, async (value) => { + return await transform(inputs, async (value) => { let buffer; let mime; if (value instanceof Blob) { - // Currently we use a NodeJS only API for base64 encoding, as + // Currently, we use a NodeJS only API for base64 encoding, as // we move to support the browser we could support either using // btoa (which does string encoding), the FileReader API or - // a JavaScript implenentation like base64-js. + // a JavaScript implementation like base64-js. // See: https://developer.mozilla.org/en-US/docs/Glossary/Base64 // See: https://github.com/beatgammit/base64-js - buffer = Buffer.from(await value.arrayBuffer()); + buffer = await value.arrayBuffer(); mime = value.type; - } else if (Buffer.isBuffer(value)) { + } else if (isTypedArray(value)) { buffer = value; } else { return value; @@ -194,21 +332,20 @@ async function transformFileInputs(inputs) { ); } - const data = buffer.toString("base64"); - mime = mime ?? "application/octet-stream"; + const data = bytesToBase64(buffer); + mime = mime || "application/octet-stream"; return `data:${mime};base64,${data}`; }); - - return result; } // Walk a JavaScript object and transform the leaf values. async function transform(value, mapper) { if (Array.isArray(value)) { - let copy = []; + const copy = []; for (const val of value) { - copy = await transform(val, mapper); + const transformed = await transform(val, mapper); + copy.push(transformed); } return copy; } @@ -224,6 +361,20 @@ async function transform(value, mapper) { return await mapper(value); } +function isTypedArray(arr) { + return ( + arr instanceof Int8Array || + arr instanceof Int16Array || + arr instanceof Int32Array || + arr instanceof Uint8Array || + arr instanceof Uint8ClampedArray || + arr instanceof Uint16Array || + arr instanceof Uint32Array || + arr instanceof Float32Array || + arr instanceof Float64Array + ); +} + // Test for a plain JS object. // Source: lodash.isPlainObject function isPlainObject(value) { @@ -246,4 +397,80 @@ function isPlainObject(value) { ); } -module.exports = { transformFileInputs, validateWebhook, withAutomaticRetries }; +/** + * Parse progress from prediction logs. + * + * This function supports log statements in the following format, + * which are generated by https://github.com/tqdm/tqdm and similar libraries: + * + * ``` + * 76%|████████████████████████████ | 7568/10000 [00:33<00:10, 229.00it/s] + * ``` + * + * @example + * const progress = parseProgressFromLogs("76%|████████████████████████████ | 7568/10000 [00:33<00:10, 229.00it/s]"); + * console.log(progress); + * // { + * // percentage: 0.76, + * // current: 7568, + * // total: 10000, + * // } + * + * @param {object|string} input - A prediction object or string. + * @returns {(object|null)} - An object with the percentage, current, and total, or null if no progress can be parsed. + */ +function parseProgressFromLogs(input) { + const logs = typeof input === "object" && input.logs ? input.logs : input; + if (!logs || typeof logs !== "string") { + return null; + } + + const pattern = /^\s*(\d+)%\s*\|.+?\|\s*(\d+)\/(\d+)/; + const lines = logs.split("\n").reverse(); + + for (const line of lines) { + const matches = line.match(pattern); + + if (matches && matches.length === 4) { + return { + percentage: parseInt(matches[1], 10) / 100, + current: parseInt(matches[2], 10), + total: parseInt(matches[3], 10), + }; + } + } + + return null; +} + +/** + * Helper to make any `ReadableStream` iterable, this is supported + * by most server runtimes but browsers still haven't implemented + * it yet. + * See: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#browser_compatibility + * + * @template T + * @param {ReadableStream} stream an instance of a `ReadableStream` + * @yields {T} a chunk/event from the stream + */ +async function* streamAsyncIterator(stream) { + const reader = stream.getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) return; + yield value; + } + } finally { + reader.releaseLock(); + } +} + +module.exports = { + transform, + transformFileInputs, + validateWebhook, + withAutomaticRetries, + parseProgressFromLogs, + streamAsyncIterator, +}; diff --git a/lib/webhooks.js b/lib/webhooks.js index f1324ec7..8da6fdf3 100644 --- a/lib/webhooks.js +++ b/lib/webhooks.js @@ -1,11 +1,14 @@ /** * Get the default webhook signing secret * + * @param {object} [options] + * @param {AbortSignal} [options.signal] - An optional AbortSignal * @returns {Promise} Resolves with the signing secret for the default webhook */ -async function getDefaultWebhookSecret() { +async function getDefaultWebhookSecret({ signal } = {}) { const response = await this.request("/webhooks/default/secret", { method: "GET", + signal, }); return response.json(); diff --git a/package-lock.json b/package-lock.json index 8ba3c038..c0232d31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "replicate", - "version": "0.27.1", + "version": "1.1.0-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "replicate", - "version": "0.27.1", + "version": "1.1.0-0", "license": "Apache-2.0", "devDependencies": { "@biomejs/biome": "^1.4.1", "@types/jest": "^29.5.3", "@typescript-eslint/eslint-plugin": "^5.56.0", "cross-fetch": "^3.1.5", - "jest": "^29.6.2", - "nock": "^13.3.0", + "jest": "^29.7.0", + "nock": "^14.0.0-beta.6", "publint": "^0.2.7", "ts-jest": "^29.1.0", "typescript": "^5.0.2" @@ -43,119 +43,48 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz", - "integrity": "sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.3.tgz", - "integrity": "sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.21.3", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helpers": "^7.21.0", - "@babel/parser": "^7.21.3", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.3", - "@babel/types": "^7.21.3", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -165,30 +94,24 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", "dev": true, "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -196,42 +119,39 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", - "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -272,119 +192,120 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", - "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.2", - "@babel/types": "^7.21.2" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", "dev": true, "dependencies": { - "@babel/types": "^7.20.2" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", - "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", - "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.0", - "@babel/types": "^7.21.0" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.5", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -462,9 +383,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -534,12 +455,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", + "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -636,12 +557,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", - "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", + "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -651,34 +572,34 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -695,13 +616,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1038,16 +959,16 @@ } }, "node_modules/@jest/console": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.2.tgz", - "integrity": "sha512-0N0yZof5hi44HAR2pPS+ikJ3nzKNoZdVu8FffRf3wy47I7Dm7etk/3KetMdRUqzVd16V4O2m2ISpNTbnIuqy1w==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.6.2", - "jest-util": "^29.6.2", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -1055,37 +976,37 @@ } }, "node_modules/@jest/core": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.6.2.tgz", - "integrity": "sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "dependencies": { - "@jest/console": "^29.6.2", - "@jest/reporters": "^29.6.2", - "@jest/test-result": "^29.6.2", - "@jest/transform": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.5.0", - "jest-config": "^29.6.2", - "jest-haste-map": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.6.2", - "jest-resolve-dependencies": "^29.6.2", - "jest-runner": "^29.6.2", - "jest-runtime": "^29.6.2", - "jest-snapshot": "^29.6.2", - "jest-util": "^29.6.2", - "jest-validate": "^29.6.2", - "jest-watcher": "^29.6.2", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", - "pretty-format": "^29.6.2", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -1102,88 +1023,88 @@ } }, "node_modules/@jest/environment": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.2.tgz", - "integrity": "sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, "dependencies": { - "@jest/fake-timers": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.6.2" + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.2.tgz", - "integrity": "sha512-m6DrEJxVKjkELTVAztTLyS/7C92Y2b0VYqmDROYKLLALHn8T/04yPs70NADUYPrV3ruI+H3J0iUIuhkjp7vkfg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "dependencies": { - "expect": "^29.6.2", - "jest-snapshot": "^29.6.2" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.2.tgz", - "integrity": "sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "dependencies": { - "jest-get-type": "^29.4.3" + "jest-get-type": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.2.tgz", - "integrity": "sha512-euZDmIlWjm1Z0lJ1D0f7a0/y5Kh/koLFMUBE5SUYWrmy8oNhJpbTBDAP6CxKnadcMLDoDf4waRYCe35cH6G6PA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.6.2", - "jest-mock": "^29.6.2", - "jest-util": "^29.6.2" + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.2.tgz", - "integrity": "sha512-cjuJmNDjs6aMijCmSa1g2TNG4Lby/AeU7/02VtpW+SLcZXzOLK2GpN2nLqcFjmhy3B3AoPeQVx7BnyOf681bAw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.2", - "@jest/expect": "^29.6.2", - "@jest/types": "^29.6.1", - "jest-mock": "^29.6.2" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.6.2.tgz", - "integrity": "sha512-sWtijrvIav8LgfJZlrGCdN0nP2EWbakglJY49J1Y5QihcQLfy7ovyxxjJBRXMNltgt4uPtEcFmIMbVshEDfFWw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.6.2", - "@jest/test-result": "^29.6.2", - "@jest/transform": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", @@ -1192,13 +1113,13 @@ "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.6.2", - "jest-util": "^29.6.2", - "jest-worker": "^29.6.2", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -1216,10 +1137,26 @@ } } }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@jest/schemas": { - "version": "29.6.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz", - "integrity": "sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -1229,9 +1166,9 @@ } }, "node_modules/@jest/source-map": { - "version": "29.6.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.0.tgz", - "integrity": "sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", @@ -1243,13 +1180,13 @@ } }, "node_modules/@jest/test-result": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.2.tgz", - "integrity": "sha512-3VKFXzcV42EYhMCsJQURptSqnyjqCGbtLuX5Xxb6Pm6gUf1wIRIl+mandIRGJyWKgNKYF9cnstti6Ls5ekduqw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "dependencies": { - "@jest/console": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, @@ -1258,14 +1195,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.6.2.tgz", - "integrity": "sha512-GVYi6PfPwVejO7slw6IDO0qKVum5jtrJ3KoLGbgBWyr2qr4GaxFV6su+ZAjdTX75Sr1DkMFRk09r2ZVa+wtCGw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "dependencies": { - "@jest/test-result": "^29.6.2", + "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.2", + "jest-haste-map": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -1273,22 +1210,22 @@ } }, "node_modules/@jest/transform": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.2.tgz", - "integrity": "sha512-ZqCqEISr58Ce3U+buNFJYUktLJZOggfyvR+bZMaiV1e8B1SIvJbwZMrYz3gx/KAPn9EXmOmN+uB08yLCjWkQQg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.2", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.6.2", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -1299,12 +1236,12 @@ } }, "node_modules/@jest/types": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz", - "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "dependencies": { - "@jest/schemas": "^29.6.0", + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -1338,9 +1275,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -1353,13 +1290,13 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@nodelib/fs.scandir": { @@ -1404,9 +1341,9 @@ "dev": true }, "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "dependencies": { "type-detect": "4.0.8" @@ -1422,9 +1359,9 @@ } }, "node_modules/@types/babel__core": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", - "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "dependencies": { "@babel/parser": "^7.20.7", @@ -1435,18 +1372,18 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "dependencies": { "@babel/parser": "^7.1.0", @@ -1454,18 +1391,18 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", - "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", "dev": true, "dependencies": { - "@babel/types": "^7.3.0" + "@babel/types": "^7.20.7" } }, "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -1866,15 +1803,15 @@ } }, "node_modules/babel-jest": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.2.tgz", - "integrity": "sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "dependencies": { - "@jest/transform": "^29.6.2", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.5.0", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -1903,9 +1840,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", - "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "dependencies": { "@babel/template": "^7.3.3", @@ -1941,12 +1878,12 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", - "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, "dependencies": { - "babel-plugin-jest-hoist": "^29.5.0", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { @@ -1993,21 +1930,21 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "funding": [ { @@ -2017,13 +1954,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -2102,9 +2043,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001469", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001469.tgz", - "integrity": "sha512-Rcp7221ScNqQPP3W+lVOYDyjdR6dC+neEQCttoNr5bAyz54AboB4iwpnWgyi8P4YUsPybVzT4LgWiBbI3drL4g==", + "version": "1.0.30001615", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz", + "integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==", "dev": true, "funding": [ { @@ -2114,6 +2055,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -2158,9 +2103,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "dev": true }, "node_modules/cliui": { @@ -2223,6 +2168,27 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -2264,9 +2230,9 @@ } }, "node_modules/dedent": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", - "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", "dev": true, "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -2303,9 +2269,9 @@ } }, "node_modules/diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -2337,9 +2303,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.337", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.337.tgz", - "integrity": "sha512-W8gdzXG86mVPoc56eM8YA+QiLxaAxJ8cmDjxZgfhLLWVvZQxyA918w5tX2JEWApZta45T1/sYcmFHTsTOUE3nw==", + "version": "1.4.754", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.754.tgz", + "integrity": "sha512-7Kr5jUdns5rL/M9wFFmMZAgFDuL2YOnanFH4OI4iFzUqyh3XOL7nAGbSlSMZdzKMIyyTpNSbqZsWG9odwLeKvA==", "dev": true }, "node_modules/emittery": { @@ -2370,9 +2336,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -2640,17 +2606,16 @@ } }, "node_modules/expect": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.2.tgz", - "integrity": "sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.6.2", - "@types/node": "*", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-util": "^29.6.2" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -2736,9 +2701,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -2792,9 +2757,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -2806,10 +2771,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/gensync": { "version": "1.0.0-beta.2", @@ -2931,18 +2899,6 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2952,6 +2908,18 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3097,12 +3065,12 @@ "dev": true }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3247,9 +3215,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -3260,15 +3228,15 @@ } }, "node_modules/jest": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.2.tgz", - "integrity": "sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "dependencies": { - "@jest/core": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^29.6.2" + "jest-cli": "^29.7.0" }, "bin": { "jest": "bin/jest.js" @@ -3286,12 +3254,13 @@ } }, "node_modules/jest-changed-files": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", - "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "dependencies": { "execa": "^5.0.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0" }, "engines": { @@ -3299,28 +3268,28 @@ } }, "node_modules/jest-circus": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.6.2.tgz", - "integrity": "sha512-G9mN+KOYIUe2sB9kpJkO9Bk18J4dTDArNFPwoZ7WKHKel55eKIS/u2bLthxgojwlf9NLCVQfgzM/WsOVvoC6Fw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.2", - "@jest/expect": "^29.6.2", - "@jest/test-result": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.6.2", - "jest-matcher-utils": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-runtime": "^29.6.2", - "jest-snapshot": "^29.6.2", - "jest-util": "^29.6.2", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0", - "pretty-format": "^29.6.2", + "pretty-format": "^29.7.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" @@ -3330,22 +3299,21 @@ } }, "node_modules/jest-cli": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.6.2.tgz", - "integrity": "sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "dependencies": { - "@jest/core": "^29.6.2", - "@jest/test-result": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", + "create-jest": "^29.7.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.6.2", - "jest-util": "^29.6.2", - "jest-validate": "^29.6.2", - "prompts": "^2.0.1", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "bin": { @@ -3364,31 +3332,31 @@ } }, "node_modules/jest-config": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.6.2.tgz", - "integrity": "sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.6.2", - "@jest/types": "^29.6.1", - "babel-jest": "^29.6.2", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.6.2", - "jest-environment-node": "^29.6.2", - "jest-get-type": "^29.4.3", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.6.2", - "jest-runner": "^29.6.2", - "jest-util": "^29.6.2", - "jest-validate": "^29.6.2", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.6.2", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -3409,24 +3377,24 @@ } }, "node_modules/jest-diff": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.2.tgz", - "integrity": "sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.6.2" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-docblock": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", - "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "dependencies": { "detect-newline": "^3.0.0" @@ -3436,62 +3404,62 @@ } }, "node_modules/jest-each": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.2.tgz", - "integrity": "sha512-MsrsqA0Ia99cIpABBc3izS1ZYoYfhIy0NNWqPSE0YXbQjwchyt6B1HD2khzyPe1WiJA7hbxXy77ZoUQxn8UlSw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", - "jest-util": "^29.6.2", - "pretty-format": "^29.6.2" + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-environment-node": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.6.2.tgz", - "integrity": "sha512-YGdFeZ3T9a+/612c5mTQIllvWkddPbYcN2v95ZH24oWMbGA4GGS2XdIF92QMhUhvrjjuQWYgUGW2zawOyH63MQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.2", - "@jest/fake-timers": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.6.2", - "jest-util": "^29.6.2" + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-get-type": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.2.tgz", - "integrity": "sha512-+51XleTDAAysvU8rT6AnS1ZJ+WHVNqhj1k6nTvN2PYP+HjU3kqlaKQ1Lnw3NYW3bm2r8vq82X0Z1nDDHZMzHVA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.6.2", - "jest-worker": "^29.6.2", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -3503,46 +3471,46 @@ } }, "node_modules/jest-leak-detector": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.2.tgz", - "integrity": "sha512-aNqYhfp5uYEO3tdWMb2bfWv6f0b4I0LOxVRpnRLAeque2uqOVVMLh6khnTcE2qJ5wAKop0HcreM1btoysD6bPQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "dependencies": { - "jest-get-type": "^29.4.3", - "pretty-format": "^29.6.2" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz", - "integrity": "sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.6.2", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.6.2" + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-message-util": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.2.tgz", - "integrity": "sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.6.2", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -3551,14 +3519,14 @@ } }, "node_modules/jest-mock": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.2.tgz", - "integrity": "sha512-hoSv3lb3byzdKfwqCuT6uTscan471GUECqgNYykg6ob0yiAw3zYc7OrPnI9Qv8Wwoa4lC7AZ9hyS4AiIx5U2zg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-util": "^29.6.2" + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -3582,26 +3550,26 @@ } }, "node_modules/jest-regex-util": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", - "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.2.tgz", - "integrity": "sha512-G/iQUvZWI5e3SMFssc4ug4dH0aZiZpsDq9o1PtXTV1210Ztyb2+w+ZgQkB3iOiC5SmAEzJBOHWz6Hvrd+QnNPw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.2", + "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.6.2", - "jest-validate": "^29.6.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" @@ -3611,43 +3579,43 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.2.tgz", - "integrity": "sha512-LGqjDWxg2fuQQm7ypDxduLu/m4+4Lb4gczc13v51VMZbVP5tSBILqVx8qfWcsdP8f0G7aIqByIALDB0R93yL+w==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "dependencies": { - "jest-regex-util": "^29.4.3", - "jest-snapshot": "^29.6.2" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.6.2.tgz", - "integrity": "sha512-wXOT/a0EspYgfMiYHxwGLPCZfC0c38MivAlb2lMEAlwHINKemrttu1uSbcGbfDV31sFaPWnWJPmb2qXM8pqZ4w==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "dependencies": { - "@jest/console": "^29.6.2", - "@jest/environment": "^29.6.2", - "@jest/test-result": "^29.6.2", - "@jest/transform": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^29.4.3", - "jest-environment-node": "^29.6.2", - "jest-haste-map": "^29.6.2", - "jest-leak-detector": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-resolve": "^29.6.2", - "jest-runtime": "^29.6.2", - "jest-util": "^29.6.2", - "jest-watcher": "^29.6.2", - "jest-worker": "^29.6.2", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -3656,31 +3624,31 @@ } }, "node_modules/jest-runtime": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.2.tgz", - "integrity": "sha512-2X9dqK768KufGJyIeLmIzToDmsN0m7Iek8QNxRSI/2+iPFYHF0jTwlO3ftn7gdKd98G/VQw9XJCk77rbTGZnJg==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.6.2", - "@jest/fake-timers": "^29.6.2", - "@jest/globals": "^29.6.2", - "@jest/source-map": "^29.6.0", - "@jest/test-result": "^29.6.2", - "@jest/transform": "^29.6.2", - "@jest/types": "^29.6.1", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-mock": "^29.6.2", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.6.2", - "jest-snapshot": "^29.6.2", - "jest-util": "^29.6.2", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -3689,9 +3657,9 @@ } }, "node_modules/jest-snapshot": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.2.tgz", - "integrity": "sha512-1OdjqvqmRdGNvWXr/YZHuyhh5DeaLp1p/F8Tht/MrMw4Kr1Uu/j4lRG+iKl1DAqUJDWxtQBMk41Lnf/JETYBRA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", @@ -3699,20 +3667,20 @@ "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.6.2", - "@jest/transform": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.6.2", + "expect": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-diff": "^29.6.2", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-util": "^29.6.2", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "natural-compare": "^1.4.0", - "pretty-format": "^29.6.2", + "pretty-format": "^29.7.0", "semver": "^7.5.3" }, "engines": { @@ -3720,12 +3688,12 @@ } }, "node_modules/jest-util": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.2.tgz", - "integrity": "sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -3737,17 +3705,17 @@ } }, "node_modules/jest-validate": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.2.tgz", - "integrity": "sha512-vGz0yMN5fUFRRbpJDPwxMpgSXW1LDKROHfBopAvDcmD6s+B/s8WJrwi+4bfH4SdInBA5C3P3BI19dBtKzx1Arg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", + "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^29.6.2" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -3766,18 +3734,18 @@ } }, "node_modules/jest-watcher": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.6.2.tgz", - "integrity": "sha512-GZitlqkMkhkefjfN/p3SJjrDaxPflqxEAv3/ik10OirZqJGYH5rPiIsgVcfof0Tdqg3shQGdEIxDBx+B4tuLzA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "dependencies": { - "@jest/test-result": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.6.2", + "jest-util": "^29.7.0", "string-length": "^4.0.1" }, "engines": { @@ -3785,13 +3753,13 @@ } }, "node_modules/jest-worker": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.2.tgz", - "integrity": "sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.6.2", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -3948,12 +3916,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -4083,18 +4045,16 @@ "dev": true }, "node_modules/nock": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.0.tgz", - "integrity": "sha512-HHqYQ6mBeiMc+N038w8LkMpDCRquCHWeNmN3v6645P3NhN2+qXOBqvPqo7Rt1VyCMzKhJ733wZqw5B7cQVFNPg==", + "version": "14.0.0-beta.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.0-beta.6.tgz", + "integrity": "sha512-b7lc7qvj1dQzxtbU7TqyTMnKbNKwGQd585xsRtcCZOv3I/yOK9Vwv4nOgnLFxFtX9m1yjhQDRbgqFCqNh9HuEw==", "dev": true, "dependencies": { - "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.21", "propagate": "^2.0.0" }, "engines": { - "node": ">= 10.13" + "node": ">= 18" } }, "node_modules/node-fetch": { @@ -4124,9 +4084,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/normalize-path": { @@ -4486,12 +4446,12 @@ } }, "node_modules/pretty-format": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", - "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.6.0", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -4573,9 +4533,9 @@ } }, "node_modules/pure-rand": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", - "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "dev": true, "funding": [ { @@ -4640,12 +4600,12 @@ } }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -5167,9 +5127,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.14.tgz", + "integrity": "sha512-JixKH8GR2pWYshIPUg/NujK3JO7JiqEEUiNArE86NQyrgUuZeTlZQN3xuS/yiV5Kb48ev9K6RqNkaJjXsdg7Jw==", "dev": true, "funding": [ { @@ -5179,14 +5139,18 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "escalade": "^3.1.1", + "escalade": "^3.1.2", "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -5203,25 +5167,19 @@ } }, "node_modules/v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" } }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/package.json b/package.json index 098bb3ad..8bf27d6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "replicate", - "version": "0.27.1", + "version": "1.1.0-0", "description": "JavaScript client for Replicate", "repository": "github:replicate/replicate-javascript", "homepage": "https://github.com/replicate/replicate-javascript#readme", @@ -16,6 +16,7 @@ "index.d.ts", "index.js", "lib/**/*.js", + "vendor/**/*", "package.json" ], "engines": { @@ -40,8 +41,8 @@ "@types/jest": "^29.5.3", "@typescript-eslint/eslint-plugin": "^5.56.0", "cross-fetch": "^3.1.5", - "jest": "^29.6.2", - "nock": "^13.3.0", + "jest": "^29.7.0", + "nock": "^14.0.0-beta.6", "publint": "^0.2.7", "ts-jest": "^29.1.0", "typescript": "^5.0.2" diff --git a/tsconfig.json b/tsconfig.json index 7a564ee1..d77efdc5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,9 +2,8 @@ "compilerOptions": { "esModuleInterop": true, "noEmit": true, - "strict": true + "strict": true, + "allowJs": true }, - "exclude": [ - "**/node_modules" - ] + "exclude": ["**/node_modules", "integration"] } diff --git a/vendor/eventsource-parser/stream.js b/vendor/eventsource-parser/stream.js new file mode 100644 index 00000000..88465daa --- /dev/null +++ b/vendor/eventsource-parser/stream.js @@ -0,0 +1,198 @@ +// Source: https://github.com/rexxars/eventsource-parser/tree/v1.1.2 +// +// MIT License +// +// Copyright (c) 2024 Espen Hovlandsdal +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if ((from && typeof from === "object") || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { + get: () => from[key], + enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable, + }); + } + return to; +}; +var __toCommonJS = (mod) => + __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// /input.ts +var input_exports = {}; +__export(input_exports, { + EventSourceParserStream: () => EventSourceParserStream, +}); +module.exports = __toCommonJS(input_exports); + +// http-url:https://unpkg.com/eventsource-parser@1.1.2/dist/index.js +function createParser(onParse) { + let isFirstChunk; + let buffer; + let startingPosition; + let startingFieldLength; + let eventId; + let eventName; + let data; + reset(); + return { + feed, + reset, + }; + function reset() { + isFirstChunk = true; + buffer = ""; + startingPosition = 0; + startingFieldLength = -1; + eventId = void 0; + eventName = void 0; + data = ""; + } + function feed(chunk) { + buffer = buffer ? buffer + chunk : chunk; + if (isFirstChunk && hasBom(buffer)) { + buffer = buffer.slice(BOM.length); + } + isFirstChunk = false; + const length = buffer.length; + let position = 0; + let discardTrailingNewline = false; + while (position < length) { + if (discardTrailingNewline) { + if (buffer[position] === "\n") { + ++position; + } + discardTrailingNewline = false; + } + let lineLength = -1; + let fieldLength = startingFieldLength; + let character; + for ( + let index = startingPosition; + lineLength < 0 && index < length; + ++index + ) { + character = buffer[index]; + if (character === ":" && fieldLength < 0) { + fieldLength = index - position; + } else if (character === "\r") { + discardTrailingNewline = true; + lineLength = index - position; + } else if (character === "\n") { + lineLength = index - position; + } + } + if (lineLength < 0) { + startingPosition = length - position; + startingFieldLength = fieldLength; + break; + } else { + startingPosition = 0; + startingFieldLength = -1; + } + parseEventStreamLine(buffer, position, fieldLength, lineLength); + position += lineLength + 1; + } + if (position === length) { + buffer = ""; + } else if (position > 0) { + buffer = buffer.slice(position); + } + } + function parseEventStreamLine(lineBuffer, index, fieldLength, lineLength) { + if (lineLength === 0) { + if (data.length > 0) { + onParse({ + type: "event", + id: eventId, + event: eventName || void 0, + data: data.slice(0, -1), + // remove trailing newline + }); + data = ""; + eventId = void 0; + } + eventName = void 0; + return; + } + const noValue = fieldLength < 0; + const field = lineBuffer.slice( + index, + index + (noValue ? lineLength : fieldLength) + ); + let step = 0; + if (noValue) { + step = lineLength; + } else if (lineBuffer[index + fieldLength + 1] === " ") { + step = fieldLength + 2; + } else { + step = fieldLength + 1; + } + const position = index + step; + const valueLength = lineLength - step; + const value = lineBuffer.slice(position, position + valueLength).toString(); + if (field === "data") { + data += value ? "".concat(value, "\n") : "\n"; + } else if (field === "event") { + eventName = value; + } else if (field === "id" && !value.includes("\0")) { + eventId = value; + } else if (field === "retry") { + const retry = parseInt(value, 10); + if (!Number.isNaN(retry)) { + onParse({ + type: "reconnect-interval", + value: retry, + }); + } + } + } +} +var BOM = [239, 187, 191]; +function hasBom(buffer) { + return BOM.every((charCode, index) => buffer.charCodeAt(index) === charCode); +} + +// http-url:https://unpkg.com/eventsource-parser@1.1.2/dist/stream.js +var EventSourceParserStream = class extends TransformStream { + constructor() { + let parser; + super({ + start(controller) { + parser = createParser((event) => { + if (event.type === "event") { + controller.enqueue(event); + } + }); + }, + transform(chunk) { + parser.feed(chunk); + }, + }); + } +}; diff --git a/vendor/streams-text-encoding/text-decoder-stream.js b/vendor/streams-text-encoding/text-decoder-stream.js new file mode 100644 index 00000000..f400709d --- /dev/null +++ b/vendor/streams-text-encoding/text-decoder-stream.js @@ -0,0 +1,95 @@ +// Adapted from https://github.com/stardazed/sd-streams +// +// MIT License +// +// Copyright (c) 2018-Present @zenmumbler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// /input.ts +var input_exports = {}; +__export(input_exports, { + TextDecoderStream: () => TextDecoderStream +}); +module.exports = __toCommonJS(input_exports); + +// http-url:https://unpkg.com/@stardazed/streams-text-encoding@1.0.2/dist/sd-streams-text-encoding.esm.js +var decDecoder = Symbol("decDecoder"); +var decTransform = Symbol("decTransform"); +var TextDecodeTransformer = class { + constructor(decoder) { + this.decoder_ = decoder; + } + transform(chunk, controller) { + if (!(chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk))) { + throw new TypeError("Input data must be a BufferSource"); + } + const text = this.decoder_.decode(chunk, { stream: true }); + if (text.length !== 0) { + controller.enqueue(text); + } + } + flush(controller) { + const text = this.decoder_.decode(); + if (text.length !== 0) { + controller.enqueue(text); + } + } +}; +var TextDecoderStream = class { + constructor(label, options) { + const decoder = new TextDecoder(label || "utf-8", options || {}); + this[decDecoder] = decoder; + this[decTransform] = new TransformStream(new TextDecodeTransformer(decoder)); + } + get encoding() { + return this[decDecoder].encoding; + } + get fatal() { + return this[decDecoder].fatal; + } + get ignoreBOM() { + return this[decDecoder].ignoreBOM; + } + get readable() { + return this[decTransform].readable; + } + get writable() { + return this[decTransform].writable; + } +}; +var encEncoder = Symbol("encEncoder"); +var encTransform = Symbol("encTransform");