{"_id":"@a-type/durable-object-utils","name":"@a-type/durable-object-utils","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.1":{"name":"@a-type/durable-object-utils","version":"0.0.1","description":"Personal collection of Cloudflare Durable Object tools","main":"index.js","scripts":{"build":"tsc","test":"vitest","test:types":"wrangler types -c tests/wrangler.toml tests/env.d.ts","bump":"bumpp"},"keywords":[],"author":"","license":"ISC","dependencies":{"durable-utils":"^0.3.5","hono":"^4.9.8","jose":"^6.1.0"},"devDependencies":{"@cloudflare/vitest-pool-workers":"0.8.49","@cloudflare/workers-types":"^4.20250920.0","@types/node":"^22.0.0","bumpp":"^10.2.3","kysely":"^0.28.7","typescript":"^5.9.2","vitest":"^3.2.4","wrangler":"^4.38.0","zod":"^4.1.11"},"peerDependencies":{"kysely":"^0.28.7"},"_id":"@a-type/durable-object-utils@0.0.1","gitHead":"ca7f77f5324d89398ecbea56699ecd37bbc2eee2","_nodeVersion":"20.19.5","_npmVersion":"11.6.0","dist":{"integrity":"sha512-8tgNgeUsQ4kBm59huQJfnsXp4llwtsLfL/usfsKOXoH1d91Tgz0ckZvsbotDrcUFD+Ezci/7qtbXgYVGHEcTLw==","shasum":"575707d030103bc63df1ab74ef930fe9b0125c6e","tarball":"https://registry.npmjs.org/@a-type/durable-object-utils/-/durable-object-utils-0.0.1.tgz","fileCount":20,"unpackedSize":33177,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDuiTi1Vn21jR3lVzGy9E5Q2j/ol8SxXOKssZqsKCmfjQIhAPGe0dzP/nhjfB/U9VcnZgxF3aN+sd6K1Zy/t55UB4Y7"}]},"_npmUser":{"name":"a-type","email":"gaforres@gmail.com"},"directories":{},"maintainers":[{"name":"a-type","email":"gaforres@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/durable-object-utils_0.0.1_1758512755466_0.9861461697695277"},"_hasShrinkwrap":false}},"time":{"created":"2025-09-22T03:45:55.292Z","0.0.1":"2025-09-22T03:45:55.666Z","modified":"2025-09-22T03:45:56.017Z"},"maintainers":[{"name":"a-type","email":"gaforres@gmail.com"}],"description":"Personal collection of Cloudflare Durable Object tools","keywords":[],"license":"ISC","readme":"# durable-object-utils\n\nPersonal collection of Cloudflare Durable Object tools\n\n## SqlWrapper\n\nA Kysely-based wrapper around DurableObject SQL for typed queries. Not a Kysely dialect; just helps write the queries and runs them using DO SQL.\n\nUsed by Scheduler.\n\n```ts\nexport const migrations: SQLMigrations.SQLSchemaMigration[] = [\n\t{\n\t\tidMonotonicInc: 1,\n\t\tdescription: 'Add Dummy table',\n\t\tsql: `\n\t\t\tCREATE TABLE IF NOT EXISTS Dummy (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tname TEXT NOT NULL\n\t\t\t);\n\t\t`,\n\t},\n];\n\nexport interface Tables {\n\tDummy: DummyTable;\n}\n\nconst db = new Kysely<Tables>({\n\tdialect: {\n\t\tcreateAdapter: () => new SqliteAdapter(),\n\t\tcreateDriver: () => new DummyDriver(),\n\t\tcreateIntrospector: (db) => new SqliteIntrospector(db),\n\t\tcreateQueryCompiler: () => new SqliteQueryCompiler(),\n\t},\n});\n\nclass MyObj extends DurableObject {\n\t#sql;\n\tconstructor(ctx, env) {\n\t\tsuper(ctx, env);\n\t\tthis.#sql = new SqlWrapper(ctx.storage, migrations);\n\t}\n\n\tasync fetch() {\n\t\tawait this.#sql.run(\n\t\t\tdb.insertInto('Dummy').values({ id: '1', name: 'one' }),\n\t\t);\n\t}\n}\n```\n\n## Scheduler\n\nKind of like PartyWhen, but compositional instead of extending DurableObject, so you can use it within another DO with other logic.\n\n```ts\nclass MyObj extends DurableObject {\n\t#sql;\n\t#scheduler;\n\n\tconstructor(ctx, env) {\n\t\tsuper(ctx, env);\n\t\tthis.#sql = new SqlWrapper(ctx.storage, migrations);\n\t\tthis.#scheduler = new Scheduler<TaskTypes>(\n\t\t\tthis.#sql,\n\t\t\tctx.storage,\n\t\t\tthis.#handleScheduledTask,\n\t\t);\n\t}\n\n\t// required\n\talarm() {\n\t\treturn this.#scheduler.handleAlarm();\n\t}\n\n\t#handleScheduledTask = (task: TaskTypes) => {};\n}\n```\n\n## SocketHandler\n\nAn abstraction to manage socket connection lifecycle and send/receive events without cluttering your DO with boilerplate.\n\nRequires use of a simple authorization token to connect. The token embeds an audience (the ID of the DurableObject) and a subject (like a userId). When the token is received by a DO's socket connector, it checks that the DO ID matches the audience and associates the subject with the connection.\n\nValidates incoming and outgoing messages to ensure consistency with your protocol.\n\n```ts\nclass MyObj extends DurableObject {\n\t#sockets;\n\n\tconstructor(ctx, env) {\n\t\tthis.#sockets = new SocketHandler(ctx, {\n\t\t\ttokenSecret: env.SOCKET_SECRET,\n\t\t\tclientMessageShape: clientMessageSchema, // Zod schema\n\t\t\tserverMessageShape: serverMessageSchema,\n\t\t\tconnectHandler: (ws, info) => {\n\t\t\t\tconsole.log(info.subject, 'connected');\n\t\t\t},\n\t\t\tdisconnectHandler: (ws, info, error) => {\n\t\t\t\tconsole.log(info.subject, 'disconnected');\n\t\t\t\tif (error) console.error(error);\n\t\t\t},\n\t\t\tmessageHandlers: {\n\t\t\t\tping: this.#handlePing,\n\t\t\t},\n\t\t});\n\t}\n\n\t#handlePing = (msg: PingMessage, info: SocketSessionInfo) => {\n\t\tthis.#sockets.send(\n\t\t\t{\n\t\t\t\ttype: 'pong',\n\t\t\t},\n\t\t\t{ to: info.subject },\n\t\t);\n\t};\n\n\t// required: delegate to socket handler\n\tfetch(req: Request) {\n\t\treturn this.#sockets.fetch(req);\n\t}\n\twebSocketMessage(\n\t\tws: WebSocket,\n\t\tmessage: string | ArrayBuffer,\n\t): void | Promise<void> {\n\t\treturn this.#sockets.onMessage(ws, message);\n\t}\n\twebSocketClose(\n\t\tws: WebSocket,\n\t\tcode: number,\n\t\treason: string,\n\t\twasClean: boolean,\n\t): void | Promise<void> {\n\t\treturn this.#sockets.onClose(ws, code, reason, wasClean);\n\t}\n\twebSocketError(ws: WebSocket, error: unknown): void | Promise<void> {\n\t\treturn this.#sockets.onError(ws, error);\n\t}\n}\n```\n","readmeFilename":"README.md","_rev":"1-4e334691d8f09ef6dd4f4ab00a824b95"}