quansync

repository·main·Indexed 20 days ago

https://github.com/quansync-dev/quansync

A library for creating APIs that can be used both synchronously and asynchronously using a single logic definition. It allows developers to define behavior via implementation objects or generator functions, providing .sync() and .async() execution paths. Features include getIsAsync for mode detection, an all() utility for concurrent or sequential execution, and a build-time macro via unplugin-quansync for async/await syntax support.

Tokens
3.7K
Snippets
16
Records
16
Agent score
72%

What's inside quansync

  1. How quansync works: Creating sync/async APIs

    main

    The quansync function allows you to create a single function that can be executed either synchronously or asynchronously. You can define this behavior in two ways:

    1. Implementation Object: Provide an object with sync and async properties containing the respective implementations.
    2. Generator Function: Provide a generator function. Inside the generator, use yield* to call other quansync functions. This allows the logic to be shared between both modes.

    Once created, you can access the synchronous version via .sync() and the asynchronous version via .async() (or by awaiting the function directly).

    import fs from 'node:fs'
    import { quansync } from 'quansync'
    
    // Method 1: Implementation Object
    const readFile = quansync({
      sync: (path: string) => fs.readFileSync(path),
      async: (path: string) => fs.promises.readFile(path),
    })
    
    // Method 2: Generator Function
    const myFunction = quansync(function* (filename) {
      // Use `yield*` to call another quansync function
      const code = yield* readFile(filename, 'utf8')
      return `// some custom prefix\n${code}`
    })
    
    // Usage
    const result = myFunction.sync('./some-file.js') // Sync
    const asyncResult = await myFunction.async('./some-file.js') // Async
  2. Use the Build-time Macro for async/await syntax

    main

    If you prefer using standard async/await syntax instead of generator functions (function* and yield*), you can use the quansync/macro entry point. This requires the unplugin-quansync build-time macro to transform the code so that it still provides a .sync() method.

    Note: This approach requires a build step using unplugin-quansync.

    import fs from 'node:fs'
    import { quansync } from 'quansync/macro'
    
    const readFile = quansync({
      sync: (path: string) => fs.readFileSync(path),
      async: (path: string) => fs.promises.readFile(path),
    })
    
    // Use an async function instead of a generator
    const myFunction = quansync(async (filename) => {
      // Use `await` instead of `yield*`
      const code = await readFile(filename, 'utf8')
      return `// some custom prefix\n${code}`
    })
    
    const result = myFunction.sync('./some-file.js')
    const asyncResult = await myFunction.async('./some-file.js')
  3. Understand the QuansyncFn type

    main

    The QuansyncFn is the core abstraction of the library, representing a "superposition" function that can be consumed in both synchronous and asynchronous contexts.

    It provides two primary execution paths:

    1. .sync(...args): Returns a generator that yields values synchronously.
    2. .async(...args): Returns a promise that resolves to a generator, allowing for asynchronous yielding.

    Warning: The .sync and .async methods are consumed upon invocation and will be lost after the function is called.

    It also includes a .bind() method to allow binding a specific this context to the function.

    export type QuansyncFn<Return = any, Args extends any[] = []>
      = ((...args: Args) => QuansyncAwaitableGenerator<QuansyncUnwrapGenerator<Return>>)
        & {
          bind: <T, A extends any[], B extends any[]>(
            this: (this: T, ...args: [...A, ...B]) => QuansyncAwaitableGenerator<QuansyncUnwrapGenerator<Return>>,
            thisArg: T,
            ...args: A
          ) => ((...args: B) => QuansyncAwaitableGenerator<QuansyncUnwrapGenerator<Return>>),
          sync: (...args: Args) => QuansyncUnwrapGenerator<Return>,
          async: (...args: Args) => Promise<QuansyncUnwrapGenerator<Return>>
        }
  4. Use `getIsAsync` to detect execution mode

    main

    The getIsAsync() function returns a boolean indicating whether the current execution context is in async mode. This is useful inside a quansync generator to branch logic based on how the function was invoked.

    import { getIsAsync, quansync } from 'quansync'
    
    const fn = quansync(function* () {
      const isAsync: boolean = yield* getIsAsync()
      console.log(isAsync)
    })
    
    fn.sync() // false
    await fn() // true
    await fn.async() // true
  5. Use `all` to run multiple quansync functions

    main

    The all function allows you to run multiple quansync functions concurrently (in async mode) or sequentially (in sync mode), similar to Promise.all.

    import { all, quansync } from 'quansync'
    
    const loadFiles = quansync(function* () {
      return yield* all([
        readFile('./one.js'),
        readFile('./two.js'),
        readFile('./three.js'),
      ])
    })
    
    const results = loadFiles.sync()
    const asyncResults = await loadFiles.async()
  6. Configure tsdown for quansync

    main

    The tsdown.config.ts file defines the build configuration for the project using defineConfig from the tsdown package. This configuration specifies entry points, target platform, export behavior, and declaration file generation settings.

    import { defineConfig } from 'tsdown'
    
    export default defineConfig({
      entry: 'src/{index,macro}.ts',
      platform: 'neutral',
      inlineOnly: [],
      exports: true,
      dts: {
        tsgo: true,
      },
    })
  7. Convert a promise to a Quansync generator with toGenerator()

    main

    The toGenerator function converts a PromiseLike, a value, or an existing QuansyncGenerator into a QuansyncGenerator. This is useful for normalizing different types of inputs into a consistent generator interface that can be used with all() or other generator-based utilities.

    import { toGenerator } from 'quansync';
    
    const promise = Promise.resolve('hello');
    const generator = toGenerator(promise);
    // 'generator' is now a QuansyncGenerator
  8. Run multiple generators with all()

    main

    The all function is a Quansync utility that takes an iterable of QuansyncGenerator objects and resolves them.

    • In a sync context, it iterates through all generators synchronously and returns an array of their results.
    • In an async context, it returns a Promise that resolves to an array of the generators' results (using Promise.all internally).
    import { quansync, all } from 'quansync';
    
    const gen = quansync(function* (x: number) {
      return x * 2;
    });
    
    const generators = [gen(1), gen(2), gen(3)];
    
    // If called in an async context:
    const results = await all(generators);
    // results === [2, 4, 6]
  9. Use the quansync macro with unplugin-quansync

    main

    The quansync function exported from src/macro.ts is a specialized version of the core quansync function designed for use with the unplugin-quansync macro transformer. It allows for handling async functions by accepting a 'fake' argument type that the transformer resolves at build-time.

    Warning: Do NOT use this function directly in your runtime code without the unplugin-quansync transformer, as it is specifically designed for build-time macro expansion.

    import { quansync } from 'quansync/macro';
    
    // This usage requires the `unplugin-quansync` macro transformer
    const syncFn = quansync(async (data) => {
      // macro logic here
    });
  10. Create a Quansync function with quansync()

    main

    The quansync function creates a "superposition" function that can behave either synchronously or asynchronously depending on the context. You can initialize it using three different input types:

    1. An object with sync and async methods: Define explicit implementations for both modes.
    2. A generator function: Provide a generator that uses yield to switch between modes.
    3. A Promise or value: Wrap a promise so it can be treated as a sync value (throwing if a promise is encountered in a sync context) or an async value.

    When the resulting function is called, it detects the execution context. If called in an async context, it executes the async path; otherwise, it executes the sync path.

    import { quansync } from 'quansync';
    
    // 1. From an object with sync/async implementations
    const dual = quansync({
      sync: (x: number) => x + 1,
      async: async (x: number) => x + 1
    });
    
    // 2. From a generator function
    const gen = quansync(function* (x: number) {
      const isAsync = yield getIsAsync;
      if (isAsync) {
        return await Promise.resolve(x + 1);
      }
      return x + 1;
    });
    
    // 3. From a Promise
    const fromPromise = quansync(Promise.resolve(42));
  11. Configure QuansyncOptions

    main

    The QuansyncOptions interface allows you to provide a callback that triggers whenever a value is yielded during execution. This is useful for intercepting or logging values as they are produced.

    export interface QuansyncOptions {
      onYield?: (value: any, isAsync: boolean) => any
    }