Skip to content

fix: lazy connect in RemoteTransport for serverless cold-start resilience#1085

Merged
hotlong merged 1 commit intomainfrom
copilot/fix-remote-transport-initialization
Apr 8, 2026
Merged

fix: lazy connect in RemoteTransport for serverless cold-start resilience#1085
hotlong merged 1 commit intomainfrom
copilot/fix-remote-transport-initialization

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 8, 2026

RemoteTransport.ensureClient() throws immediately when @libsql/client is null, but ObjectQL.init() swallows driver.connect() failures. On Vercel cold starts, a transient connect failure leaves the transport uninitialized and restoreMetadataFromDb() crashes querying sys_metadata.

RemoteTransport

  • Replace synchronous ensureClient() with async ensureConnected() that invokes a connect factory when client is null
  • Add setConnectFactory() for caller-provided lazy initialization
  • De-duplicate concurrent reconnect attempts via singleton promise

TursoDriver

  • Register connect factory in constructor so the transport self-heals without requiring an explicit connect() call
// Before: throws if connect() failed or was never called
private ensureClient(): Client {
  if (!this.client) throw new Error('...not initialized...');
  return this.client;
}

// After: lazy-connects via factory, de-duplicates concurrent callers
private async ensureConnected(): Promise<Client> {
  if (this.client) return this.client;
  if (this.connectFactory) {
    if (!this.connectPromise) {
      this.connectPromise = this.connectFactory().then((c) => {
        this.client = c;
        this.connectPromise = null;
        return c;
      });
    }
    return this.connectPromise;
  }
  throw new Error('...not initialized...');
}

ObjectQL Engine

  • init() now tracks failed drivers and emits a warning with the list, instead of silently swallowing connect errors

Tests

  • 5 new cases: lazy connect on find/create/checkHealth, concurrent de-duplication, recovery after transport client loss

…art resilience

- Add connectFactory and async ensureConnected() to RemoteTransport with de-duplicated reconnection
- Register lazy connect factory in TursoDriver constructor for self-healing
- Improve ObjectQL.init() to track and warn about failed driver connections
- Add 5 tests for lazy connect, concurrent de-duplication, and recovery scenarios
- Update CHANGELOGs for driver-turso and objectql

Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/7435d290-93fc-4ff7-9032-c70211d64e0e

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Apr 8, 2026 7:31am
spec Ready Ready Preview, Comment Apr 8, 2026 7:31am

Request Review

@hotlong hotlong marked this pull request as ready for review April 8, 2026 07:31
Copilot AI review requested due to automatic review settings April 8, 2026 07:31
@github-actions github-actions bot added documentation Improvements or additions to documentation tests size/m labels Apr 8, 2026
@hotlong hotlong merged commit 0ae9b42 into main Apr 8, 2026
14 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves Turso remote-mode resilience in serverless/cold-start scenarios by introducing lazy (re)connection in RemoteTransport, wiring that capability into TursoDriver, and surfacing initial driver connection failures during ObjectQL.init().

Changes:

  • Add lazy-connect support to RemoteTransport via a caller-provided connect factory with concurrent connect de-duplication.
  • Register the lazy-connect factory in TursoDriver (remote mode) so CRUD/health calls can self-heal without a prior successful connect().
  • Update ObjectQL.init() to track failed driver connects and emit a warning listing them; add new Turso remote-mode tests for lazy connect/recovery.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/plugins/driver-turso/src/turso-driver.ts Registers a lazy connect factory on the remote transport in remote mode.
packages/plugins/driver-turso/src/remote-transport.ts Replaces sync ensureClient() usage with async ensureConnected() + connect factory + promise de-duplication.
packages/plugins/driver-turso/src/turso-driver.test.ts Adds test coverage for lazy connect, concurrent de-duplication, and recovery after client loss.
packages/objectql/src/engine.ts Tracks and warns about drivers that fail initial connect() during init.
packages/plugins/driver-turso/CHANGELOG.md + packages/objectql/CHANGELOG.md Adds release notes for the new behavior.

Comment on lines +246 to +256
if (this.tursoConfig.client) {
this.libsqlClient = this.tursoConfig.client;
} else {
const { createClient } = await import('@libsql/client');
this.libsqlClient = createClient({
url: this.tursoConfig.url,
authToken: this.tursoConfig.authToken,
concurrency: this.tursoConfig.concurrency,
});
}
return this.libsqlClient;
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lazy-connect factory returns this.libsqlClient, but libsqlClient is typed as Client | null and this function contains an await (dynamic import), so TypeScript won’t be able to narrow it to non-null. With strict: true this should be a type error (Promise<Client | null> not assignable to Promise<Client>). Return a non-null local client variable (or assert non-null) so setConnectFactory always receives Promise<Client>.

Suggested change
if (this.tursoConfig.client) {
this.libsqlClient = this.tursoConfig.client;
} else {
const { createClient } = await import('@libsql/client');
this.libsqlClient = createClient({
url: this.tursoConfig.url,
authToken: this.tursoConfig.authToken,
concurrency: this.tursoConfig.concurrency,
});
}
return this.libsqlClient;
let client: Client;
if (this.tursoConfig.client) {
client = this.tursoConfig.client;
} else {
const { createClient } = await import('@libsql/client');
client = createClient({
url: this.tursoConfig.url,
authToken: this.tursoConfig.authToken,
concurrency: this.tursoConfig.concurrency,
});
}
this.libsqlClient = client;
return client;

Copilot uses AI. Check for mistakes.
Comment on lines 61 to 66
/**
* Set the @libsql/client instance used for all queries.
*/
setClient(client: Client): void {
this.client = client;
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteTransport.client is explicitly Client | null, but setClient only accepts Client. This forces callers/tests to use null as any to simulate a lost client, and makes “client cleared” a first-class scenario without a typed API. Consider changing the signature to setClient(client: Client | null) (or adding a clearClient() method) so self-healing flows don’t require unsafe casts.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +76
* becomes unavailable (e.g., serverless cold-start, transient error).
*/
setConnectFactory(factory: () => Promise<Client>): void {
this.connectFactory = factory;
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setConnectFactory() updates the factory but leaves any existing connectPromise intact. If the factory is overridden while a lazy-connect is in flight (or after a failed/stuck attempt), subsequent callers can still await the old promise and end up with a client created by the previous factory. Consider resetting connectPromise (and/or guarding against factory changes during an in-flight connect) when setting a new factory.

Suggested change
* becomes unavailable (e.g., serverless cold-start, transient error).
*/
setConnectFactory(factory: () => Promise<Client>): void {
this.connectFactory = factory;
* becomes unavailable (e.g., serverless cold-start, transient error).
*
* Replacing the factory invalidates any cached lazy-connect attempt so
* subsequent callers cannot continue awaiting a promise created by the
* previous factory.
*/
setConnectFactory(factory: () => Promise<Client>): void {
this.connectFactory = factory;
this.connectPromise = null;

Copilot uses AI. Check for mistakes.
Comment on lines 114 to 118
async execute(command: unknown, params?: unknown[]): Promise<unknown> {
this.ensureClient();
await this.ensureConnected();
if (typeof command !== 'string') return command;

const stmt: InStatement = params && params.length > 0
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In execute(), await this.ensureConnected() relies on the side-effect of setting this.client, but the method still uses this.client! later. This is vulnerable to races if another task calls close()/setClient() between awaits, and it also keeps the non-null assertion. Prefer const client = await this.ensureConnected() and use that local for the rest of the method (same applies to other CRUD methods).

Copilot uses AI. Check for mistakes.
// Clear only the transport's reference (simulates stale state) and point
// the factory at a fresh, working client.
const transport = driver.getRemoteTransport()!;
transport.setClient(null as unknown as any);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test clears the transport client via null as unknown as any, which is an unsafe cast and currently required because RemoteTransport.setClient doesn’t accept null. Once the transport exposes a typed way to clear the client (e.g., setClient(null) / clearClient()), update the test to use it directly to keep the type-safety and intent clear.

Suggested change
transport.setClient(null as unknown as any);
Reflect.set(transport as object, 'client', undefined);

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +8
## 4.0.3

### Patch Changes

- fix: implement lazy connect in RemoteTransport to self-heal from serverless cold-start failures, transient network errors, or missed `connect()` calls. The transport now accepts a connect factory and auto-initializes the @libsql/client on first operation when the client is not yet available. Concurrent reconnection attempts are de-duplicated.

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a ## 4.0.3 changelog entry, but package.json for this package is still 4.0.2. Given the repo uses Changesets (.changeset/config.json) and other packages keep changelog/package.json versions aligned, either bump the package version(s) to 4.0.3 as part of the fixed release group or replace this manual changelog edit with an appropriate .changeset/*.md entry.

Suggested change
## 4.0.3
### Patch Changes
- fix: implement lazy connect in RemoteTransport to self-heal from serverless cold-start failures, transient network errors, or missed `connect()` calls. The transport now accepts a connect factory and auto-initializes the @libsql/client on first operation when the client is not yet available. Concurrent reconnection attempts are de-duplicated.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +8
## 4.0.3

### Patch Changes

- fix: ObjectQL.init() now tracks and warns about failed driver connections instead of silently swallowing errors, improving debuggability for cold-start and serverless issues.

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces a ## 4.0.3 changelog entry, but packages/objectql/package.json still reports version 4.0.2. Since this repo is configured for Changesets and other packages keep changelog/version in sync, consider either bumping the package version(s) as part of the release (fixed group) or switching to a Changeset file instead of manually editing the changelog.

Suggested change
## 4.0.3
### Patch Changes
- fix: ObjectQL.init() now tracks and warns about failed driver connections instead of silently swallowing errors, improving debuggability for cold-start and serverless issues.

Copilot uses AI. Check for mistakes.
Comment on lines +609 to +611
this.logger.warn(
`${failedDrivers.length} of ${this.drivers.size} driver(s) failed initial connect. ` +
`Operations may recover via lazy reconnection or fail at query time.`,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObjectQL.init() now warns when one or more drivers fail initial connect(). Since packages/objectql/src/engine.test.ts already covers init behavior, consider adding a test case where driver.connect() rejects to assert: (1) init still completes without throwing, and (2) a warning is emitted with the failed driver names. This will prevent regressions in the new debuggability behavior.

Suggested change
this.logger.warn(
`${failedDrivers.length} of ${this.drivers.size} driver(s) failed initial connect. ` +
`Operations may recover via lazy reconnection or fail at query time.`,
const failedDriverList = failedDrivers.join(', ');
this.logger.warn(
`${failedDrivers.length} of ${this.drivers.size} driver(s) failed initial connect ` +
`(${failedDriverList}). Operations may recover via lazy reconnection or fail at query time.`,

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants