Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions sdk/typescript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions sdk/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
"./cloudflare": {
"import": "./dist/integrations/cloudflare/index.js",
"types": "./dist/integrations/cloudflare/index.d.ts"
},
"./serverless": {
"import": "./dist/integrations/serverless/index.js",
"types": "./dist/integrations/serverless/index.d.ts"
}
},
"keywords": [
Expand Down Expand Up @@ -56,11 +60,15 @@
"vitest": "^4.0.1"
},
"peerDependencies": {
"@tursodatabase/serverless": "^0.2.4",
"just-bash": ">=2.0.0"
},
"peerDependenciesMeta": {
"just-bash": {
"optional": true
},
"@tursodatabase/serverless": {
"optional": true
}
},
"dependencies": {
Expand Down
196 changes: 196 additions & 0 deletions sdk/typescript/src/integrations/serverless/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* Adapter that wraps @tursodatabase/serverless Connection to match
* the DatabasePromise interface used by AgentFS internals.
*
* The core challenge: DatabasePromise.prepare() is synchronous and returns
* a Statement immediately, but serverless Connection.prepare() is async
* (it needs to fetch column metadata over HTTP).
*
* Solution: return a LazyStatement that defers the actual prepare() call
* until run()/get()/all() is called — those are already async, so the
* deferral is invisible to callers.
*
* Note on parameter passing: DatabasePromise's Statement uses rest params
* (run(...args)), while serverless Statement uses a single array param
* (run(args?)). The adapter collects rest params and forwards them as a
* single array — this is correct and tested.
*/

import type { Connection, Statement as ServerlessStatement } from "@tursodatabase/serverless";
import type { DatabasePromise } from "@tursodatabase/database-common";

/**
* A statement that defers the async prepare() call until execution.
*
* DatabasePromise.prepare() must return synchronously, but the serverless
* driver's prepare() is async. LazyStatement bridges this by storing the
* SQL and only calling conn.prepare() when run/get/all is invoked.
*
* The statement promise is cached for performance — options (raw, pluck,
* safeIntegers) are applied at execution time on the resolved statement.
*/
class LazyStatement {
private conn: Connection;
private sql: string;
private stmtPromise: Promise<ServerlessStatement> | null = null;
private _raw = false;
private _pluck = false;
private _safeIntegers = false;

constructor(conn: Connection, sql: string) {
this.conn = conn;
this.sql = sql;
}

private async getStmt(): Promise<ServerlessStatement> {
if (!this.stmtPromise) {
this.stmtPromise = this.conn.prepare(this.sql);
}
const stmt = await this.stmtPromise;
// Apply options at execution time so they reflect the current state
stmt.raw(this._raw);
stmt.pluck(this._pluck);
stmt.safeIntegers(this._safeIntegers);
return stmt;
}

raw(raw?: boolean): this {
this._raw = raw !== false;
return this;
}

pluck(pluck?: boolean): this {
this._pluck = pluck !== false;
return this;
}

safeIntegers(toggle?: boolean): this {
this._safeIntegers = toggle !== false;
return this;
}

columns(): any[] {
throw new Error("columns() requires an async prepare — not supported synchronously in serverless mode");
}

get source(): void {
return undefined;
}

get reader(): void {
return undefined;
}

get database(): any {
return undefined;
}

// Note: DatabasePromise Statement uses rest params (...args),
// but serverless Statement uses a single array param (args?).
// We collect rest params and forward as a single array.

async run(...args: any[]): Promise<{ changes: number; lastInsertRowid: number }> {
const stmt = await this.getStmt();
return stmt.run(args);
}

async get(...args: any[]): Promise<any> {
const stmt = await this.getStmt();
return stmt.get(args);
}

async all(...args: any[]): Promise<any[]> {
const stmt = await this.getStmt();
return stmt.all(args);
}

async *iterate(...args: any[]): AsyncGenerator<any> {
const stmt = await this.getStmt();
yield* stmt.iterate(args);
}

bind(..._args: any[]): this {
throw new Error("bind() is not supported in serverless mode — pass parameters to run/get/all instead");
}

interrupt(): void {}

close(): void {}
}

function notSupported(name: string): () => never {
return () => {
throw new Error(`${name}() is not supported in serverless mode`);
};
}

/**
* Wraps a @tursodatabase/serverless Connection to match
* the DatabasePromise interface expected by AgentFS.openWith().
*
* @example
* ```typescript
* import { connect } from "@tursodatabase/serverless";
* import { AgentFS } from "agentfs-sdk";
* import { createServerlessAdapter } from "agentfs-sdk/serverless";
*
* const conn = connect({
* url: "http://localhost:8080",
* });
*
* const db = createServerlessAdapter(conn);
* const agent = await AgentFS.openWith(db);
* ```
*/
export function createServerlessAdapter(conn: Connection): DatabasePromise {
return {
name: "serverless",
readonly: false,
open: true,
memory: false,
inTransaction: false,

async connect(): Promise<void> {
// Validate the connection by running a simple query
await conn.execute("SELECT 1");
},

prepare(sql: string): LazyStatement {
return new LazyStatement(conn, sql);
},

// DatabasePromise.transaction() returns a wrapper function that
// executes fn inside a transaction when called. The serverless
// driver's transaction() has the same shape.
transaction(fn: (...args: any[]) => Promise<any>) {
return (...bindParameters: any[]) => {
return conn.transaction(async () => {
return fn(...bindParameters);
});
};
},

async exec(sql: string): Promise<void> {
await conn.exec(sql);
},

pragma(): Promise<any[]> {
throw new Error("pragma() is not supported in serverless mode — pragmas are not available over HTTP");
},

backup: notSupported("backup"),
serialize: notSupported("serialize"),
function: notSupported("function"),
aggregate: notSupported("aggregate"),
table: notSupported("table"),
loadExtension: notSupported("loadExtension"),
maxWriteReplicationIndex: notSupported("maxWriteReplicationIndex"),
interrupt: notSupported("interrupt"),

defaultSafeIntegers(_toggle?: boolean) {},

async close(): Promise<void> {
await conn.close();
},
} as unknown as DatabasePromise;
}
32 changes: 32 additions & 0 deletions sdk/typescript/src/integrations/serverless/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Serverless integration for AgentFS.
*
* Provides an adapter that wraps `@tursodatabase/serverless` Connection
* to be compatible with AgentFS's DatabasePromise interface, enabling
* AgentFS to work with remote Turso databases over HTTP.
*
* @example
* ```typescript
* import { connect } from "@tursodatabase/serverless";
* import { AgentFS } from "agentfs-sdk";
* import { createServerlessAdapter } from "agentfs-sdk/serverless";
*
* const conn = connect({
* url: process.env.TURSO_DATABASE_URL!,
* authToken: process.env.TURSO_AUTH_TOKEN,
* });
*
* const db = createServerlessAdapter(conn);
* const agent = await AgentFS.openWith(db);
*
* await agent.fs.writeFile("/hello.txt", "Hello from Turso Cloud!");
* const content = await agent.fs.readFile("/hello.txt", "utf8");
* console.log(content);
*
* await agent.close();
* ```
*
* @see https://github.com/tursodatabase/agentfs/issues/156
*/

export { createServerlessAdapter } from "./adapter.js";
Loading
Loading