{"_id":"@avenceslau/doable","name":"@avenceslau/doable","dist-tags":{"latest":"0.0.0"},"versions":{"0.0.0":{"name":"@avenceslau/doable","version":"0.0.0","description":"Typed caller-side transform pipeline for Durable Object RPC stubs","homepage":"https://github.com/avenceslau/doable/tree/main/do-rpc-transforms#readme","bugs":{"url":"https://github.com/avenceslau/doable/issues"},"repository":{"type":"git","url":"git+https://github.com/avenceslau/doable.git","directory":"do-rpc-transforms"},"license":"MIT OR Apache-2.0","author":{"name":"wrangler@cloudflare.com"},"exports":{".":{"import":"./dist/index.mjs","types":"./dist/index.d.mts"}},"scripts":{"build":"tsup","check:lint":"eslint . --max-warnings=0 --cache","check:type":"tsc -p ./tsconfig.json","demo:dev":"wrangler dev -c demo-worker/wrangler.jsonc","demo:deploy":"wrangler deploy -c demo-worker/wrangler.jsonc","test":"vitest","test:ci":"vitest run && vitest run -c vitest.workers.config.mts","test:workers":"vitest run -c vitest.workers.config.mts","type:tests":"tsc -p ./tests/tsconfig.json"},"devDependencies":{"@cloudflare/vitest-pool-workers":"^0.10.15","@cloudflare/workers-types":"^4.20260305.0","@types/node":"^20.19.9","better-result":"^2.7.0","eslint":"^9.39.1","tsup":"8.3.0","typescript":"^5.8.3","vitest":"^3.2.3","wrangler":"^4.71.0"},"volta":{"extends":"../../package.json"},"workers-sdk":{"prerelease":true},"_id":"@avenceslau/doable@0.0.0","_nodeVersion":"20.10.0","_npmVersion":"10.2.3","dist":{"integrity":"sha512-rrRlJCX2MsrXFXM0DrRjDdUIXuwIBrqjLc2wpqtRYH5vOM5s8+cPaiEYG3G5uQGsaFz+8NCuaha7tTAGRgj0Iw==","shasum":"7b37aff150fa5dc5019cf99afb0b155897d51022","tarball":"https://registry.npmjs.org/@avenceslau/doable/-/doable-0.0.0.tgz","fileCount":6,"unpackedSize":104079,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIE3yzhS0LSTM2t80gNje+FradtEvmQS3IDkNqOHGFoHaAiEA2LXSyR7CI9PFybf4QUAWHwDv5U+nLtoyolyV8zjebuU="}]},"_npmUser":{"name":"avenceslau","email":"andrevences@gmail.com"},"directories":{},"maintainers":[{"name":"avenceslau","email":"andrevences@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/doable_0.0.0_1772989579910_0.9203203370729081"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-08T17:06:19.825Z","0.0.0":"2026-03-08T17:06:20.062Z","modified":"2026-03-08T17:06:20.261Z"},"maintainers":[{"name":"avenceslau","email":"andrevences@gmail.com"}],"description":"Typed caller-side transform pipeline for Durable Object RPC stubs","homepage":"https://github.com/avenceslau/doable/tree/main/do-rpc-transforms#readme","repository":{"type":"git","url":"git+https://github.com/avenceslau/doable.git","directory":"do-rpc-transforms"},"author":{"name":"wrangler@cloudflare.com"},"bugs":{"url":"https://github.com/avenceslau/doable/issues"},"license":"MIT OR Apache-2.0","readme":"# @avenceslau/doable\n\nTyped, composable transform hooks for Durable Object RPC.\n\nUse this package to add cross-cutting behavior (metrics, retries, codecs, rate limits, context propagation, error shaping) without rewriting every DO method.\n\n## Status\n\nThis package is currently prerelease and iterating quickly.\n\n## Install\n\n```bash\nnpm i @avenceslau/doable\n```\n\n## Core concepts\n\n- **Caller transforms** run on the stub side (`stub.with(...)`)\n- **Callee transforms** run inside the Durable Object (`useDOTransforms(...).with(...).done()`)\n- **`context`** is per-call metadata that flows through the transform chain\n- **`TRANSFORM_CALL_ID_CONTEXT_KEY`** is an internal per-call UUID key added automatically\n\n## Quick start\n\n### 1) Caller-side transforms\n\nWrap a namespace once, then chain `.with(...)` on stubs.\n\n```ts\nimport { createTransform, withTransforms } from \"@avenceslau/doable\";\n\ntype Ctx = { requestId: string };\n\nconst injectRequestId = createTransform<MyDO, {}, Ctx>()\n\t.callerParams<{ requestId: string }>()\n\t.caller(({ requestId }) => async ({ next }) => {\n\t\treturn next({ context: { requestId } });\n\t});\n\nconst ns = withTransforms(env.MY_DO);\nconst id = ns.idFromName(\"demo\");\nconst stub = ns.get(id).with(injectRequestId.callerConfig({ requestId: crypto.randomUUID() }));\n\nawait stub.myMethod();\n```\n\n### 2) Callee-side transforms\n\nRegister transforms once on the DO class.\n\n```ts\nimport { createTransform, useDOTransforms } from \"@avenceslau/doable\";\n\nconst audit = createTransform<MyDO>()\n\t.callee(() => async ({ method, context, next }) => {\n\t\tconst result = await next();\n\t\tconsole.log(\"method\", method, \"requestId\", context.requestId);\n\t\treturn result;\n\t});\n\nexport class MyDO extends DurableObject {\n\tasync myMethod() {\n\t\treturn \"ok\";\n\t}\n}\n\nuseDOTransforms(MyDO).with(audit).done();\n```\n\n## Parameterized caller/callee configs\n\n`createTransform` supports independent config for each side.\n\n```ts\nconst policy = createTransform<MyDO, {}, { requestId?: string }>()\n\t.callerParams<{ requestId: string }>()\n\t.caller(({ requestId }) => async ({ next }) => {\n\t\treturn next({ context: { requestId } });\n\t})\n\t.calleeParams<{ maxCalls: number }>()\n\t.callee(({ maxCalls }) => async ({ next, instance, method }) => {\n\t\t// example policy using maxCalls\n\t\treturn next();\n\t});\n\n// caller side\nstub.with(policy.callerConfig({ requestId: \"r-1\" }));\n\n// callee side\nuseDOTransforms(MyDO).with(policy.calleeConfig({ maxCalls: 3 })).done();\n```\n\nIf a side has no params, you can pass it directly:\n\n```ts\nconst codec = createTransform<object>()\n\t.callerParams<void>()\n\t.caller(() => async ({ next }) => next())\n\t.calleeParams<void>()\n\t.callee(() => async ({ next }) => next());\n\nstub.with(codec);\nuseDOTransforms(MyDO).with(codec).done();\n```\n\n## Method-specific callee transforms\n\nApply transforms only to one method:\n\n```ts\nuseDOTransforms(MyDO)\n\t.method(\"createTodo\")\n\t.with(rateLimitTransform)\n\t.done();\n```\n\n## Context API\n\nUse `next({ context: ... })` to set/merge metadata.\n\n```ts\nconst t = createTransform<MyDO, {}, { traceId?: string }>()\n\t.caller(() => async ({ next }) => {\n\t\treturn next({ context: { traceId: \"abc\" } });\n\t})\n\t.callee(() => async ({ context, next }) => {\n\t\tconsole.log(context.traceId);\n\t\treturn next();\n\t});\n```\n\n## DO output typing with `.done()`\n\n`useDOTransforms(...).done()` finalizes class typing so callee transform output effects are reflected in stub method return types.\n\n```ts\nexport const MyDOWithTransforms = useDOTransforms(MyDO)\n\t.with(codec)\n\t.with(rateLimit)\n\t.done();\n\ntype Env = {\n\tMY_DO: DurableObjectNamespace<InstanceType<typeof MyDOWithTransforms>>;\n};\n```\n\n## API summary\n\n- `createTransform<TStub, TContract?, TContext?, TOutput?>()`\n- `withTransforms(namespace)`\n- `withCalleeTransforms(instance, transforms, options?)`\n- `useDOTransforms(MyDO).with(...).method(\"...\").with(...).done()`\n- `TRANSFORM_CALL_ID_CONTEXT_KEY`\n\n## Development\n\n```bash\npnpm run check:type\npnpm run type:tests\npnpm run test\npnpm run test:workers\n```\n\nDemo worker:\n\n```bash\npnpm run demo:dev\npnpm run demo:deploy\n```\n","readmeFilename":"README.md","_rev":"1-0b0b4e7bb259c598c28a985fd6dbb214"}