{"id":24989352,"url":"https://github.com/s2-streamstore/s2-sdk-typescript","last_synced_at":"2026-04-14T21:01:28.102Z","repository":{"id":331988461,"uuid":"1070501929","full_name":"s2-streamstore/s2-sdk-typescript","owner":"s2-streamstore","description":"TypeScript SDK for S2, the durable streams API","archived":false,"fork":false,"pushed_at":"2026-04-08T20:37:42.000Z","size":2428,"stargazers_count":27,"open_issues_count":26,"forks_count":7,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-04-08T22:22:01.810Z","etag":null,"topics":["durable","durable-streams","real-time","streaming","streamstore","typescript","write-ahead-log"],"latest_commit_sha":null,"homepage":"https://s2.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/s2-streamstore.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-06T02:58:42.000Z","updated_at":"2026-04-08T20:37:53.000Z","dependencies_parsed_at":"2026-04-01T00:02:00.978Z","dependency_job_id":null,"html_url":"https://github.com/s2-streamstore/s2-sdk-typescript","commit_stats":null,"previous_names":["s2-streamstore/s2-sdk-typescript"],"tags_count":37,"template":false,"template_full_name":null,"purl":"pkg:github/s2-streamstore/s2-sdk-typescript","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s2-streamstore%2Fs2-sdk-typescript","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s2-streamstore%2Fs2-sdk-typescript/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s2-streamstore%2Fs2-sdk-typescript/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s2-streamstore%2Fs2-sdk-typescript/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/s2-streamstore","download_url":"https://codeload.github.com/s2-streamstore/s2-sdk-typescript/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s2-streamstore%2Fs2-sdk-typescript/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31815080,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-14T18:05:02.291Z","status":"ssl_error","status_checked_at":"2026-04-14T18:05:01.765Z","response_time":153,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["durable","durable-streams","real-time","streaming","streamstore","typescript","write-ahead-log"],"created_at":"2025-02-04T12:57:06.346Z","updated_at":"2026-04-14T21:01:28.096Z","avatar_url":"https://github.com/s2-streamstore.png","language":"TypeScript","funding_links":[],"categories":["Databases","پیاده‌سازی‌های سرور","🗄️ Database","Other Tools and Integrations"],"sub_categories":["Multi-Database Tools","🗄️ \u003ca name=\"databases\"\u003e\u003c/a\u003eپایگاه‌های داده","How to Submit"],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cp\u003e\n    \u003c!-- Light mode logo --\u003e\n    \u003ca href=\"https://s2.dev#gh-light-mode-only\"\u003e\n      \u003cimg src=\"https://raw.githubusercontent.com/s2-streamstore/s2-sdk-rust/main/assets/s2-black.png\" height=\"60\"\u003e\n    \u003c/a\u003e\n    \u003c!-- Dark mode logo --\u003e\n    \u003ca href=\"https://s2.dev#gh-dark-mode-only\"\u003e\n      \u003cimg src=\"https://raw.githubusercontent.com/s2-streamstore/s2-sdk-rust/main/assets/s2-white.png\" height=\"60\"\u003e\n    \u003c/a\u003e\n  \u003c/p\u003e\n\n  \u003ch1\u003eTypeScript SDK for S2\u003c/h1\u003e\n\n  \u003cp\u003e\n    \u003c!-- npm --\u003e\n    \u003ca href=\"https://www.npmjs.com/package/@s2-dev/streamstore\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@s2-dev/streamstore.svg\" alt=\"npm version\" /\u003e\u003c/a\u003e\n    \u003c!-- Discord (chat) --\u003e\n    \u003ca href=\"https://discord.gg/vTCs7kMkAf\"\u003e\u003cimg src=\"https://img.shields.io/discord/1209937852528599092?logo=discord\" /\u003e\u003c/a\u003e\n  \u003c/p\u003e\n\u003c/div\u003e\n\nThis repo contains the official TypeScript SDK for [S2](https://s2.dev), a serverless data store for streams, built on the service's [REST API](https://s2.dev/docs/rest/protocol).\n\nS2 is a managed service that provides unlimited, durable streams.\n\nStreams can be appended to, with all new records added to the tail of the stream. You can read from any portion of a stream – indexing by record sequence number, or timestamp – and follow updates live.\n\nSee it in action on the [playground](https://s2.dev/playground).\n\n**Quick links:**\n- Runnable [examples](./examples) directory\n- Patterns [package](packages/patterns)\n- SDK [documentation](https://s2-streamstore.github.io/s2-sdk-typescript/)\n- S2 REST API [documentation](https://s2.dev/docs/rest/protocol)\n\n\u003e **Note:** The repository for releases prior to 0.16.x can be found at this [link](https://github.com/s2-streamstore/s2-sdk-typescript-old).\n\n## Install\n\n```bash\nnpm add @s2-dev/streamstore\n# or\nyarn add @s2-dev/streamstore\n# or\nbun add @s2-dev/streamstore\n```\n\n## Quick start\n\nWant to get up and running? Head to the [S2 dashboard](https://s2.dev/dashboard) to sign-up and grab an access key, and create a new \"basin\" from the UI.\n\nThen define the following environment variables respectively:\n```bash\nexport S2_ACCESS_TOKEN=\"\u003ctoken\u003e\"\nexport S2_BASIN=\"\u003cbasin\u003e\"\n```\n\nFrom there, you can run the following snippet (or any of the other [examples](./examples)).\n\n\u003c!-- snippet:start quick-start --\u003e\n```ts\nimport {\n\tAppendAck,\n\tAppendInput,\n\tAppendRecord,\n\tS2,\n\tS2Environment,\n} from \"@s2-dev/streamstore\";\n\nconst basinName = process.env.S2_BASIN ?? \"my-existing-basin\";\nconst streamName = process.env.S2_STREAM ?? \"my-new-stream\";\n\nconst s2 = new S2({\n\t...S2Environment.parse(),\n\taccessToken: process.env.S2_ACCESS_TOKEN ?? \"my-access-token\",\n});\n\n// Create a basin (namespace) client for basin-level operations.\nconst basin = s2.basin(basinName);\n\n// Make a new stream within the basin, using the default configuration.\nconst streamResponse = await basin.streams.create({ stream: streamName });\nconsole.dir(streamResponse, { depth: null });\n\n// Create a stream client on our new stream.\nconst stream = basin.stream(streamName);\n\n// Make a single append call.\nconst append: Promise\u003cAppendAck\u003e = stream.append(\n\t// `append` expects an input batch of one or many records.\n\tAppendInput.create([\n\t\t// Records can use a string encoding...\n\t\tAppendRecord.string({\n\t\t\tbody: \"Hello from the docs snippet!\",\n\t\t\theaders: [[\"content-type\", \"text/plain\"]],\n\t\t}),\n\t\t// ...or contain raw binary data.\n\t\tAppendRecord.bytes({\n\t\t\tbody: new TextEncoder().encode(\"Bytes payload\"),\n\t\t}),\n\t]),\n);\n\n// When the promise resolves, the data is fully durable and present on the stream.\nconst ack = await append;\nconsole.log(\n\t`Appended records ${ack.start.seqNum} through ${ack.end.seqNum} (exclusive).`,\n);\nconsole.dir(ack, { depth: null });\n\n// Read the two records back as binary.\nconst batch = await stream.read(\n\t{\n\t\tstart: { from: { seqNum: ack.start.seqNum } },\n\t\tstop: { limits: { count: 2 } },\n\t},\n\t{ as: \"bytes\" },\n);\n\nfor (const record of batch.records) {\n\tconsole.dir(record, { depth: null });\n\tconsole.log(\"decoded body: %s\", new TextDecoder().decode(record.body));\n}\n```\n\u003c!-- snippet:end quick-start --\u003e\n\n## Development\n\nRun examples:\n\n```bash\nexport S2_ACCESS_TOKEN=\"\u003ctoken\u003e\"\nexport S2_BASIN=\"\u003cbasin\u003e\"\nexport S2_STREAM=\"\u003cstream\u003e\" # optional per example\nnpx tsx examples/\u003cexample\u003e.ts\n```\n\nRun tests:\n\n```bash\nbun run test\n```\n\nThe SDK also ships with a basic browser example, to experiment with using the SDK directly from the web.\n\n```bash\nbun run --cwd packages/streamstore example:browser\n```\n\n## Using S2\n\nS2 SDKs, including this TypeScript one, provide high-level abstractions and conveniences over the core [REST API](https://s2.dev/docs/rest/protocol).\n\n### Account and basin operations\n\nThe account and basin APIs allow for CRUD ops on basins (namespaces of streams), streams, granular access tokens, and more.\n\n### Data plane (stream) operations\n\nThe core SDK verbs are around appending data to streams, reading data from them.\n\nSee the examples and documentation for more details.\n\nBelow are some high level notes on how to interact with the data plane.\n\n#### Appends\n\nThe atomic unit of append is an `AppendInput`, which contains a batch of `AppendRecord`s and some optional additional parameters.\n\nRecords contain a body and optional headers. After an append completes, each record will be assigned a sequence number (and a timestamp).\n\n\n\u003c!-- snippet:start data-plane-unary --\u003e\n```ts\n// Append a mixed batch: string + bytes with headers.\nconsole.log(\"Appending two records (string + bytes).\");\nconst mixedAck = await stream.append(\n\tAppendInput.create([\n\t\tAppendRecord.string({\n\t\t\tbody: \"string payload\",\n\t\t\theaders: [\n\t\t\t\t[\"record-type\", \"example\"],\n\t\t\t\t[\"user-id\", \"123\"],\n\t\t\t],\n\t\t}),\n\t\tAppendRecord.bytes({\n\t\t\tbody: new TextEncoder().encode(\"bytes payload\"),\n\t\t\theaders: [[new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])]],\n\t\t}),\n\t]),\n);\nconsole.dir(mixedAck, { depth: null });\n```\n\u003c!-- snippet:end data-plane-unary --\u003e\n\n### Append sessions (ordered, stateful appends)\n\nUse an `AppendSession` when you want higher throughput and ordering guarantees:\n- It is stateful and enforces that the order you submit batches becomes the order on the stream.\n- It supports pipelining submissions while still preserving ordering (especially with the `s2s` transport).\n\n\u003c!-- snippet:start data-plane-append-session --\u003e\n```ts\nconsole.log(\"Opening appendSession with maxInflightBytes=1MiB.\");\nconst appendSession = await stream.appendSession({\n\t// This determines the maximum amount of unacknowledged, pending appends,\n\t// which can be outstanding at any given time. This is used to apply backpressure.\n\tmaxInflightBytes: 1024 * 1024,\n});\n\nconst startSeq = mixedAck.end.seqNum;\n// Submit an append batch.\n// This returns a promise that resolves into a `BatchSubmitTicket` once the session has\n// capacity to send it.\nconst append1: BatchSubmitTicket = await appendSession.submit(\n\tAppendInput.create([\n\t\tAppendRecord.string({ body: \"session record A\" }),\n\t\tAppendRecord.string({ body: \"session record B\" }),\n\t]),\n);\nconst append2: BatchSubmitTicket = await appendSession.submit(\n\tAppendInput.create([AppendRecord.string({ body: \"session record C\" })]),\n);\n\n// The tickets can be used to wait for the append to become durable (acknowledged by S2).\nconsole.dir(await append1.ack(), { depth: null });\nconsole.dir(await append2.ack(), { depth: null });\n\nconsole.log(\"Closing append session to flush outstanding batches.\");\nawait appendSession.close();\n```\n\u003c!-- snippet:end data-plane-append-session --\u003e\n\n### Producer \n\nStreams can support up to 200 appended batches per second (per single stream), but tens of MiB/second.\n\nFor throughput, you typically want fewer, but larger batches.\n\nThe `Producer` API simplifies this by connecting an `appendSession` with an auto-batcher (via `BatchTransform`), which lingers and accumulates records for a configurable amount of time. This is the recommended path for most high-throughput writers.\n\n\u003c!-- snippet:start producer-core --\u003e\n```ts\nconst producer = new Producer(\n\tnew BatchTransform({\n\t\t// Linger and collect new records for up to 25ms per batch.\n\t\tlingerDurationMillis: 25,\n\t\tmaxBatchRecords: 200,\n\t}),\n\tawait stream.appendSession(),\n);\n\nconst tickets = [];\nfor (let i = 0; i \u003c 10; i += 1) {\n\tconst ticket = await producer.submit(\n\t\tAppendRecord.string({\n\t\t\tbody: `record-${i}`,\n\t\t}),\n\t);\n\ttickets.push(ticket);\n}\n\nconst acks = await Promise.all(tickets.map((ticket) =\u003e ticket.ack()));\nfor (const ack of acks) {\n\tconsole.log(\"Record durable at seqNum:\", ack.seqNum());\n}\n\n// Use the seqNum of the third ack as a coordinate for reading it back.\nlet record3 = await stream.read({\n\tstart: { from: { seqNum: acks[3].seqNum() } },\n\tstop: { limits: { count: 1 } },\n});\nconsole.dir(record3, { depth: null });\n\nawait producer.close();\nawait stream.close();\n```\n\u003c!-- snippet:end producer-core --\u003e\n\n### Read sessions\n\nRead operations, similarly, can be done via individual `read` calls, or via a `readSession`.\n\nUse a session whenever you want:\n- to read more than a single response batch (responses larger than 1 MiB),\n- to keep a session open and tail for new data (omit stop criteria).\n\n\u003c!-- snippet:start read-session-core --\u003e\n```ts\nconst readSession = await stream.readSession({\n\tstart: { from: { tailOffset: 10 }, clamp: true },\n\tstop: { waitSecs: 10 },\n});\n\nfor await (const record of readSession) {\n\tconsole.log(record.seqNum, record.body);\n}\n```\n\u003c!-- snippet:end read-session-core --\u003e\n\n## Client configuration\n\n### Retries and append retry policy\n\n\u003c!-- snippet:start client-config --\u003e\n```ts\nimport { S2, S2Environment, S2Error } from \"@s2-dev/streamstore\";\n\nconst accessToken = process.env.S2_ACCESS_TOKEN;\nif (!accessToken) {\n\tthrow new Error(\"Set S2_ACCESS_TOKEN to configure the SDK.\");\n}\n\nconst basinName = process.env.S2_BASIN;\nif (!basinName) {\n\tthrow new Error(\"Set S2_BASIN so we know which basin to inspect.\");\n}\n\nconst streamName = process.env.S2_STREAM ?? \"docs/client-config\";\n\n// Global retry config applies to every stream/append/read session created via this client.\nconst s2 = new S2({\n\t...S2Environment.parse(),\n\taccessToken,\n\tretry: {\n\t\tmaxAttempts: 3,\n\t\tminBaseDelayMillis: 100,\n\t\tmaxBaseDelayMillis: 500,\n\t\tappendRetryPolicy: \"all\",\n\t\trequestTimeoutMillis: 5_000,\n\t},\n});\n\nconst basin = s2.basin(basinName);\nawait basin.streams.create({ stream: streamName }).catch((error: unknown) =\u003e {\n\tif (!(error instanceof S2Error \u0026\u0026 error.status === 409)) {\n\t\tthrow error;\n\t}\n});\n\nconst stream = basin.stream(streamName);\nconst tail = await stream.checkTail();\nconsole.log(\"Tail info:\");\nconsole.dir(tail, { depth: null });\n```\n\u003c!-- snippet:end client-config --\u003e\n\n- `appendRetryPolicy: \"noSideEffects\"` only retries appends that are naturally idempotent via `matchSeqNum`.\n- `appendRetryPolicy: \"all\"` can retry any failure (higher durability, but can duplicate data without idempotency).\n\n### Session transports\n\nSessions can use either:\n- `fetch` (HTTP/1.1)\n- `s2s` (S2’s streaming protocol over HTTP/2)\n\nYou can force a transport per stream:\n\n\u003c!-- snippet:start force-transport --\u003e\n```ts\n// Override the automatic transport detection to force the fetch transport.\nconst stream = basin.stream(streamName, {\n\tforceTransport: \"fetch\",\n});\n```\n\u003c!-- snippet:end force-transport --\u003e\n\n... or rely on environment auto-detection to specify the transport, as is the default behavior.\n\n\u003e [!IMPORTANT]\n\u003e HTTP/2 library use, required for `s2s`, is currently only enabled by default for Node.js and Deno. Bun defaults to HTTP/1 (see [tracking issue](https://github.com/s2-streamstore/s2-sdk-typescript/issues/113)), but can be forced using the mechanism described above.\n\n## Patterns\n\nFor higher-level, more opinionated building blocks (typed append/read sessions, framing, dedupe helpers), see the [patterns](packages/patterns/README.md) package.\n\n## Feedback\n\nWe use [Github Issues](https://github.com/s2-streamstore/s2-sdk-typescript/issues) to\ntrack feature requests and issues with the SDK. If you wish to provide feedback,\nreport a bug or request a feature, feel free to open a Github issue.\n\n## Reach out to us\n\nJoin our [Discord](https://discord.gg/vTCs7kMkAf) server. We would love to hear\nfrom you.\n\nYou can also email us at [hi@s2.dev](mailto:hi@s2.dev).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fs2-streamstore%2Fs2-sdk-typescript","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fs2-streamstore%2Fs2-sdk-typescript","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fs2-streamstore%2Fs2-sdk-typescript/lists"}