Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Missing typing for sqlite3Worker1Promiser #53

Open
benjamin-wright opened this issue Nov 25, 2023 · 9 comments · May be fixed by #54
Open

Missing typing for sqlite3Worker1Promiser #53

benjamin-wright opened this issue Nov 25, 2023 · 9 comments · May be fixed by #54

Comments

@benjamin-wright
Copy link

In order to follow the recommended "wrapped worker" approach from the readme I had to add:

declare module '@sqlite.org/sqlite-wasm' {
  export function sqlite3Worker1Promiser(...args: any): any
}

immediately below the import statement, because sqlite3Worker1Promiser is not included in the index.d.ts

@tomayac tomayac linked a pull request Nov 27, 2023 that will close this issue
@tomayac
Copy link
Collaborator

tomayac commented Nov 27, 2023

I'm not much of a TypeScript person, but the any suggests there's more work to be done. Started #54. Feel free to pile on. FYI @jbaiter.

@filipe-freire
Copy link

Hey there! Firstly, thanks for shipping this package in the first place! :)

I understand that having this typed as "any" is far from ideal, and even saw the ongoing PR to add the type definitions to it. Not an easy job!

However, it would make a potential developer less confused when using the package with Typescript if the type was shipped by default instead of having to declare it manually. Especially since the recommended approach uses "sqlite3Worker1Promiser".

Maybe we could add a JSDoc noting that the type is under development and keep it as something along the lines of:

(...args: unknown[]) => Promise<unknown>

I know there's nothing more permanent than a temporary solution, so I understand if we don't follow with this approach too 😅

@tomayac
Copy link
Collaborator

tomayac commented Oct 7, 2024

@filipe-freire I'm not opposed, but am unsure where you'd add this, the caveat from #53 (comment) still applies 🫣. To the thin wrapper code part of this repo (if so, please file a quick PR), or the underlying SQLite code (if so, that's something to raise with @sgbeal)?

@sgbeal
Copy link
Collaborator

sgbeal commented Oct 7, 2024

