diff --git a/docs/local-dev.md b/docs/local-dev.md index 462128b..905c74c 100644 --- a/docs/local-dev.md +++ b/docs/local-dev.md @@ -55,8 +55,8 @@ DS_LOCAL_DATA_ROOT=/tmp/my-ds-local bun run src/local/cli.ts start --name defaul ## Programmatic API -The published `@prisma/streams-local` package surface is built for both Bun and -Node consumers. +The published `@prisma/streams-local` package surface is built for Bun +`>=1.2.0` and Node `>=22` consumers. ```ts import { startLocalDurableStreamsServer } from "@prisma/streams-local"; diff --git a/docs/overview.md b/docs/overview.md index 9317a3c..8ebea23 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -70,8 +70,8 @@ See [security.md](./security.md) and [auth.md](./auth.md). ## Prerequisites -- Bun `>=1.3.11` -- Node.js `>=22` for local embedding and Node-based consumers +- Bun `>=1.3.11` for the full self-hosted server and repository workflows +- Bun `>=1.2.0` or Node.js `>=22` for the published `@prisma/streams-local` package ## Quick Start @@ -116,8 +116,8 @@ console.log(server.exports.sqlite.path); await server.close(); ``` -The published `@prisma/streams-local` surface is built to run on both Bun and -Node. The full self-hosted server remains Bun-only. +The published `@prisma/streams-local` surface is built to run on Bun `>=1.2.0` +and Node `>=22`. The full self-hosted server remains Bun-only. The local embedded runtime always applies the built-in `1024 MB` auto-tune preset. That keeps the local package on the current control-plane and endpoint diff --git a/docs/releasing.md b/docs/releasing.md index 394684e..6341498 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -12,6 +12,14 @@ and other trusted local workflows. ## Release Checklist +Release branch policy: + +- Always cut and publish releases from `main` only. +- Do not run `release.yml` against feature branches or temporary release + branches. +- Merge the release changes into `main` first, then dispatch the release + workflow from `main`. + 0. Ensure npm trusted publishing is configured for both packages: - `@prisma/streams-local` @@ -46,7 +54,7 @@ These tests build the generated package directories, pack them, install them into temporary consumers, and verify: - Node end-to-end usage of `@prisma/streams-local` -- Bun end-to-end usage of `@prisma/streams-local`, including the live `/touch/*` path +- Bun end-to-end usage of `@prisma/streams-local` on Bun `1.2.x` and newer, including the live `/touch/*` path - stateful local-runtime reopen flows that must read `/_schema` and skip duplicate first-schema installs when the registry already matches - local package exposure of `GET /v1/server/_details` and `GET /v1/stream/{name}/_routing_keys` @@ -81,10 +89,10 @@ npm publish --access public ./dist/npm/streams-local npm publish --access public ./dist/npm/streams-server ``` -Or use the repository release workflow after pushing to `main`: +Or use the repository release workflow from `main` only: ```bash -gh workflow run release.yml +gh workflow run release.yml --ref main ``` The GitHub workflow builds, validates, and publishes both packages with npm @@ -113,6 +121,8 @@ For `@prisma/streams-local`, the build intentionally: each embed their own copy of the runtime - keeps the local runtime Bun-compatible even though the generated bundle targets the Node module surface +- publishes a local-package Bun engine floor of `>=1.2.0` while keeping the + full server on the repository Bun floor - pins the embedded local runtime to the built-in `1024 MB` auto-tune preset so Prisma CLI gets a predictable cache and concurrency budget - keeps npm dependencies external instead of rebundling them into the local @@ -133,7 +143,7 @@ The split gives you: ## Current Packaging Contract -- `@prisma/streams-local` supports Bun and Node +- `@prisma/streams-local` supports Bun `>=1.2.0` and Node `>=22` - `@prisma/streams-local/internal/daemon` is intentionally internal - `@prisma/streams-server` is Bun-only - the root repository package is still private and is not the publish target diff --git a/package.json b/package.json index 9c0be2f..faeab02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prisma-streams", - "version": "0.1.3", + "version": "0.1.4", "private": true, "description": "Prisma Streams is a Bun + TypeScript implementation of the Durable Streams HTTP protocol, with a local development server for Prisma CLI integration.", "repository": { diff --git a/scripts/build-local-node.mjs b/scripts/build-local-node.mjs index 46f1ebe..3f11770 100644 --- a/scripts/build-local-node.mjs +++ b/scripts/build-local-node.mjs @@ -2,6 +2,7 @@ import { cpSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } f import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; +import { localPackageBunEngine } from "./package-contract.mjs"; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(__dirname, ".."); @@ -36,8 +37,8 @@ function replaceInBuiltLocalFiles(replacements) { function writeDistReadme() { const readme = `# Prisma Streams Local Build -This directory contains the generated Node-compatible package artifacts for the -published \`@prisma/streams-local\` runtime. +This directory contains the generated Node/Bun-compatible package artifacts for +the published \`@prisma/streams-local\` runtime. ## What Local Streams Is @@ -52,13 +53,18 @@ The embedded local runtime always applies the built-in \`1024 MB\` auto-tune preset, so Prisma CLI gets a predictable cache and concurrency budget and the same current HTTP surface, including \`GET /v1/server/_details\`. +Published runtime floor: + +- Bun \`${localPackageBunEngine}\` +- Node.js \`>=22\` + ## Supported Package Surface - \`@prisma/streams-local\` - \`@prisma/streams-local/internal/daemon\` (internal Prisma CLI plumbing) -The full self-hosted server remains Bun-only and is not part of this Node build -surface. +The full self-hosted server remains Bun-only and is not part of this local +build surface. ## Integrating It diff --git a/scripts/build-npm-packages.mjs b/scripts/build-npm-packages.mjs index ee56c39..3343ed5 100644 --- a/scripts/build-npm-packages.mjs +++ b/scripts/build-npm-packages.mjs @@ -2,6 +2,7 @@ import { chmodSync, cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } fro import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; +import { localPackageBunEngine } from "./package-contract.mjs"; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(__dirname, ".."); @@ -104,7 +105,7 @@ function buildLocalPackage() { license: rootPackage.license, type: "module", engines: { - bun: rootPackage.engines.bun, + bun: localPackageBunEngine, node: rootPackage.engines.node, }, publishConfig: { diff --git a/scripts/package-contract.mjs b/scripts/package-contract.mjs new file mode 100644 index 0000000..19641e2 --- /dev/null +++ b/scripts/package-contract.mjs @@ -0,0 +1 @@ +export const localPackageBunEngine = ">=1.2.0"; diff --git a/scripts/test-bun-local-package.mjs b/scripts/test-bun-local-package.mjs index ef7263c..f68d65b 100644 --- a/scripts/test-bun-local-package.mjs +++ b/scripts/test-bun-local-package.mjs @@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url"; import { basename, dirname, join, resolve } from "node:path"; import { tmpdir } from "node:os"; import { spawnSync } from "node:child_process"; +import { localPackageBunEngine } from "./package-contract.mjs"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const TABLE_KEY_POSTS = "8c646d3dd6bc68f4"; @@ -38,6 +39,9 @@ try { if ("@durable-streams/client" in (localPackageManifest.dependencies ?? {})) { throw new Error("@prisma/streams-local should not publish @durable-streams/client"); } + if (localPackageManifest.engines?.bun !== localPackageBunEngine) { + throw new Error(`@prisma/streams-local should publish bun ${localPackageBunEngine}, got ${localPackageManifest.engines?.bun}`); + } const packOutput = run("npm", ["pack", "--pack-destination", packDir], localPackageDir); const tarballName = packOutput.split(/\r?\n/).filter(Boolean).at(-1); if (!tarballName) throw new Error("npm pack did not produce a tarball name"); diff --git a/scripts/test-node-local-package.mjs b/scripts/test-node-local-package.mjs index 819927f..6be7ecd 100644 --- a/scripts/test-node-local-package.mjs +++ b/scripts/test-node-local-package.mjs @@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url"; import { basename, dirname, join, resolve } from "node:path"; import { tmpdir } from "node:os"; import { spawnSync } from "node:child_process"; +import { localPackageBunEngine } from "./package-contract.mjs"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const TABLE_KEY_POSTS = "8c646d3dd6bc68f4"; @@ -38,6 +39,9 @@ try { if ("@durable-streams/client" in (localPackageManifest.dependencies ?? {})) { throw new Error("@prisma/streams-local should not publish @durable-streams/client"); } + if (localPackageManifest.engines?.bun !== localPackageBunEngine) { + throw new Error(`@prisma/streams-local should publish bun ${localPackageBunEngine}, got ${localPackageManifest.engines?.bun}`); + } const packOutput = run("npm", ["pack", "--pack-destination", packDir], localPackageDir); const tarballName = packOutput.split(/\r?\n/).filter(Boolean).at(-1); if (!tarballName) throw new Error("npm pack did not produce a tarball name"); diff --git a/src/bootstrap.ts b/src/bootstrap.ts index c305081..377fffc 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -1,9 +1,9 @@ import { mkdirSync, rmSync } from "node:fs"; import { dirname } from "node:path"; -import { zstdDecompressSync } from "node:zlib"; import type { Config } from "./config"; import { SqliteDurableStore } from "./db/db"; import type { ObjectStore } from "./objectstore/interface"; +import { zstdDecompressSync } from "./util/zstd"; import { localSegmentPath, schemaObjectKey, segmentObjectKey, streamHash16Hex } from "./util/stream_paths"; import { retry } from "./util/retry"; import { dsError } from "./util/ds_error.ts"; diff --git a/src/manifest.ts b/src/manifest.ts index d755ce2..cdfd58f 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -1,4 +1,3 @@ -import { zstdCompressSync } from "node:zlib"; import { Result } from "better-result"; import type { IndexRunRow, @@ -13,6 +12,7 @@ import type { StreamRow, } from "./db/db"; import { encodeOffsetResult } from "./offset"; +import { zstdCompressSync } from "./util/zstd"; import { dsError } from "./util/ds_error.ts"; function b64(bytes: Uint8Array): string { diff --git a/src/profiles/metrics/block_format.ts b/src/profiles/metrics/block_format.ts index 8e8f81c..bddb9c2 100644 --- a/src/profiles/metrics/block_format.ts +++ b/src/profiles/metrics/block_format.ts @@ -1,6 +1,6 @@ import { Result } from "better-result"; -import { zstdCompressSync, zstdDecompressSync } from "node:zlib"; import { BinaryCursor, BinaryPayloadError, BinaryWriter, readI64 } from "../../search/binary/codec"; +import { zstdCompressSync, zstdDecompressSync } from "../../util/zstd"; import type { MetricsBlockRecord } from "./normalize"; export type MetricsBlockSectionInput = { diff --git a/src/search/agg_format.ts b/src/search/agg_format.ts index 15cffaf..b2797de 100644 --- a/src/search/agg_format.ts +++ b/src/search/agg_format.ts @@ -1,8 +1,8 @@ import { Result } from "better-result"; -import { zstdCompressSync, zstdDecompressSync } from "node:zlib"; import { BinaryCursor, BinaryPayloadError, BinaryWriter, concatBytes, readF64, readI64, readU16, readU32 } from "./binary/codec"; import { RestartStringTableView, encodeRestartStringTable } from "./binary/restart_strings"; import { readUVarint, readZigZagVarint, writeUVarint, writeZigZagVarint } from "./binary/varint"; +import { zstdCompressSync, zstdDecompressSync } from "../util/zstd"; import type { SearchCompanionPlan, SearchCompanionPlanRollup } from "./companion_plan"; export type AggSummaryState = { diff --git a/src/segment/format.ts b/src/segment/format.ts index a2c3d2c..942b550 100644 --- a/src/segment/format.ts +++ b/src/segment/format.ts @@ -1,9 +1,9 @@ import { Result } from "better-result"; -import { zstdCompressSync, zstdDecompressSync } from "node:zlib"; import { Bloom256 } from "../util/bloom256"; import { crc32c } from "../util/crc32c"; import { concatBytes, readU32BE, readU64BE, writeU32BE, writeU64BE } from "../util/endian"; import { dsError } from "../util/ds_error.ts"; +import { zstdCompressSync, zstdDecompressSync } from "../util/zstd"; export type SegmentRecord = { appendNs: bigint; diff --git a/src/util/zstd.ts b/src/util/zstd.ts new file mode 100644 index 0000000..5bd405b --- /dev/null +++ b/src/util/zstd.ts @@ -0,0 +1,24 @@ +import * as zlib from "node:zlib"; +import { dsError } from "./ds_error.ts"; + +type ZstdOperation = (bytes: Uint8Array) => Uint8Array | Buffer; + +function getZstdOperation(name: "zstdCompressSync" | "zstdDecompressSync") { + const operation = (zlib as Record)[name]; + + if (typeof operation !== "function") { + throw dsError( + `${name} is not available in this runtime. Prisma Streams local requires node:zlib zstd support.`, + ); + } + + return operation as ZstdOperation; +} + +export function zstdCompressSync(bytes: Uint8Array) { + return getZstdOperation("zstdCompressSync")(bytes); +} + +export function zstdDecompressSync(bytes: Uint8Array) { + return getZstdOperation("zstdDecompressSync")(bytes); +} diff --git a/test/live_stream2_read_perf.test.ts b/test/live_stream2_read_perf.test.ts index 0c1d179..d52e854 100644 --- a/test/live_stream2_read_perf.test.ts +++ b/test/live_stream2_read_perf.test.ts @@ -87,7 +87,7 @@ describe("live stream-2 read perf fixtures", () => { expect(bodyBytes).toBeGreaterThan(600_000); expect(elapsedMs).toBeLessThan(100); }); - }); + }, 15_000); test("wal-tail read stays below 100ms on warm live fixture data", async () => { await withFixtureApp(async (app) => { @@ -96,5 +96,5 @@ describe("live stream-2 read perf fixtures", () => { expect(bodyBytes).toBeGreaterThan(600_000); expect(elapsedMs).toBeLessThan(100); }); - }); + }, 15_000); });