Skip to content
← Public packages

@kentcdodds/package-storage-migrations

Ordered idempotent packageStorage schema migrations + isolate-memoized runner. Caller always passes storage.

AGENTS.md

121 lines · 3.3 KB · Markdown

@kentcdodds/package-storage-migrations — agent playbook

Human Intent lives in README.md. This file is the agent runbook.

Positive defaults

  • Import the runner; pass the caller's storage:
    import {
      runPackageStorageMigrations,
      createMigrationRunner,
    } from 'kody:@kentcdodds/package-storage-migrations'
    import { packageStorage } from 'kody:runtime'
    
    const ensureSchema = createMigrationRunner({
      storage: packageStorage(),
      versionKey: 'my-app:schema-version',
      migrations: [/* version >= 1, unique, ascending when sorted */],
    })
    await ensureSchema()
  • Prefer createMigrationRunner at module scope for Worker handlers so schema work runs at most once per isolate boot.
  • Never call packageStorage() inside product migration helpers that live in this package — stamp would hit this package's bucket. Only ./smoke may use this package's storage for self-test.
  • Version integers must be unique integers >= 1. Duplicate versions throw.

Imports

SpecifierUse
kody:@kentcdodds/package-storage-migrationsrunPackageStorageMigrations, createMigrationRunner, types
kody:@kentcdodds/package-storage-migrations/smokePost-publish roundtrip against this package's bucket

Smoke tests

After publish (./smoke)
import smoke from 'kody:@kentcdodds/package-storage-migrations/smoke'

export default async function main() {
	return await smoke()
}

Expect { ok: true, secondApplied: 0, sharedEnsure: true, … }.

Caller-style execute (copy-paste)
import {
	runPackageStorageMigrations,
	createMigrationRunner,
} from 'kody:@kentcdodds/package-storage-migrations'

export default async function main() {
	const map = new Map()
	const storage = {
		get: async (k) => map.get(k),
		set: async (k, v) => void map.set(k, v),
		delete: async (k) => void map.delete(k),
	}
	await storage.set('notes-v1', [
		{ id: '1', text: 'hi', createdAt: '2026-01-01T00:00:00.000Z' },
	])
	const migrations = [
		{
			version: 1,
			name: 'notes-to-document',
			async up(s) {
				const legacy = await s.get('notes-v1')
				if (Array.isArray(legacy)) {
					await s.set('notes', { items: legacy })
					await s.delete('notes-v1')
				}
			},
		},
	]
	const first = await runPackageStorageMigrations({
		storage,
		versionKey: 't:v',
		migrations,
	})
	const second = await runPackageStorageMigrations({
		storage,
		versionKey: 't:v',
		migrations,
	})
	const ensure = createMigrationRunner({
		storage,
		versionKey: 't:v',
		migrations,
	})
	const a = await ensure()
	const b = await ensure()
	return {
		first,
		second,
		idempotent: second.applied.length === 0,
		sharedEnsure: a === b,
	}
}

Edge cases

  • Missing / non-numeric version key → treat as 0 (all migrations pending).
  • String numeric versions in storage are accepted and floored.
  • Failed up leaves version unset for that step; runner promise is cleared so the next ensure() can retry.
  • Visibility stays private unless Kent asks to publish to /community.

Do not

  • Rewrite @kentcdodds/package-app-kit to depend on this until Patch confirms realtime tip bd4c9a4 (or newer) is published.
  • Touch realtime files from this package lane.
  • Call packageStorage() on the factory / public API path (smoke only).