To the thin wrapper code part of this repo (if so, please file a quick PR), or the underlying SQLite code (if so, that's something to raise with @sgbeal)?

In the upstream project we use only vanilla JS and avoid all tooling-specific extensions (because none of us use them, so we can't be relied upon to maintain such pieces properly long-term).

@filipe-freire
Copy link

filipe-freire commented Oct 7, 2024

Never mind my comment above, after analyzing the PR more thoroughly the work there is already way beyond what I suggested. Apologies for that 😅 The work would be done in this wrapper though.

I unfortunately don't have the insight needed to complete that PR. However, an option is to merge it as it stands right now, which would still provide a better DX to someone just starting out.

The only thing needed would be to change the Docs to reflect the types added:

import {
    sqlite3Worker1Promiser,
    type Promiser,
    type PromiserResponseSuccess
} from '@sqlite.org/sqlite-wasm';

const log = console.log;
const error = console.error;

const initializeSQLite = async () => {
    try {
        log('Loading and initializing SQLite3 module...');

        const promiser = (await new Promise((resolve) => {
            const _promiser = sqlite3Worker1Promiser({
                onready: () => resolve(_promiser)
            });
        })) satisfies Promiser;

        log('Done initializing. Running demo...');

        const configResponse = (await promiser(
            'config-get', {}
        )) as PromiserResponseSuccess<'config-get'>;
        log('Running SQLite3 version', configResponse.result.version.libVersion);

        const openResponse = (await promiser('open', {
            filename: 'file:mydb.sqlite3?vfs=opfs'
        })) as PromiserResponseSuccess<'open' >;
        const {
            dbId
        } = openResponse;
        log(
            'OPFS is available, created persisted database at',
            openResponse.result.filename.replace(/^file:(.*?)\?vfs=opfs$/, '$1')
        );
        // Your SQLite code here.
    } catch (err) {
        if (!(err instanceof Error)) {
            err = new Error(err.result.message);
        }
        error(err.name, err.message);
    }
};

initializeSQLite();

Type casting is not an ideal solution, yet it might be a good enough compromise while this work is ongoing. I'll defer the decision to you! ✌🏻

@tomayac
Copy link
Collaborator

tomayac commented Oct 7, 2024

I don't know how merge-ready #54 is. Do people here generally approve of a not-perfect-but-good-enough merger? I think for the documentation, we should keep it vanilla. Those wanting to use the types will know how to include them, whereas people unfamiliar with TypeScript might be wondering what the to them weird type imports are.

@filipe-freire
Copy link

filipe-freire commented Oct 7, 2024

How about simply adding a Typescript version for its users out there? Having them figure it out by themselves is not the best solution here imo...

As for the approval of "not-perfect-but-good-enough" PR's, I'll defer that to y'all 😁

@mkjawadi
Copy link

Any update on this? I cannot follow the recommended "wrapped worker" approach in my Vite React + TypeScript project due to sqlite3Worker1Promiser issue. Is there any demo of this approach with TypeScript? Thanks in advance.

@binajmen
Copy link

@mkjawadi

You can inspire yourself from the pending PR (#54) and create a <pick your name>.d.ts with this content:

declare module "@sqlite.org/sqlite-wasm" {
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
  type TODO = any;

  /**
   * A function to be called when the SQLite3 module and worker APIs are done
   * loading asynchronously. This is the only way of knowing that the loading
   * has completed.
   *
   * @since V3.46: Is passed the function which gets returned by
   *   `sqlite3Worker1Promiser()`, as accessing it from this callback is more
   *   convenient for certain usage patterns. The promiser v2 interface obviates
   *   the need for this callback.
   */
  type OnreadyFunction = () => void;

  type Sqlite3Worker1PromiserConfig = {
    onready?: OnreadyFunction;
    /**
     * A worker instance which loads `sqlite3-worker1.js`, or a functional
     * equivalent. Note that the promiser factory replaces the
     * `worker.onmessage` property. This config option may alternately be a
     * function, in which case this function is called to instantiate the
     * worker.
     */
    worker?: Worker | (() => Worker);
    /** Function to generate unique message IDs */
    generateMessageId?: (messageObject: TODO) => string;
    /**
     * A `console.debug()` style function for logging information about Worker
     * messages.
     */
    // biome-ignore lint/suspicious/noExplicitAny: <explanation>
    debug?: (...args: any[]) => void;
    /**
     * A callback function that is called when a `message` event is received
     * from the worker, and the event is not handled by the proxy.
     *
     * @note This *should* ideally never happen, as the proxy aims to handle
     * all known message types.
     */
    onunhandled?: (event: MessageEvent) => void;
  };

  /**
   * A db identifier string (returned by 'open') which tells the operation which
   * database instance to work on. If not provided, the first-opened db is
   * used.
   *
   * @warning This is an "opaque" value, with no inherently useful syntax
   * or information. Its value is subject to change with any given build
   * of this API and cannot be used as a basis for anything useful beyond
   * its one intended purpose.
   */
  type DbId = string | undefined;
  type Sqlite3Version = {
    libVersion: string;
    sourceId: string;
    libVersionNumber: number;
    downloadVersion: number;
  };

  // Message types and their corresponding arguments and results. Should be able to get better types for some of these (open, exec and stack) from the existing types, although the Promiser verions have minor differences
  type PromiserMethods = {
    /** @link https://sqlite.org/wasm/doc/trunk/api-worker1.md#method-open */
    open: {
      args: Partial<
        {
          /**
           * The db filename. [=":memory:" or "" (unspecified)]: TODO: See the
           * sqlite3.oo1.DB constructor for peculiarities and transformations
           */
          filename?: string;
        } & {
          /**
           * Sqlite3_vfs name. Ignored if filename is ":memory:" or "". This may
           * change how the given filename is resolved. The VFS may optionally
           * be provided via a URL-style filename argument: filename:
           * "file:foo.db?vfs=...". By default it uses a transient database,
           * created anew on each request.
           *
           * If both this argument and a URI-style argument are provided, which
           * one has precedence is unspecified.
           */
          vfs?: string;
        }
      >;
      result: {
        dbId: DbId;
        /** Db filename, possibly differing from the input */
        filename: string;
        /**
         * Indicates if the given filename resides in the known-persistent
         * storage
         */
        persistent: boolean;
        /** Name of the underlying VFS */
        vfs: "string";
      };
      /** @link https://sqlite.org/wasm/doc/trunk/api-worker1.md#method-close */
    };
    close: {
      args: { dbId?: DbId };
      result: {
        /** Filename of closed db, or undefined if no db was closed */
        filename: string | undefined;
      };
      /** @link https://sqlite.org/wasm/doc/trunk/api-worker1.md#method-config-get */
    };
    "config-get": {
      args: unknown;
      result: {
        dbID: DbId;
        version: Sqlite3Version;
        /** Indicates if BigInt support is enabled */
        bigIntEnabled: boolean;
        /** Indicates if opfs support is enabled */
        opfsEnabled: boolean; //not documented on sqlie.org?
        /** Result of sqlite3.capi.sqlite3_js_vfs_list() */
        vfsList: string[]; // is there a full list somewhere I can use?
      };
    };
    /**
     * Interface for running arbitrary SQL. Wraps`oo1.DB.exec()` methods. And
     * supports most of its features as defined in
     * https://sqlite.org/wasm/doc/trunk/api-oo1.md#db-exec. There are a few
     * limitations imposed by the state having to cross thread boundaries.
     *
     * @link https://sqlite.org/wasm/doc/trunk/api-worker1.md#method-exec
     */
    exec: {
      args: {
        sql: string;
        dbId?: DbId;
        /**
         * At the end of the result set, the same event is fired with
         * (row=undefined, rowNumber=null) to indicate that the end of the
         * result set has been reached. Note that the rows arrive via
         * worker-posted messages, with all the implications of that.
         */
        callback?: (result: {
          /**
           * Internally-synthesized message type string used temporarily for
           * worker message dispatching.
           */
          type: string;
          /** Sqlilte3 VALUE */
          row: TODO;
          /** 1-based index */
          rowNumber: number;
          columnNames: string[];
        }) => void;
        /**
         * A single value valid as an argument for Stmt.bind(). This is only
         * applied to the first non-empty statement in the SQL which has any
         * bindable parameters. (Empty statements are skipped entirely.)
         */
        bind?: Exclude<TODO, null>;
        [key: string]: TODO; //
      };
      result: { [key: string]: TODO };
    };
  };

  type PromiserResponseSuccess<T extends keyof PromiserMethods> = {
    /** Type of the inbound message */
    type: T;
    /** Operation dependent result */
    result: PromiserMethods[T]["result"];
    /** Same value, if any, provided by the inbound message */
    messageId: string;
    /**
     * The id of the db which was operated on, if any, as returned by the
     * corresponding 'open' operation.
     */
    dbId: DbId;
    // possibly other metadata ...
    /* 
    WorkerReceivedTime: number
    WorkerRespondTime: number
    departureTime: number
     */
  };

  type PromiserResponseError = {
    type: "error";
    /** Operation independent object */
    result: {
      /** Type of the triggereing operation */
      operation: string;
      /** Error Message */
      message: string;
      /** The ErrorClass.name property from the thrown exception */
      errorClass: string;
      /** The message object which triggered the error */
      input: object;
      /** _if available_ a stack trace array */
      stack: TODO[];
    };
    /** Same value, if any, provided by the inbound message */
    messageId: string;
    dbId: DbId;
  };
  type PromiserResponse<T extends keyof PromiserMethods> =
    | PromiserResponseSuccess<T>
    | PromiserResponseError;

  type Promiser = {
    <T extends keyof PromiserMethods>(
      /** The type of the message */
      messageType: T,
      /** The arguments for the message type */
      messageArguments: PromiserMethods[T]["args"],
    ): Promise<PromiserResponse<T>>;

    <T extends keyof PromiserMethods>(message: {
      /** The type of the message */
      type: T;
      /** The arguments for the message type */
      args: PromiserMethods[T]["args"];
    }): Promise<PromiserResponse<T>>;
  };

  /** Factory for creating promiser instances. */
  const sqlite3Worker1Promiser: {
    /**
     * Promiser v1
     *
     * @example
     *   const factory = sqlite3Worker1Promiser({
     *     onready: () => {
     *       promiser('open', { filename: 'my_database.sqlite' })
     *         .then((msg) => {
     *           // ...
     *         })
     *         .catch((e) => {
     *           console.error(e);
     *         });
     *     },
     *   });
     *
     * @link https://sqlite.org/wasm/doc/trunk/api-worker1.md#promiser
     */
    (config?: Sqlite3Worker1PromiserConfig | OnreadyFunction): Promiser;

    /**
     * Promiser v2
     *
     * @since 3.46:
     * @example
     *   const factoryPromise = sqlite3Worker1Promiser.v2(config);
     *   const factory = await factoryPromise;
     *
     * @link https://sqlite.org/wasm/doc/trunk/api-worker1.md#promiser.v2
     */
    v2: (
      config?: Sqlite3Worker1PromiserConfig | OnreadyFunction,
    ) => Promise<Promiser>;
    defaultConfig: Sqlite3Worker1PromiserConfig;
  };
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants