Skip to main content

Strategy Registry

WIP

Register and resolve upload strategies by id, and write your own.

The registry owns the set of available strategies and keeps the engine decoupled from any concrete protocol. createStrategyRegistry() returns an empty registry — you populate it with .set().

Why it exists

  • The core never imports strategy implementations.
  • Strategies can be swapped or added without changing engine logic.
  • Registry key types come from your intent map M, so the backend's intent response and the registered strategies are kept in sync at compile time.

API

Contracts.Strategy.Registry<M, C, P, R>:

type Registry<M, C, P, R> = {
  get<K>(id: K): Contracts.Strategy.Me<M, C, P, R, K> | undefined
  has(id: string): id is keyof M & string
  set<K>(strategy: Contracts.Strategy.Me<M, C, P, R, K>): void
}

Usage

import { PostStrategy, multipartStrategy, createStrategyRegistry } from '@gentleduck/upload/strategies'

const strategies = createStrategyRegistry<Intents, Cursors, Purpose, Result>()
strategies.set(PostStrategy())
strategies.set(multipartStrategy())

There is no array constructor form — always create empty and set() each strategy.

How resolution works

When your backend returns an intent, the engine reads its strategy field and calls registry.get(strategy). A hit runs that strategy's start(). A miss moves the item to the error phase with a strategy_missing error naming the unregistered id.

Writing a custom strategy

A strategy is a plain object with id, resumable, and start. The context gives you the file, the intent, the injected transport, your API, progress reporting, and cursor helpers.

import type { Contracts } from '@gentleduck/upload/core'

type MyIntent = Contracts.Intent.Base<'my-put'> & { uploadUrl: string; token: string }
type MyCursor = { bytesUploaded: number }

function myPutStrategy<
  M extends Contracts.Intent.Map & { 'my-put': MyIntent },
  C extends Contracts.Cursor.Map<M> & { 'my-put'?: MyCursor },
  P extends string,
  R extends Contracts.Result.Base,
>(): Contracts.Strategy.Me<M, C, P, R, 'my-put'> {
  return {
    id: 'my-put',      // must equal the intent's `strategy`
    resumable: true,
    async start(ctx) {
      const offset = ctx.readCursor()?.bytesUploaded ?? 0
      await ctx.transport.put({
        url: ctx.intent.uploadUrl,
        body: ctx.file.slice(offset),
        headers: { authorization: `Bearer ${ctx.intent.token}` },
        signal: ctx.signal,
        onProgress: (uploaded) =>
          ctx.reportProgress({ uploadedBytes: offset + uploaded, totalBytes: ctx.file.size }),
      })
      ctx.persistCursor({ bytesUploaded: ctx.file.size } as C['my-put'])
    },
  }
}

Register it and have the backend return { strategy: 'my-put', ... }:

strategies.set(myPutStrategy())

The id must exactly match the intent's strategy field, and both must be keys of your intent map — the registry's generics enforce it.