@vielzeug/codex 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -107
- package/data/catalog.json +1679 -0
- package/data/llms-full.txt +18997 -31516
- package/data/llms.txt +32 -110
- package/data/manifest.json +8 -0
- package/data/packages/arsenal.json +210 -0
- package/data/packages/assay.json +40 -0
- package/data/packages/clockwork.json +67 -0
- package/data/packages/codex.json +43 -0
- package/data/packages/coins.json +103 -0
- package/data/packages/conduit.json +60 -0
- package/data/packages/courier.json +58 -0
- package/data/packages/dnd.json +75 -0
- package/data/packages/familiar.json +30 -0
- package/data/packages/flux.json +93 -0
- package/data/packages/forge.json +84 -0
- package/data/packages/herald.json +122 -0
- package/data/packages/keymap.json +65 -0
- package/data/packages/ledger.json +54 -0
- package/data/packages/lingua.json +66 -0
- package/data/packages/orbit.json +112 -0
- package/data/packages/ore.json +73 -0
- package/data/packages/prism.json +70 -0
- package/data/packages/pulse.json +58 -0
- package/data/packages/refine.json +12 -0
- package/data/packages/ripple.json +79 -0
- package/data/packages/rune.json +81 -0
- package/data/packages/sandbox.json +39 -0
- package/data/packages/scout.json +60 -0
- package/data/packages/scroll.json +113 -0
- package/data/packages/sourcerer.json +74 -0
- package/data/packages/spell.json +134 -0
- package/data/packages/tempo.json +113 -0
- package/data/packages/vault.json +90 -0
- package/data/packages/ward.json +125 -0
- package/data/packages/wayfinder.json +113 -0
- package/data/refine.json +11752 -0
- package/data/search.json +1437 -0
- package/dist/catalog.js +149 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cli.js +33 -59
- package/dist/cli.js.map +1 -1
- package/dist/errors.js +0 -14
- package/dist/errors.js.map +1 -1
- package/dist/http.js +54 -96
- package/dist/http.js.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/server.js +4 -9
- package/dist/server.js.map +1 -1
- package/dist/snapshot.js +233 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/tools/index.js +21 -42
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/packages.js +67 -166
- package/dist/tools/packages.js.map +1 -1
- package/dist/tools/refine.js +99 -305
- package/dist/tools/refine.js.map +1 -1
- package/dist/tools/schema.js +8 -8
- package/dist/tools/schema.js.map +1 -1
- package/dist/tools/shared.js +1 -26
- package/dist/tools/shared.js.map +1 -1
- package/dist/types.js +1 -2
- package/dist/types.js.map +1 -1
- package/mcp-setup.json +10 -0
- package/package.json +7 -7
- package/data/.cache.json +0 -33
- package/data/vielzeug-data.json +0 -15999
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "// Re-export Temporal namespace so consumers never need to import @js-temporal/polyfill directly.\nexport { Temporal } from '@js-temporal/polyfill';\n\n// ─── Errors ───────────────────────────────────────────────────────────────────\nexport {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';\n\n// ─── Core ─────────────────────────────────────────────────────────────────────\nexport {\n difference,\n isValid,\n now,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDate,\n parsePlainDateTime,\n parseZoned,\n shift,\n} from './core';\n\n// ─── Conversion utilities ─────────────────────────────────────────────────────\nexport { inTz, toInstant } from './_convert';\n\n// ─── Boundary ─────────────────────────────────────────────────────────────────\nexport { endOf, startOf } from './boundary';\n\n// ─── Compare ──────────────────────────────────────────────────────────────────\nexport { clamp, isAfter, isBefore, isSame, within } from './compare';\n\n// ─── Format ───────────────────────────────────────────────────────────────────\nexport {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';\n\n// ─── Classify ─────────────────────────────────────────────────────────────────\nexport { expires, timeDiff } from './classify';\n\n// ─── Range ────────────────────────────────────────────────────────────────────\nexport { dateRange, recurrence } from './range';\n\n// ─── Public types ─────────────────────────────────────────────────────────────\nexport type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';\n",
|
|
3
|
+
"docs": {
|
|
4
|
+
"index": "---\ntitle: Tempo — Temporal date and time utilities\ndescription: Temporal-powered parsing, timezone conversion, arithmetic (DST-safe), and Intl formatting for modern TypeScript.\npackage: tempo\ncategory: time\nkeywords: [temporal, date-time, timezone, formatting, arithmetic, dst, intl, calendar]\nrelated: [arsenal]\nexports:\n [\n now,\n nowInstant,\n parse,\n parsePlainDate,\n parsePlainDateTime,\n parseInstant,\n parseZoned,\n isValid,\n toInstant,\n inTz,\n shift,\n difference,\n within,\n clamp,\n isBefore,\n isAfter,\n isSame,\n startOf,\n endOf,\n format,\n formatParts,\n formatRange,\n formatRangeParts,\n formatInstant,\n formatZoned,\n formatRelative,\n parseDuration,\n formatDuration,\n expires,\n timeDiff,\n humanize,\n dateRange,\n recurrence,\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"tempo\" />\n\n## Why Tempo?\n\nManual date handling breaks at daylight-saving boundaries, timezone edges, and DST transitions.\n\n```ts\n// Before — fragile, loses timezone context\nconst reminder = new Date(meeting.getTime() - 15 * 60_000);\n\n// After — DST-safe, handles transitions correctly\nconst reminder = shift(meeting, { minutes: -15 });\n```\n\n| Feature | Tempo | date-fns | Day.js | Native Date |\n| -------------- | ---------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------ | -------------------------------------- |\n| Bundle size | <PackageInfo package=\"tempo\" type=\"size\" /> | ~10 kB | ~3 kB | 0 kB |\n| DST-safe math | <ore-icon name=\"check\" size=\"16\"></ore-icon> (Temporal) | Manual | Manual | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Timezone aware | <ore-icon name=\"check\" size=\"16\"></ore-icon> Full support | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n| Immutable | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Format presets | <ore-icon name=\"check\" size=\"16\"></ore-icon> (`'short'`, `'medium'`, `'long'`, etc.) | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Type inference | <ore-icon name=\"check\" size=\"16\"></ore-icon> Full TypeScript | Partial | Partial | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Tempo when** you need reliable timezone handling, DST-safe arithmetic, and clean Temporal-based APIs without heavy dependencies.\n\n**Consider alternatives when** you need extensive locale data (date-fns).\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/tempo\n```\n\n```sh [npm]\nnpm install @vielzeug/tempo\n```\n\n```sh [yarn]\nyarn add @vielzeug/tempo\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { format, formatInstant, inTz, parsePlainDateTime, shift, toInstant } from '@vielzeug/tempo';\n\n// Parse a wall-clock string (no timezone attached)\nconst localMeeting = parsePlainDateTime('2026-03-21T10:30:00');\n\n// Convert to an absolute instant using the user's timezone\nconst meetingInstant = toInstant(localMeeting, { tz: 'America/New_York' });\n\n// Project to a zoned view and subtract 15 minutes (DST-safe)\nconst meetingNY = inTz(meetingInstant, 'America/New_York');\nconst reminder = shift(meetingNY, { minutes: -15 });\n\n// Format for display\nconst text = format(reminder, { pattern: 'short', locale: 'en-US', tz: 'America/New_York' });\n\n// Format for APIs/logs (stable UTC instant string)\nconst stable = formatInstant(reminder);\n```\n\n> **No `Temporal.*` imports needed.** Tempo re-exports `Temporal` and provides `parseInstant`, `parseZoned`, `parsePlainDateTime`, `parsePlainDate`, `nowInstant`, and `now` as drop-in replacements for every common Temporal constructor. Use `parse(input, as?)` for flexible input detection.\n\n## Features\n\n<div class=\"features-grid\">\n\n- **Zero Temporal imports** — `parseInstant()`, `parseZoned()`, `parsePlainDateTime()`, `parsePlainDate()`, `nowInstant()`, `now()` replace every common `Temporal.*` constructor; import only from `@vielzeug/tempo`\n- **DST-safe arithmetic** — `shift()` handles transitions correctly; always returns `ZonedDateTime` (call `.toInstant()` if needed)\n- **Timezone conversion** — `inTz()` to project any input into a timezone, `toInstant()` to normalize to UTC; invalid timezone strings throw `TempoError`\n- **Formatting split by intent** — `format()` for UI (with presets and `intl` escape hatch), `formatInstant()` for UTC strings, `formatZoned()` for zoned strings\n- **Relative and range formatting** — `formatRelative()` for UX copy, `formatRange()` / `formatRangeParts()` for localized time spans, `formatParts()` for custom rendering\n- **Range + comparison helpers** — `within()`, `clamp()`, `isBefore()`, `isAfter()`, `isSame()` with calendar-unit and week-start support\n- **Boundary helpers** — `startOf()` and `endOf()` for day/week/month/year-style snapping\n- **Duration tools** — `difference()`, `parseDuration()`, `formatDuration()`\n- **Expiry classification** — `expires()` for flexible threshold-based TTL bucketing; `timeDiff()` for structured time differences; `humanize()` for human-readable output\n- **Recurrence generation** — `recurrence()` for lazily generating repeating dates (daily/weekly/monthly/yearly); `dateRange()` for step-based date sequences; timezone inferred from `ZonedDateTime` inputs\n- **Intl integration** — formatting respects locale and calendar systems\n- **Polyfilled Temporal** — works in runtimes without native support via `@js-temporal/polyfill`\n- <PackageInfo package=\"tempo\" type=\"size\" /> gzipped\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Spell](/spell/) — schema validation with a similar `v` namespace pattern; combine with Tempo's date validators for typed form fields that accept date strings\n- [Rune](/rune/) — structured logger; use Tempo to format timestamps consistently in log entries and audit trails\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Tempo — API Reference\ndescription: Complete API reference for @vielzeug/tempo date/time functions.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Common gotcha |\n| ---------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------- |\n| `now(tz)` | Current zoned date/time | Requires a valid IANA timezone string |\n| `nowInstant()` | Current UTC instant | Use instead of `Temporal.Now.instant()` |\n| `parse(input, as?)` | Parse any ISO 8601 string; `as` pins the return type | Without `as`, auto-detects: ZonedDateTime → Instant → PlainDateTime → PlainDate |\n| `parseInstant(input)` | Parse a UTC ISO string to `Instant` | Input must end in `Z` or include an offset |\n| `parseZoned(input)` | Parse a zoned ISO string to `ZonedDateTime` | Must include offset and timezone (`[Region/City]`) |\n| `parsePlainDateTime(input)` | Parse a wall-clock string to `PlainDateTime` | No timezone attached — use `toInstant()` to pin it |\n| `parsePlainDate(input)` | Parse a date-only string to `PlainDate` | Use instead of `Temporal.PlainDate.from()` |\n| `isValid(value)` | Type guard for any `TimeInput` | Returns `false` for strings, numbers, and `null` |\n| `toInstant(input, options?)` | Normalize any input to a UTC instant | Plain inputs require `options.tz` |\n| `inTz(input, tz)` | Project any input into a specific timezone | Returns `ZonedDateTime`; re-projects `ZonedDateTime` inputs (wall-clock changes) |\n| `shift(input, duration, options?)` | DST-safe add/subtract | `options.tz` required for plain/instant inputs |\n| `difference(start, end, options?)` | Duration between two values | Requires `tz` only for calendar units or plain inputs |\n| `within(value, start, end, options?)` | Inclusive range check | Bounds auto-normalized; use `unit` for calendar checks |\n| `clamp(value, start, end, options?)` | Clamp to `[start, end]`; see notes for return type | With `unit` + `ZonedDateTime` input, returns `ZonedDateTime`; otherwise `Instant` |\n| `isBefore(a, b, options?)` | Returns `true` when `a` is earlier than `b` | Omit `unit` for raw timeline comparison |\n| `isAfter(a, b, options?)` | Returns `true` when `a` is later than `b` | Omit `unit` for raw timeline comparison |\n| `isSame(a, b, options?)` | Returns `true` when `a` and `b` are equal | Set `unit` for calendar-unit equality |\n| `startOf(input, unit, options?)` | Snap to start of a calendar unit | Week boundaries depend on `weekStartsOn` (default Monday) |\n| `endOf(input, unit, options?)` | Snap to end of a calendar unit | Returns 1 nanosecond before the next unit starts |\n| `format(input, options?)` | Localized display formatting with presets | `intl` and `pattern` are mutually exclusive |\n| `formatParts(input, options?)` | Raw `Intl.DateTimeFormatPart[]` for custom rendering | Same options as `format()` |\n| `formatInstant(input, options?)` | UTC ISO-8601 instant string | Always UTC regardless of input zone |\n| `formatZoned(input, options?)` | Zoned ISO-8601 string | `options.tz` required for non-zoned inputs |\n| `formatRange(start, end, options?)` | Localized time-span string | Throws when zoned inputs are in different zones without `tz` |\n| `formatRangeParts(start, end, options?)` | Raw `Intl.DateTimeRangeFormatPart[]` array | Same zone-mismatch rules as `formatRange()` |\n| `formatRelative(input, options?)` | UX relative text (\"in 2 hours\", \"3 days ago\") | Input restricted to `Instant` or `ZonedDateTime` |\n| `parseDuration(input)` | Parse ISO 8601 duration or `DurationLike` object | Throws `TempoError` for invalid strings |\n| `formatDuration(input, options?)` | Format a duration for display | Falls back to plain English if `Intl.DurationFormat` absent |\n| `expires(date, thresholds, options?)` | Classify a date against named threshold buckets | Returns `null` when no threshold matches; define thresholds at module scope for best perf |\n| `timeDiff(a, b?, options?)` | Largest-unit difference as `{ unit, value }` | No `tz` needed when both inputs are `Instant` |\n| `humanize(diff, options?)` | `TimeDiffResult` → human-readable string | English-only; use `formatRelative()` for localized output |\n| `dateRange(start, end, step, options?)` | Lazy generator of `ZonedDateTime` values | `step` must advance time forward; `tz` inferred from `ZonedDateTime`, required otherwise |\n| `recurrence(start, rule, options?)` | Lazy generator for repeating dates | `count` or `until` required; `tz` inferred from `ZonedDateTime` start |\n| `TempoError` | Base error class thrown by all tempo functions | `instanceof TempoError` catches every subtype below |\n| `TempoInvalidInputError` | Subtype — parse/duration input could not be understood | Thrown by `parse()`, `parseInstant()`, `parseZoned()`, `parsePlainDate()`, `parsePlainDateTime()`, `parseDuration()` |\n| `TempoInvalidTzError` | Subtype — timezone string is not a valid IANA name or offset | Thrown by any function that resolves a `tz` string |\n| `TempoMissingTzError` | Subtype — operation needs a timezone but none could be inferred | Thrown when a plain input is passed without `options.tz` |\n| `TempoUnsupportedInputError` | Subtype — input value is not a recognised `TimeInput` | Thrown by `toInstant()` for non-`TimeInput` values |\n\n## Package Entry Point\n\n```ts\nimport {\n Temporal,\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n clamp,\n dateRange,\n difference,\n endOf,\n expires,\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n inTz,\n isAfter,\n isBefore,\n isSame,\n isValid,\n now,\n nowInstant,\n parse,\n parseDuration,\n parseInstant,\n parsePlainDateTime,\n parsePlainDate,\n parseZoned,\n recurrence,\n shift,\n startOf,\n timeDiff,\n toInstant,\n within,\n} from '@vielzeug/tempo';\n```\n\n`Temporal` is re-exported directly — consumers never need to import `@js-temporal/polyfill` themselves. Use `parse(input, as?)` for flexible input detection, or the specific helpers (`parseInstant`, `parseZoned`, `parsePlainDateTime`, `parsePlainDate`) when the input format is known.\n\n## Core Functions\n\n### `now(tz): Temporal.ZonedDateTime`\n\n```ts\nnow(tz: string): Temporal.ZonedDateTime;\n```\n\nReturns the current time as a `ZonedDateTime` in `tz`.\n\n**Example:**\n\n```ts\nimport { now } from '@vielzeug/tempo';\n\nnow('UTC');\nnow('Europe/Berlin');\nnow('Asia/Tokyo');\n```\n\n---\n\n### `nowInstant(): Temporal.Instant`\n\n```ts\nnowInstant(): Temporal.Instant;\n```\n\nReturns the current absolute instant (UTC point in time). Use this instead of `Temporal.Now.instant()` so your code only imports from `@vielzeug/tempo`.\n\n**Example:**\n\n```ts\nimport { expires, nowInstant, timeDiff } from '@vielzeug/tempo';\n\n// Snapshot the current instant\nconst t = nowInstant();\n\ntimeDiff(t); // { unit: 'millisecond', value: 0 }\nexpires(t, { expired: { days: 0 }, safe: { years: 100 } }); // 'safe'\n```\n\n---\n\n### `parseZoned(input): Temporal.ZonedDateTime`\n\n```ts\nparseZoned(input: string): Temporal.ZonedDateTime;\n```\n\nParses a full ISO 8601 zoned date-time string (must include both offset and `[Region/City]` identifier) into a `ZonedDateTime`. Throws a descriptive `[tempo]` error on invalid input. Use instead of `Temporal.ZonedDateTime.from()`.\n\n**Example:**\n\n```ts\nimport { parseZoned } from '@vielzeug/tempo';\n\nparseZoned('2026-03-21T11:00:00+01:00[Europe/Berlin]');\nparseZoned('2026-03-21T00:00:00[UTC]');\n```\n\n---\n\n### `parsePlainDate(input): Temporal.PlainDate`\n\n```ts\nparsePlainDate(input: string): Temporal.PlainDate;\n```\n\nParses an ISO 8601 date-only string into a timezone-free `PlainDate`. Use this instead of `Temporal.PlainDate.from()`. Pair with `inTz()` or `toInstant()` when a timezone is needed.\n\n**Example:**\n\n```ts\nimport { inTz, parsePlainDate } from '@vielzeug/tempo';\n\nparsePlainDate('2026-03-21'); // 2026-03-21\n\n// Attach a timezone when needed\ninTz(parsePlainDate('2026-03-21'), 'America/New_York');\n```\n\n---\n\n### `parsePlainDateTime(input): Temporal.PlainDateTime`\n\n```ts\nparsePlainDateTime(input: string): Temporal.PlainDateTime;\n```\n\nParses an ISO 8601 date or date-time string into a timezone-free `PlainDateTime`. Use this at the boundary where user input or database values arrive as wall-clock strings.\n\n**Example:**\n\n```ts\nimport { parsePlainDateTime } from '@vielzeug/tempo';\n\nparsePlainDateTime('2026-03-21'); // 2026-03-21T00:00:00\nparsePlainDateTime('2026-03-21T10:15:30'); // 2026-03-21T10:15:30\n```\n\n---\n\n### `parseInstant(input): Temporal.Instant`\n\n```ts\nparseInstant(input: string): Temporal.Instant;\n```\n\nParses a UTC ISO 8601 string into a `Temporal.Instant`. The input must include an offset or end in `Z`. Throws a `TempoError` with code `INVALID_INPUT` on invalid input.\n\n**Example:**\n\n```ts\nimport { parseInstant } from '@vielzeug/tempo';\n\nparseInstant('2026-03-21T10:15:30Z');\nparseInstant('2026-03-21T11:15:30+01:00');\n```\n\n---\n\n### `parse(input, as?): TimeInput`\n\n```ts\nparse(input: string, as: 'zoned'): Temporal.ZonedDateTime;\nparse(input: string, as: 'instant'): Temporal.Instant;\nparse(input: string, as: 'plain-datetime'): Temporal.PlainDateTime;\nparse(input: string, as: 'plain-date'): Temporal.PlainDate;\nparse(input: string, as?: ParseAs): TimeInput;\n```\n\nParses any ISO 8601 string. Without `as`, auto-detects the most specific type in order: `ZonedDateTime` → `Instant` → `PlainDateTime` → `PlainDate`. With `as`, the return type is narrowed at compile time. Throws a descriptive `[tempo]` error if parsing fails.\n\nThe `as` parameter accepts a `ParseAs` value: `'zoned' | 'instant' | 'plain-datetime' | 'plain-date'`.\n\n**Example:**\n\n```ts\nimport { parse } from '@vielzeug/tempo';\n\n// Auto-detect\nparse('2026-03-21T11:00:00+01:00[Europe/Berlin]'); // Temporal.ZonedDateTime\nparse('2026-03-21T10:00:00Z'); // Temporal.Instant\nparse('2026-03-21T10:00:00'); // Temporal.PlainDateTime\nparse('2026-03-21'); // Temporal.PlainDate\n\n// Typed overloads — return type is narrowed\nparse('2026-03-21T10:00:00Z', 'instant'); // Temporal.Instant\nparse('2026-03-21', 'plain-date'); // Temporal.PlainDate\n```\n\n---\n\n### `isValid(value): value is TimeInput`\n\n```ts\nisValid(value: unknown): value is TimeInput;\n```\n\nType guard that returns `true` when `value` is a valid `Temporal.Instant`, `ZonedDateTime`, `PlainDateTime`, or `PlainDate`.\n\n**Example:**\n\n```ts\nimport { isValid, parseInstant } from '@vielzeug/tempo';\n\nisValid(parseInstant('2026-03-21T10:00:00Z')); // true\nisValid('2026-03-21'); // false\nisValid(null); // false\n```\n\n---\n\n### `toInstant(input, options?): Temporal.Instant`\n\n```ts\ntoInstant(input: TimeInput, options?: TimeOptions): Temporal.Instant;\n```\n\nNormalizes any `TimeInput` to an absolute `Temporal.Instant`.\n\n**Parameters — `TimeOptions`:**\n\n| Option | Type | Description |\n| ------ | -------- | -------------------------------------------------- |\n| `tz` | `string` | Required when input is `PlainDate`/`PlainDateTime` |\n\n**Example:**\n\n```ts\nimport { parseInstant, parsePlainDateTime, toInstant } from '@vielzeug/tempo';\n\ntoInstant(parseInstant('2026-03-21T10:15:30Z'));\ntoInstant(parsePlainDateTime('2026-03-21T10:15:30'), { tz: 'America/New_York' });\n```\n\n---\n\n### `inTz(input, tz): Temporal.ZonedDateTime`\n\n```ts\ninTz(input: TimeInput, tz: string): Temporal.ZonedDateTime;\n```\n\nProjects any `TimeInput` into `tz`. When `input` is already a `ZonedDateTime`, it is **re-projected** via `withTimeZone()` — the absolute instant is preserved but the wall-clock time changes.\n\n**Example:**\n\n```ts\nimport { inTz, parseInstant, parseZoned } from '@vielzeug/tempo';\n\ninTz(parseInstant('2026-03-21T10:15:30Z'), 'Europe/Berlin');\n// → 2026-03-21T11:15:30+01:00[Europe/Berlin]\n\n// ZonedDateTime re-projected — same instant, different wall clock\ninTz(parseZoned('2026-03-21T11:15:30+01:00[Europe/Berlin]'), 'UTC');\n// → 2026-03-21T10:15:30+00:00[UTC]\n```\n\n---\n\n### `shift(input, duration, options?): Temporal.ZonedDateTime`\n\n```ts\n// ZonedDateTime input — tz inferred, options optional\nshift(input: Temporal.ZonedDateTime, duration: Temporal.DurationLike, options?: TimeOptions): Temporal.ZonedDateTime;\n\n// Instant / PlainDate / PlainDateTime — tz required\nshift(input: Temporal.Instant | Temporal.PlainDate | Temporal.PlainDateTime, duration: Temporal.DurationLike, options: ShiftOptions & { tz: string }): Temporal.ZonedDateTime;\n```\n\nAdds `duration` to `input` using DST-aware calendar arithmetic. Negative values subtract. **Always returns `Temporal.ZonedDateTime`** — call `.toInstant()` if you need an `Instant` back.\n\n**Parameters — `ShiftOptions` (extends `TimeOptions`):**\n\n| Option | Type | Default | Description |\n| -------- | ------------------------ | -------------- | ----------------------------------------------------- |\n| `tz` | `string` | — | Required for plain/instant inputs; inferred for zoned |\n| `prefer` | `DateTimeDisambiguation` | `'compatible'` | DST disambiguation for `PlainDateTime` inputs |\n\n**Example:**\n\n```ts\nimport { parseInstant, parseZoned, shift } from '@vielzeug/tempo';\n\n// Zoned input — timezone inferred\nshift(parseZoned('2026-03-08T01:30:00-05:00[America/New_York]'), { hours: 1 });\n// → 2026-03-08T03:30:00-04:00[America/New_York] (DST spring-forward handled)\n\n// Instant input — timezone required\nshift(parseInstant('2026-03-21T10:00:00Z'), { days: 1 }, { tz: 'UTC' });\n```\n\n---\n\n### `difference(start, end, options?): Temporal.Duration`\n\n```ts\ndifference(start: TimeInput, end: TimeInput, options?: DifferenceOptions): Temporal.Duration;\n```\n\nReturns the signed duration from `start` to `end`. When `start > end` the result is negative.\n\n**Parameters — `DifferenceOptions`:**\n\n| Option | Type | Default | Description |\n| ------------------- | ------------------------ | -------------- | ------------------------------------------- |\n| `tz` | `string` | — | Required for plain inputs or calendar units |\n| `largestUnit` | `Temporal.DateTimeUnit` | `'second'` | Largest unit in the returned duration |\n| `smallestUnit` | `Temporal.DateTimeUnit` | `'second'` | Smallest unit; excess is rounded |\n| `roundingMode` | `Temporal.RoundingMode` | `'trunc'` | Rounding direction for `smallestUnit` |\n| `roundingIncrement` | `number` | `1` | Rounding granularity for `smallestUnit` |\n| `prefer` | `DateTimeDisambiguation` | `'compatible'` | DST disambiguation |\n\nTwo `Temporal.Instant` inputs with sub-day `largestUnit`/`smallestUnit` do not need `tz`. Calendar units (`day`, `week`, `month`, `year`) always require `tz`.\n\n**Example:**\n\n```ts\nimport { difference, parseInstant, parseZoned } from '@vielzeug/tempo';\n\n// Instant-to-instant, sub-day units — no tz needed\ndifference(parseInstant('2026-03-21T10:00:00Z'), parseInstant('2026-03-21T12:30:00Z'), {\n largestUnit: 'hour',\n smallestUnit: 'minute',\n}); // PT2H30M\n\n// DST-correct day count across spring-forward\ndifference(\n parseZoned('2026-03-08T00:00:00-05:00[America/New_York]'),\n parseZoned('2026-03-09T00:00:00-04:00[America/New_York]'),\n { largestUnit: 'hour' },\n).hours; // 23 (one hour shorter due to DST)\n```\n\n## Query and Comparison\n\n### `within(value, start, end, options?): boolean`\n\n```ts\nwithin(value: TimeInput, start: TimeInput, end: TimeInput, options?: CompareOptions): boolean;\n```\n\nReturns `true` when `value` falls within `[start, end]` (inclusive). Bounds are automatically normalized, so `within(v, hi, lo)` behaves the same as `within(v, lo, hi)`.\n\nSet `options.unit` to compare on calendar-unit boundaries (e.g., same day, same week).\n\n**Example:**\n\n```ts\nimport { parseInstant, within } from '@vielzeug/tempo';\n\nconst lo = parseInstant('2026-03-21T10:00:00Z');\nconst hi = parseInstant('2026-03-21T12:00:00Z');\n\nwithin(parseInstant('2026-03-21T11:00:00Z'), lo, hi); // true\nwithin(lo, lo, hi); // true (inclusive)\nwithin(parseInstant('2026-03-22T05:00:00Z'), lo, hi, { unit: 'day', tz: 'UTC' }); // true\n```\n\n---\n\n### `clamp(value, start, end, options?): Temporal.Instant`\n\n```ts\nclamp(value: TimeInput, start: TimeInput, end: TimeInput, options?: CompareOptions): Temporal.Instant;\n```\n\nReturns `value` clamped to `[start, end]`. Returns a `Temporal.Instant` for non-zoned inputs. When `value` is a `ZonedDateTime` and `options.unit` is set, returns a `Temporal.ZonedDateTime` floored to the start of the clamped unit. Bounds are automatically normalized when `start > end`.\n\n**Example:**\n\n```ts\nimport { clamp, parseInstant } from '@vielzeug/tempo';\n\nconst lo = parseInstant('2026-03-21T10:00:00Z');\nconst hi = parseInstant('2026-03-21T12:00:00Z');\n\nclamp(parseInstant('2026-03-21T13:00:00Z'), lo, hi).toString(); // '2026-03-21T12:00:00Z'\nclamp(parseInstant('2026-03-23T05:00:00Z'), lo, hi, { unit: 'day', tz: 'America/New_York' });\n```\n\n---\n\n### `isBefore(a, b, options?): boolean`\n\n```ts\nisBefore(a: TimeInput, b: TimeInput, options?: CompareOptions): boolean;\n```\n\nReturns `true` when `a` is earlier than `b` on the timeline. Set `options.unit` for calendar-unit comparison.\n\n**Example:**\n\n```ts\nimport { isBefore, parseInstant } from '@vielzeug/tempo';\n\nisBefore(parseInstant('2026-03-21T23:30:00Z'), parseInstant('2026-03-22T00:15:00Z'), {\n unit: 'day',\n tz: 'UTC',\n}); // true — different UTC days\n```\n\n---\n\n### `isAfter(a, b, options?): boolean`\n\n```ts\nisAfter(a: TimeInput, b: TimeInput, options?: CompareOptions): boolean;\n```\n\nReturns `true` when `a` is later than `b` on the timeline. Set `options.unit` for calendar-unit comparison.\n\n**Example:**\n\n```ts\nimport { isAfter, parseInstant } from '@vielzeug/tempo';\n\nisAfter(parseInstant('2026-03-21T12:00:00Z'), parseInstant('2026-03-21T10:00:00Z')); // true\n```\n\n---\n\n### `isSame(a, b, options?): boolean`\n\n```ts\nisSame(a: TimeInput, b: TimeInput, options?: CompareOptions): boolean;\n```\n\nReturns `true` when `a` and `b` represent the same point or the same calendar unit. Infers timezone from `ZonedDateTime` inputs; throws when both are zoned in different zones and `options.tz` is omitted.\n\n**Example:**\n\n```ts\nimport { isSame, parseInstant } from '@vielzeug/tempo';\n\nisSame(parseInstant('2026-03-21T10:00:00Z'), parseInstant('2026-03-21T10:00:00Z')); // true\n\n// Same calendar day in New York even though UTC dates differ\nisSame(parseInstant('2026-03-21T23:30:00Z'), parseInstant('2026-03-22T00:15:00Z'), {\n unit: 'day',\n tz: 'America/New_York',\n}); // true\n```\n\n## Boundary Helpers\n\n### `startOf(input, unit, options?): Temporal.ZonedDateTime`\n\n```ts\nstartOf(input: TimeInput, unit: BoundaryUnit, options?: BoundaryOptions): Temporal.ZonedDateTime;\n```\n\nSnaps `input` to the start of `unit`. For `ZonedDateTime` inputs, `options.tz` is inferred automatically.\n\n**Parameters — `BoundaryOptions`:**\n\n| Option | Type | Default | Description |\n| -------------- | -------------- | ------- | ------------------------------------ |\n| `tz` | `string` | — | Required for non-zoned inputs |\n| `weekStartsOn` | `WeekStartDay` | `1` | ISO weekday (1 = Monday, 7 = Sunday) |\n\nSupported units: `'minute'` · `'hour'` · `'day'` · `'week'` · `'month'` · `'year'`\n\n**Example:**\n\n```ts\nimport { parseInstant, startOf } from '@vielzeug/tempo';\n\nstartOf(parseInstant('2026-03-21T10:15:30Z'), 'day', { tz: 'UTC' });\n// → 2026-03-21T00:00:00+00:00[UTC]\n\nstartOf(parseInstant('2026-03-25T12:00:00Z'), 'week', { tz: 'UTC', weekStartsOn: 1 });\n// → 2026-03-23T00:00:00+00:00[UTC] (Monday)\n```\n\n---\n\n### `endOf(input, unit, options?): Temporal.ZonedDateTime`\n\n```ts\nendOf(input: TimeInput, unit: BoundaryUnit, options?: BoundaryOptions): Temporal.ZonedDateTime;\n```\n\nSnaps `input` to the last nanosecond of `unit` (`startOf(nextUnit) - 1ns`).\n\n**Example:**\n\n```ts\nimport { endOf, parseInstant } from '@vielzeug/tempo';\n\nendOf(parseInstant('2026-03-21T10:15:30Z'), 'day', { tz: 'UTC' });\n// → 2026-03-21T23:59:59.999999999+00:00[UTC]\n```\n\n## Formatting\n\n### `format(input, options?): string`\n\n```ts\nformat(input: TimeInput, options?: FormatOptions): string;\n```\n\nFormats `input` for display using `Intl.DateTimeFormat`. Use `pattern` for common presets or `intl` for a custom `Intl.DateTimeFormatOptions` object. `intl` and `pattern` are **mutually exclusive** — enforced at the type level.\n\n**Parameters — `FormatOptions` (discriminated union):**\n\n| Variant | Option | Type | Default | Description |\n| ---------------- | --------- | ---------------------------- | ------------- | -------------------------------------------------------- |\n| `pattern` branch | `pattern` | `FormatPattern` | `'medium'` | Preset shorthand |\n| `intl` branch | `intl` | `Intl.DateTimeFormatOptions` | — | Full `Intl` spec; mutually exclusive with `pattern` |\n| Both variants | `locale` | `Intl.LocalesArgument` | system locale | BCP 47 locale tag |\n| Both variants | `tz` | `string` | — | Inferred from `ZonedDateTime` inputs; required otherwise |\n\n**Patterns:**\n\n| Pattern | Equivalent |\n| ------------- | --------------------------------------------- |\n| `'short'` | `{ dateStyle: 'short', timeStyle: 'short' }` |\n| `'medium'` | `{ dateStyle: 'medium', timeStyle: 'short' }` |\n| `'long'` | `{ dateStyle: 'full', timeStyle: 'long' }` |\n| `'date-only'` | `{ dateStyle: 'short' }` |\n| `'time-only'` | `{ timeStyle: 'short' }` |\n\n**Example:**\n\n```ts\nimport { format, parseInstant } from '@vielzeug/tempo';\n\nformat(parseInstant('2026-03-21T10:15:30Z'), { pattern: 'short', locale: 'en-GB', tz: 'UTC' });\n// → '21/03/2026, 10:15'\n\n// Escape hatch: full Intl spec\nformat(parseInstant('2026-03-21T10:15:30Z'), {\n intl: { hour: '2-digit', minute: '2-digit', hour12: false },\n locale: 'en-US',\n tz: 'UTC',\n});\n// → '10:15'\n```\n\n---\n\n### `formatParts(input, options?): Intl.DateTimeFormatPart[]`\n\n```ts\nformatParts(input: TimeInput, options?: FormatOptions): Intl.DateTimeFormatPart[];\n```\n\nReturns the raw `Intl.DateTimeFormatPart[]` array for `input`, enabling custom rendering where individual parts (year, month, day, etc.) need to be styled or composed differently. Accepts the same options as `format()`.\n\n**Example:**\n\n```ts\nimport { formatParts, parseInstant } from '@vielzeug/tempo';\n\nconst parts = formatParts(parseInstant('2026-03-21T10:15:30Z'), { pattern: 'date-only', tz: 'UTC' });\n// [{ type: 'month', value: '3' }, { type: 'literal', value: '/' }, ...]\n```\n\n---\n\n### `formatInstant(input, options?): string`\n\n```ts\nformatInstant(input: TimeInput, options?: TimeOptions): string;\n```\n\nReturns a UTC ISO-8601 instant string (`YYYY-MM-DDTHH:mm:ssZ`). Use this for transport, logging, and APIs.\n\n**Example:**\n\n```ts\nimport { formatInstant, parseInstant } from '@vielzeug/tempo';\n\nformatInstant(parseInstant('2026-03-21T10:15:30Z'));\n// → '2026-03-21T10:15:30Z'\n```\n\n---\n\n### `formatZoned(input, options?): string`\n\n```ts\nformatZoned(input: TimeInput, options?: TimeOptions): string;\n```\n\nReturns a full zoned ISO-8601 string including offset and timezone ID. Infers timezone from `ZonedDateTime` inputs; requires `options.tz` for all other input types.\n\n**Example:**\n\n```ts\nimport { formatZoned, parseInstant, parseZoned } from '@vielzeug/tempo';\n\nformatZoned(parseInstant('2026-03-21T10:15:30Z'), { tz: 'Europe/Berlin' });\n// → '2026-03-21T11:15:30+01:00[Europe/Berlin]'\n\nformatZoned(parseZoned('2026-03-21T10:15:30+01:00[Europe/Berlin]'));\n// → '2026-03-21T10:15:30+01:00[Europe/Berlin]'\n```\n\n---\n\n### `formatRange(start, end, options?): string`\n\n```ts\nformatRange(start: TimeInput, end: TimeInput, options?: FormatOptions): string;\n```\n\nFormats a localized time span using `Intl.DateTimeFormat.formatRange`. Infers a shared timezone from `ZonedDateTime` inputs; throws if they are in different zones and `options.tz` is omitted.\n\n**Example:**\n\n```ts\nimport { formatRange, parseInstant } from '@vielzeug/tempo';\n\nformatRange(parseInstant('2026-03-21T10:00:00Z'), parseInstant('2026-03-21T12:00:00Z'), {\n pattern: 'short',\n locale: 'en-US',\n tz: 'UTC',\n});\n// → '3/21/2026, 10:00 – 12:00 AM'\n```\n\n---\n\n### `formatRangeParts(start, end, options?): Intl.DateTimeRangeFormatPart[]`\n\n```ts\nformatRangeParts(\n start: TimeInput,\n end: TimeInput,\n options?: FormatOptions,\n): ReturnType<Intl.DateTimeFormat['formatRangeToParts']>;\n```\n\nReturns the raw `Intl.DateTimeRangeFormatPart[]` array for a time span, enabling fine-grained rendering of range start, end, and shared parts separately. Applies the same timezone-inference and mismatch-detection rules as `formatRange()`.\n\n**Example:**\n\n```ts\nimport { formatRangeParts, parseInstant } from '@vielzeug/tempo';\n\nconst parts = formatRangeParts(parseInstant('2026-03-21T10:00:00Z'), parseInstant('2026-03-21T12:00:00Z'), {\n pattern: 'short',\n locale: 'en-US',\n tz: 'UTC',\n});\n// [{ type: 'month', value: '3', source: 'shared' }, ...]\n\n// Render only the start part\nconst startParts = parts.filter((p) => p.source === 'startRange' || p.source === 'shared');\n```\n\n---\n\n### `formatRelative(input, options?): string`\n\n```ts\nformatRelative(input: RelativeTimeInput, options?: RelativeFormatOptions): string;\n```\n\nReturns a UX-friendly relative time string (\"in 2 hours\", \"3 days ago\") using `Intl.RelativeTimeFormat`. When `options.base` is omitted, the current instant is used.\n\n**Parameters — `RelativeFormatOptions`:**\n\n| Option | Type | Default | Description |\n| --------- | -------------------------------- | -------- | -------------------------------------------------------- |\n| `base` | `RelativeTimeInput` | `now` | Reference point for the relative calculation |\n| `locale` | `Intl.LocalesArgument` | system | BCP 47 locale tag |\n| `numeric` | `Intl.RelativeTimeFormatNumeric` | `'auto'` | `'always'` forces \"1 day ago\"; `'auto'` uses \"yesterday\" |\n| `style` | `Intl.RelativeTimeFormatStyle` | `'long'` | `'short'` or `'narrow'` for compact labels |\n\nUnit selection uses approximate thresholds: differences under 60 s → `'second'`, under 60 min → `'minute'`, under 24 h → `'hour'`, under 7 d → `'day'`, under ~4.35 weeks → `'week'`, under 12 months → `'month'`, otherwise `'year'`. These thresholds use fixed second constants (1 month ≈ 30.4375 days) and do not account for DST — use `difference()` with `ZonedDateTime` inputs for calendar-accurate results.\n\n**Example:**\n\n```ts\nimport { formatRelative, parseInstant } from '@vielzeug/tempo';\n\nconst base = parseInstant('2026-03-21T10:00:00Z');\n\nformatRelative(parseInstant('2026-03-21T12:00:00Z'), { base, locale: 'en-US', numeric: 'always' });\n// → 'in 2 hours'\n\nformatRelative(parseInstant('2026-03-19T10:00:00Z'), { base, locale: 'en-US' });\n// → '2 days ago'\n```\n\n## Duration Helpers\n\n### `parseDuration(input): Temporal.Duration`\n\n```ts\nparseDuration(input: string | Temporal.DurationLike): Temporal.Duration;\n```\n\nParses an ISO 8601 duration string or a `Temporal.DurationLike` object into a `Temporal.Duration`. Throws `TempoError` with code `INVALID_INPUT` for invalid input.\n\n**Example:**\n\n```ts\nimport { parseDuration } from '@vielzeug/tempo';\n\nparseDuration('PT2H30M');\nparseDuration({ hours: 2, minutes: 30 });\nparseDuration('-PT1H'); // negative duration\n```\n\n---\n\n### `formatDuration(input, options?): string`\n\n```ts\nformatDuration(input: string | Temporal.DurationLike, options?: DurationFormatOptions): string;\n```\n\nFormats a duration for display. Uses `Intl.DurationFormat` when available; falls back to a plain English representation (\"2 hours, 30 minutes\").\n\n**Parameters — `DurationFormatOptions`:**\n\n| Option | Type | Default | Description |\n| -------- | -------------------------------------------- | -------- | ----------------- |\n| `locale` | `Intl.LocalesArgument` | system | BCP 47 locale tag |\n| `style` | `'digital' \\| 'long' \\| 'narrow' \\| 'short'` | `'long'` | Display style |\n\n**Example:**\n\n```ts\nimport { formatDuration } from '@vielzeug/tempo';\n\nformatDuration('PT2H30M', { locale: 'en-US', style: 'short' });\nformatDuration({ hours: 1, minutes: 30 }, { locale: 'de-DE' });\n```\n\n## Expiry and Classification\n\n### `expires(date, thresholds, options?, now?): K | null`\n\n```ts\nexpires<K extends string>(\n date: TimeInput,\n thresholds: Record<K, Temporal.DurationLike>,\n options?: TimeOptions,\n now?: Temporal.Instant,\n): K | null;\n```\n\nClassifies `date` into a named bucket by computing `diff = date − now` and returning the key of the first threshold where `diff ≤ threshold`. Thresholds are sorted ascending before comparison.\n\n- Returns `null` when no threshold matches.\n- Requires `options.tz` when input is `PlainDate` or `PlainDateTime`.\n- Pass `now` to fix the reference point (useful in tests).\n\n**Negative thresholds classify past dates.** A threshold of `{ days: -N }` matches dates that are more than N days in the _past_ (i.e. `diff ≤ -N days`). Use negative thresholds at the front of your map for \"expired\" or \"overdue\" buckets:\n\n```ts\nconst THRESHOLDS = {\n longExpired: { days: -30 }, // more than 30 days ago\n expired: { days: 0 }, // any past date (diff ≤ 0)\n critical: { days: 3 }, // within 3 days in the future\n warning: { days: 14 },\n safe: { years: 100 },\n} as const;\n```\n\n**Performance:** define threshold objects at module scope (not inline literals) so the internal `WeakMap` sort-cache is effective.\n\n**Example:**\n\n```ts\nimport { expires } from '@vielzeug/tempo';\n\nconst THRESHOLDS = {\n longExpired: { days: -30 }, // more than 30 days past\n expired: { days: 0 }, // any past date\n critical: { days: 3 }, // within 3 days\n warning: { days: 14 }, // within 14 days\n safe: { years: 100 }, // catch-all far future\n} as const;\n\nexpires(Temporal.Now.instant().subtract({ days: 60 }), THRESHOLDS); // 'longExpired'\nexpires(Temporal.Now.instant().subtract({ hours: 6 }), THRESHOLDS); // 'expired'\nexpires(Temporal.Now.instant().add({ hours: 48 }), THRESHOLDS); // 'critical'\nexpires(Temporal.Now.instant().add({ days: 10 }), THRESHOLDS); // 'warning'\nexpires(Temporal.Now.instant().add({ years: 1 }), THRESHOLDS); // 'safe'\n\n// No threshold matches — returns null\nexpires(Temporal.Now.instant().add({ years: 200 }), { soon: { days: 3 } }); // null\n```\n\n---\n\n### `timeDiff(a, b?, options?): TimeDiffResult`\n\n```ts\ntimeDiff(a: TimeInput, b?: TimeInput, options?: TimeOptions): TimeDiffResult;\n```\n\nReturns the absolute difference between `a` and `b` as `{ unit, value }` in the largest meaningful unit. When `b` is omitted, the current instant is used.\n\n**Timezone requirement:**\n\n- When both inputs are `Temporal.Instant`: no `tz` needed — uses millisecond arithmetic (1 day = 86 400 s).\n- When either input is `PlainDate`, `PlainDateTime`, or `ZonedDateTime`: `tz` is required (inferred from `ZonedDateTime` inputs).\n\nSub-second differences return `{ unit: 'millisecond', value: <ms> }`. Zero difference returns `{ unit: 'millisecond', value: 0 }`.\n\n**Example:**\n\n```ts\nimport { parseInstant, parseZoned, timeDiff } from '@vielzeug/tempo';\n\n// No tz needed for two Instants — uses ms arithmetic (1 year ≈ 365.25 days)\n// 2026-01 → 2027-06 ≈ 17 months; floor(17/12) = 1, so unit is 'year', value is 1\ntimeDiff(parseInstant('2026-01-01T00:00:00Z'), parseInstant('2027-06-01T00:00:00Z')); // { unit: 'year', value: 1 }\n\n// b defaults to now\ntimeDiff(Temporal.Now.instant().subtract({ hours: 3 })); // { unit: 'hour', value: 3 }\n\n// Calendar-accurate with tz\ntimeDiff(parseInstant('2026-01-01T00:00:00Z'), parseZoned('2026-06-15T00:00:00[UTC]'), {\n tz: 'UTC',\n}); // { unit: 'month', value: 5 }\n```\n\n---\n\n### `humanize(diff, options?): string`\n\n```ts\nhumanize(diff: TimeDiffResult, options?: { locale?: Intl.LocalesArgument }): string;\n```\n\nConverts a `TimeDiffResult` to a human-readable string. Uses the singular form when `value === 1`, plural otherwise. Unit names are **English-only** — use `formatRelative()` or `formatDuration()` for fully localized output.\n\n**Parameters — `options`:**\n\n| Option | Type | Default | Description |\n| -------- | ---------------------- | ----------- | --------------------------------------------------- |\n| `locale` | `Intl.LocalesArgument` | `undefined` | Locale for the numeric part via `Intl.NumberFormat` |\n\n**Returns:** `string`\n\n**Example:**\n\n```ts\nimport { humanize, timeDiff } from '@vielzeug/tempo';\n\nhumanize({ unit: 'day', value: 1 }); // '1 day'\nhumanize({ unit: 'hour', value: 7 }); // '7 hours'\nhumanize({ unit: 'millisecond', value: 0 }); // '0 milliseconds'\nhumanize({ unit: 'day', value: 3 }, { locale: 'ar' }); // '٣ days'\n\n// Typical combined usage\nhumanize(timeDiff(publishedAt)); // '3 days'\n```\n\n## Range and Recurrence\n\n### `dateRange(start, end, step, options?): Generator<Temporal.ZonedDateTime>`\n\n```ts\ndateRange(\n start: TimeInput,\n end: TimeInput,\n step: Temporal.DurationLike,\n options?: TimeOptions,\n): Generator<Temporal.ZonedDateTime>;\n```\n\nLazily generates `ZonedDateTime` values between `start` and `end` (inclusive), advancing by `step`. Returns a generator — use `for...of` for lazy consumption or spread (`[...dateRange(...)]`) to collect into an array. Returns nothing when `start > end`. Throws `RangeError` if `step` does not advance the date forward.\n\nWhen `start` is a `ZonedDateTime`, `options.tz` is inferred from it automatically. If `end` is in a different timezone it is silently re-projected into `start`'s timezone. Pass `options.tz` explicitly to override. For plain inputs, `options.tz` is required.\n\n**Example:**\n\n```ts\nimport { dateRange, parsePlainDate, parseZoned } from '@vielzeug/tempo';\n\nconst start = parseZoned('2026-03-01T00:00:00[UTC]');\nconst end = parseZoned('2026-03-31T00:00:00[UTC]');\n\n// ZonedDateTime inputs — tz inferred, no need to pass options\nfor (const day of dateRange(start, end, { days: 1 })) {\n render(day);\n if (someCondition) break; // safe to break early\n}\n\n// Collect to array\nconst days = [...dateRange(start, end, { days: 1 })];\n// [Mar 1, Mar 2, ..., Mar 31]\n\n// Plain inputs still require tz\nconst days = [...dateRange(parsePlainDate('2026-03-01'), parsePlainDate('2026-03-31'), { days: 1 }, { tz: 'UTC' })];\n```\n\n---\n\n### `recurrence(start, rule, options?): Generator<Temporal.ZonedDateTime>`\n\n```ts\nrecurrence(\n start: TimeInput,\n rule: RecurrenceRule,\n options?: TimeOptions,\n): Generator<Temporal.ZonedDateTime>;\n```\n\nLazily generates `ZonedDateTime` occurrences according to a recurrence rule. Supports `daily`, `weekly`, `monthly`, and `yearly` frequencies.\n\nEither `count` or `until` (or both) **must** be provided — this is enforced at compile time by the `RecurrenceRule` type and validated eagerly at call time for JavaScript callers. When `start` is a `ZonedDateTime`, `options.tz` is inferred automatically.\n\n**Parameters — `RecurrenceRule`:**\n\n| Field | Type | Required | Description |\n| ----------- | ---------------------------------------------- | ------------------------------------------ | ------------------------------------- |\n| `frequency` | `'daily' \\| 'weekly' \\| 'monthly' \\| 'yearly'` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Recurrence frequency |\n| `interval` | `number` | — | Step multiplier (default `1`) |\n| `count` | `number` | One of these two | Maximum number of occurrences to emit |\n| `until` | `TimeInput` | One of these two | Inclusive end boundary |\n\n**Example:**\n\n```ts\nimport { parseInstant, parseZoned, recurrence } from '@vielzeug/tempo';\n\nconst start = parseZoned('2026-01-05T09:00:00[Europe/Berlin]');\n\n// ZonedDateTime start — tz inferred, no options needed\nconst mondays = [...recurrence(start, { frequency: 'weekly', count: 4 })];\n\n// Bi-weekly meetings until a deadline\nconst deadline = parseZoned('2026-06-30T00:00:00[Europe/Berlin]');\nfor (const meeting of recurrence(start, { frequency: 'weekly', interval: 2, until: deadline })) {\n schedule(meeting);\n}\n\n// Plain input — tz required\nconst quarters = [\n ...recurrence(parseInstant('2026-01-05T09:00:00Z'), { frequency: 'monthly', interval: 3, count: 6 }, { tz: 'UTC' }),\n];\n```\n\n## Errors\n\nAll errors thrown by tempo are instances of `TempoError`. Catch the base class to handle any tempo-originated\nerror, or catch a specific subtype to distinguish failure modes.\n\n```ts\nimport { TempoError, TempoMissingTzError, parse, parsePlainDateTime, toInstant } from '@vielzeug/tempo';\n\ntry {\n parse('not-a-date');\n} catch (e) {\n if (e instanceof TempoError) {\n console.log(e.name); // 'TempoInvalidInputError'\n console.log(e.message); // 'Unable to parse date/time string: \"not-a-date\". ...'\n }\n}\n\ntry {\n toInstant(parsePlainDateTime('2026-03-21T10:00:00'));\n} catch (e) {\n if (e instanceof TempoMissingTzError) {\n // narrow to this specific failure mode\n }\n}\n```\n\n### `TempoError`\n\nBase class for every error tempo throws. Use `TempoError.is()` to catch anything tempo-originated regardless\nof subtype.\n\n```ts\nclass TempoError extends Error {\n constructor(message: string, opts?: ErrorOptions);\n static is(err: unknown): err is TempoError;\n}\n```\n\n---\n\n### `TempoInvalidInputError`\n\nThrown by `parse()`, `parseInstant()`, `parseZoned()`, `parsePlainDate()`, `parsePlainDateTime()`, and\n`parseDuration()` when the input string cannot be understood. Also the default error class for any `fail()`\ncall site that doesn't specify a more specific subtype (e.g. cross-timezone mismatches in `within()` /\n`clamp()` / `difference()`).\n\n### `TempoInvalidTzError`\n\nThrown when a timezone string is not a valid IANA name or UTC offset — from `validateTz()`, used by every\nfunction that resolves a `tz` option (`now()`, `inTz()`, `shift()`, `startOf()`, `endOf()`, etc.).\n\n### `TempoMissingTzError`\n\nThrown when an operation requires a timezone but none could be inferred — a `PlainDate` or `PlainDateTime`\ninput was passed without `options.tz`, or without a shared timezone across two/more `TimeInput` values.\n\n### `TempoUnsupportedInputError`\n\nThrown by `toInstant()` when the input value is not one of the four `TimeInput` types\n(`Instant`, `ZonedDateTime`, `PlainDateTime`, `PlainDate`).\n\n---\n\n## Types\n\n```ts\ntype TimeInput = Temporal.Instant | Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime;\n\ntype RelativeTimeInput = Temporal.Instant | Temporal.ZonedDateTime;\n\ntype DateTimeDisambiguation = 'compatible' | 'earlier' | 'later' | 'reject';\n\n/** Discriminant for the parse() `as` parameter. Controls the expected return type. */\ntype ParseAs = 'instant' | 'plain-date' | 'plain-datetime' | 'zoned';\n\ntype FormatPattern = 'date-only' | 'long' | 'medium' | 'short' | 'time-only';\n\n// Discriminated union — intl and pattern are mutually exclusive\ntype FormatOptions =\n | { intl: Intl.DateTimeFormatOptions; locale?: Intl.LocalesArgument; pattern?: never; tz?: string }\n | { intl?: never; locale?: Intl.LocalesArgument; pattern?: FormatPattern; tz?: string };\n\ntype TempoUnit =\n | 'day'\n | 'hour'\n | 'microsecond'\n | 'millisecond'\n | 'minute'\n | 'month'\n | 'nanosecond'\n | 'second'\n | 'week'\n | 'year';\n\ntype CalendarUnit = Extract<TempoUnit, 'day' | 'month' | 'week' | 'year'>;\ntype BoundaryUnit = Exclude<TempoUnit, 'microsecond' | 'millisecond' | 'nanosecond' | 'second'>;\ntype TimeDiffUnit = Exclude<TempoUnit, 'microsecond' | 'nanosecond'>;\ntype TimeDiffResult = { unit: TimeDiffUnit; value: number };\ntype WeekStartDay = 1 | 2 | 3 | 4 | 5 | 6 | 7;\n\n// Either count or until (or both) must be provided\ntype RecurrenceRule = {\n frequency: 'daily' | 'monthly' | 'weekly' | 'yearly';\n interval?: number;\n} & ({ count: number; until?: TimeInput } | { count?: number; until: TimeInput });\n\ninterface TimeOptions {\n tz?: string;\n}\n\ninterface DisambiguationOptions {\n prefer?: DateTimeDisambiguation;\n}\n\ninterface ShiftOptions extends DisambiguationOptions, TimeOptions {}\n\ninterface DifferenceOptions extends DisambiguationOptions, TimeOptions {\n largestUnit?: Temporal.DateTimeUnit;\n roundingIncrement?: number;\n roundingMode?: Temporal.RoundingMode;\n smallestUnit?: Temporal.DateTimeUnit;\n}\n\ninterface RelativeFormatOptions {\n base?: RelativeTimeInput;\n locale?: Intl.LocalesArgument;\n numeric?: Intl.RelativeTimeFormatNumeric;\n style?: Intl.RelativeTimeFormatStyle;\n}\n\ninterface BoundaryOptions extends TimeOptions {\n weekStartsOn?: WeekStartDay;\n}\n\ninterface CompareOptions extends TimeOptions {\n unit?: BoundaryUnit;\n weekStartsOn?: WeekStartDay;\n}\n\ninterface DurationFormatOptions {\n locale?: Intl.LocalesArgument;\n style?: 'digital' | 'long' | 'narrow' | 'short';\n}\n```\n",
|
|
6
|
+
"usage": "---\ntitle: Tempo — Usage Guide\ndescription: Parsing, timezone conversion, arithmetic, boundaries, and formatting with Tempo.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse named imports from `@vielzeug/tempo` for tree-shaking.\n\n```ts\nimport { format, inTz, now, parsePlainDateTime, shift, toInstant } from '@vielzeug/tempo';\n\n// Current time in a timezone\nconst berlin = now('Europe/Berlin');\n\n// Parse a wall-clock string, then pin it to a timezone\nconst local = parsePlainDateTime('2026-03-21T10:15:30');\nconst instant = toInstant(local, { tz: 'America/New_York' });\n\n// Project to a different timezone\nconst tokyo = inTz(instant, 'Asia/Tokyo');\n\n// Format for display\nformat(instant, { pattern: 'short', locale: 'en-US', tz: 'America/New_York' });\n```\n\n## Parsing and Conversion\n\nAll common Temporal constructors have a tempo equivalent — import only from `@vielzeug/tempo`:\n\n| Instead of… | Use… |\n| ----------------------------------------------------------- | --------------------------------- |\n| `Temporal.Now.instant()` | `nowInstant()` |\n| `Temporal.Now.zonedDateTimeISO(tz)` | `now(tz)` |\n| `Temporal.Instant.from(str)` | `parseInstant(str)` |\n| `Temporal.ZonedDateTime.from(str)` | `parseZoned(str)` |\n| `Temporal.PlainDateTime.from(str)` | `parsePlainDateTime(str)` |\n| `Temporal.PlainDate.from(str)` | `parsePlainDate(str)` |\n| `Temporal.ZonedDateTime.from` / `Temporal.Instant.from` / … | `parse(str)` (unknown format) |\n\n```ts\nimport {\n inTz,\n isValid,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDateTime,\n parsePlainDate,\n parseZoned,\n toInstant,\n} from '@vielzeug/tempo';\n\n// Current instant\nconst t = nowInstant();\n\n// Wall-clock string from user input or database\nconst local = parsePlainDateTime('2026-03-21T10:15:30');\nconst instant = toInstant(local, { tz: 'Europe/Berlin' });\nconst tokyo = inTz(instant, 'Asia/Tokyo');\n\n// UTC ISO string from an API response\nconst ts = parseInstant('2026-03-21T10:15:30Z');\n\n// Zoned date-time string\nconst meeting = parseZoned('2026-03-21T11:00:00+01:00[Europe/Berlin]');\n\n// Date-only string\nconst date = parsePlainDate('2026-03-21');\n\n// Unknown ISO format — picks the most specific type automatically\nparse('2026-03-21T11:00:00+01:00[Europe/Berlin]'); // ZonedDateTime\nparse('2026-03-21T10:00:00Z'); // Instant\nparse('2026-03-21T10:00:00'); // PlainDateTime\nparse('2026-03-21'); // PlainDate\n\n// Type guard — validate before passing to Tempo functions\nif (isValid(externalValue)) {\n format(externalValue, { pattern: 'short', tz: 'UTC' });\n}\n```\n\n## DST-Safe Arithmetic\n\n`shift()` handles DST transitions correctly.\n\n```ts\nimport { parseZoned, shift } from '@vielzeug/tempo';\n\nconst before = parseZoned('2026-03-08T01:30:00-05:00[America/New_York]');\nconst after = shift(before, { hours: 1 });\n\nconsole.log(after.toString());\n// 2026-03-08T03:30:00-04:00[America/New_York]\n```\n\n## Difference and Range Tools\n\n```ts\nimport { clamp, difference, parseInstant, within } from '@vielzeug/tempo';\n\n// difference() returns a signed duration: negative when start is after end\nconst duration = difference(parseInstant('2026-03-21T10:00:00Z'), parseInstant('2026-03-21T12:30:00Z'), {\n tz: 'UTC',\n largestUnit: 'hour',\n smallestUnit: 'minute',\n});\n\nconst inWindow = within(\n parseInstant('2026-03-21T11:00:00Z'),\n parseInstant('2026-03-21T10:00:00Z'),\n parseInstant('2026-03-21T12:00:00Z'),\n);\n\nconst inWindowByDay = within(\n parseInstant('2026-03-22T04:59:00Z'),\n parseInstant('2026-03-21T06:00:00Z'),\n parseInstant('2026-03-22T03:00:00Z'),\n { unit: 'day', tz: 'America/New_York' },\n);\n\n// clamp returns Temporal.Instant — project to a timezone as needed\nconst clamped = clamp(\n parseInstant('2026-03-21T13:00:00Z'),\n parseInstant('2026-03-21T10:00:00Z'),\n parseInstant('2026-03-21T12:00:00Z'),\n);\nconst bounded = clamped.toZonedDateTimeISO('UTC');\n\n// with unit comparison, clamp aligns to the requested unit boundary\nconst clampedByDay = clamp(\n parseInstant('2026-03-23T05:00:00Z'),\n parseInstant('2026-03-21T09:00:00Z'),\n parseInstant('2026-03-22T18:00:00Z'),\n { unit: 'day', tz: 'America/New_York' },\n);\n```\n\n## Comparison Helpers\n\n```ts\nimport { isAfter, isBefore, isSame, parseInstant } from '@vielzeug/tempo';\n\nisBefore(parseInstant('2026-03-21T10:00:00Z'), parseInstant('2026-03-21T11:00:00Z'));\nisAfter(parseInstant('2026-03-21T12:00:00Z'), parseInstant('2026-03-21T11:00:00Z'));\nisSame(parseInstant('2026-03-21T23:30:00Z'), parseInstant('2026-03-22T00:15:00Z'), {\n unit: 'day',\n tz: 'America/New_York',\n});\n\nisBefore(parseInstant('2026-03-21T23:30:00Z'), parseInstant('2026-03-22T00:15:00Z'), {\n unit: 'day',\n tz: 'UTC',\n});\n```\n\n## Start and End Boundaries\n\n```ts\nimport { endOf, parseInstant, startOf } from '@vielzeug/tempo';\n\nconst dayStart = startOf(parseInstant('2026-03-21T10:15:30Z'), 'day', { tz: 'UTC' });\nconst dayEnd = endOf(parseInstant('2026-03-21T10:15:30Z'), 'day', { tz: 'UTC' });\n\nconst weekStart = startOf(parseInstant('2026-03-21T10:15:30Z'), 'week', {\n tz: 'Europe/Berlin',\n weekStartsOn: 1,\n});\n```\n\n## Formatting\n\nUse `format()` for UI, `formatInstant()`/`formatZoned()` for machine output, `formatRelative()` for UX copy.\n\n```ts\nimport {\n format,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n parseInstant,\n} from '@vielzeug/tempo';\n\nconst instant = parseInstant('2026-03-21T10:15:30Z');\n\nformat(instant, { pattern: 'short', locale: 'en-GB', tz: 'UTC' });\nformatInstant(instant);\nformatZoned(instant, { tz: 'Europe/Berlin' });\n\nformatRange(parseInstant('2026-03-21T10:00:00Z'), parseInstant('2026-03-21T12:00:00Z'), {\n pattern: 'short',\n locale: 'en-US',\n tz: 'America/New_York',\n});\n\nformatRelative(parseInstant('2026-03-21T12:00:00Z'), {\n base: parseInstant('2026-03-21T10:00:00Z'),\n locale: 'en-US',\n numeric: 'always',\n});\n\n// formatParts / formatRangeParts — raw Intl parts for custom rendering\nconst parts = formatParts(instant, { pattern: 'date-only', tz: 'UTC' });\n// [{ type: 'month', value: '3' }, { type: 'literal', value: '/' }, ...]\n\nconst rangeParts = formatRangeParts(parseInstant('2026-03-21T10:00:00Z'), parseInstant('2026-03-21T12:00:00Z'), {\n pattern: 'short',\n locale: 'en-US',\n tz: 'UTC',\n});\nconst startOnly = rangeParts.filter((p) => p.source === 'startRange' || p.source === 'shared');\n```\n\n## Duration Helpers\n\n```ts\nimport { formatDuration, parseDuration } from '@vielzeug/tempo';\n\nconst duration = parseDuration('PT2H30M');\nconst text = formatDuration(duration, { locale: 'en-US', style: 'short' });\n```\n\n> **Note:** `formatDuration()` uses `Intl.DurationFormat` when available. In environments that do not support it, it falls back to a plain **English-only** representation (e.g., `'2 hours, 30 minutes'`).\n\n## Expiry and Classification\n\nUse `expires()` to classify a date into a named threshold bucket of your choosing.\n\n```ts\nimport { expires, humanize, now, parseInstant, shift, timeDiff } from '@vielzeug/tempo';\n\nconst THRESHOLDS = {\n longExpired: { days: -30 }, // more than 30 days past\n expired: { days: 0 }, // any past date\n critical: { days: 3 }, // within 3 days\n warning: { days: 14 }, // within 14 days\n safe: { years: 100 },\n} as const;\n\n// Use shift(now(tz), ...) for day-level offsets\nexpires(shift(now('UTC'), { days: -60 }).toInstant(), THRESHOLDS); // 'longExpired'\nexpires(shift(now('UTC'), { hours: 48 }).toInstant(), THRESHOLDS); // 'critical'\nexpires(shift(now('UTC'), { years: 200 }).toInstant(), THRESHOLDS); // null (no match)\n\n// Pin the reference time for deterministic behavior in tests\nconst pinnedNow = parseInstant('2026-06-01T00:00:00Z');\nexpires(parseInstant('2026-06-04T00:00:00Z'), THRESHOLDS, {}, pinnedNow); // 'critical'\n\n// timeDiff — largest-unit human-readable time difference\n// No tz needed when both are Instants\ntimeDiff(parseInstant('2026-01-01T00:00:00Z'), parseInstant('2027-06-01T00:00:00Z')); // { unit: 'year', value: 1 }\n\n// humanize converts a TimeDiffResult to a readable string (English only)\nhumanize(timeDiff(expiresAt)); // '3 days', '1 hour', etc.\n```\n\n## Date Ranges and Recurrence\n\nUse `dateRange()` to lazily generate sequences of `ZonedDateTime` values for calendars, reports, or iteration.\n\nWhen `start` is a `ZonedDateTime`, the timezone is inferred automatically — no need to pass `options`. For plain inputs, pass `options.tz` explicitly.\n\n```ts\nimport { dateRange, parseZoned, recurrence } from '@vielzeug/tempo';\n\n// dateRange returns a Generator — use for...of or spread to collect\nconst start = parseZoned('2026-03-01T00:00:00[UTC]');\nconst end = parseZoned('2026-03-31T00:00:00[UTC]');\n\n// ZonedDateTime inputs — tz inferred, no options needed\nfor (const day of dateRange(start, end, { days: 1 })) {\n render(day);\n}\n\n// Collect to array\nconst days = [...dateRange(start, end, { days: 1 })];\n\n// Every Monday in a date range\nconst mondays = [\n ...dateRange(parseZoned('2026-03-02T00:00:00[UTC]'), parseZoned('2026-03-30T00:00:00[UTC]'), { weeks: 1 }),\n];\n\n// recurrence — repeating dates with count or until\nconst meetingStart = parseZoned('2026-01-05T09:00:00[Europe/Berlin]');\nconst deadline = parseZoned('2026-06-30T00:00:00[Europe/Berlin]');\n\n// ZonedDateTime start — tz inferred, no options needed\nfor (const meeting of recurrence(meetingStart, { frequency: 'weekly', until: deadline })) {\n schedule(meeting);\n}\n\n// Every 3 months for 6 occurrences (tz inferred from ZonedDateTime start)\nconst quarters = [...recurrence(meetingStart, { frequency: 'monthly', interval: 3, count: 6 })];\n```\n\n## Framework Integration\n\nTempo is a pure-utility library with no subscription model. Use its functions directly wherever date/time values are formatted or computed.\n\n::: code-group\n\n```tsx [React]\nimport { format, now, parseInstant, shift } from '@vielzeug/tempo';\n\nfunction DeadlineLabel({ iso }: { iso: string }) {\n const deadline = parseInstant(iso);\n const tomorrow = shift(now('UTC'), { days: 1 });\n const isUrgent = deadline.epochMilliseconds < tomorrow.toInstant().epochMilliseconds;\n\n return <span className={isUrgent ? 'urgent' : ''}>{format(deadline, { locale: navigator.language })}</span>;\n}\n```\n\n```ts [Vue 3]\nimport { computed } from 'vue';\nimport { format, now, parseInstant, shift } from '@vielzeug/tempo';\n\nfunction useDeadlineLabel(iso: string) {\n return computed(() => {\n const deadline = parseInstant(iso);\n const tomorrow = shift(now('UTC'), { days: 1 });\n const isUrgent = deadline.epochMilliseconds < tomorrow.toInstant().epochMilliseconds;\n return { label: format(deadline, { locale: 'en' }), isUrgent };\n });\n}\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { format, now, parseInstant, shift } from '@vielzeug/tempo';\n\n export let iso: string;\n\n $: deadline = parseInstant(iso);\n $: isUrgent = deadline.epochMilliseconds < shift(now('UTC'), { days: 1 }).toInstant().epochMilliseconds;\n $: label = format(deadline, { locale: 'en' });\n</script>\n\n<span class:urgent={isUrgent}>{label}</span>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### With Rune\n\nFormat timestamps for structured log output using Tempo.\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\nimport { formatInstant, now } from '@vielzeug/tempo';\n\nconst log = createLogger({ namespace: 'app' });\n\nlog.info({ timestamp: formatInstant(now('UTC')) }, 'server started');\n```\n\n### With Vault\n\nUse TTL values derived from Tempo duration helpers.\n\n```ts\nimport { createLocalStorage, table, ttl } from '@vielzeug/vault';\nimport { shift, now } from '@vielzeug/tempo';\n\ntype Session = { id: string; token: string };\nconst schema = { sessions: table<Session>('id') };\nconst db = createLocalStorage('app', schema);\n\n// Store session with a 1-hour TTL\nconst expiresIn = shift(now('UTC'), { hours: 1 }).toInstant().epochMilliseconds - Date.now();\nawait db.put('sessions', { id: '1', token: 'abc' }, ttl.ms(expiresIn));\n```\n\n## Best Practices\n\n- Store `Temporal.Instant` values in databases and APIs — never store offset-aware strings.\n- Use `parsePlainDateTime()` at the system boundary when receiving wall-clock strings from external sources; use `parseInstant()` for UTC ISO strings; use `parse()` when the format is unknown.\n- Use `isValid()` as a type guard when accepting `TimeInput` from external data.\n- Convert to `ZonedDateTime` only when rendering to users; keep instants everywhere else.\n- Always pass `tz` when calling `toInstant()`, `shift()`, or `difference()` with plain inputs.\n- Use `format()` for UI labels, `formatInstant()` for transport/logging, and `formatZoned()` for zoned ISO strings.\n- Use `formatParts()` / `formatRangeParts()` when individual date parts need separate styling.\n- Use `formatRelative()` for UX copy (\"3 hours ago\") rather than computing the difference manually.\n- Prefer `dateRange()` over manual `while` loops when generating sequences of dates.\n",
|
|
7
|
+
"examples": "---\ntitle: Tempo — Examples\ndescription: Practical examples and recipes for tempo.\n---\n\n## Examples\n\n- [DST-Safe Arithmetic](./examples/dst-safe-arithmetic.md)\n- [Locale Formatting](./examples/locale-formatting.md)\n- [Timezone Conversion](./examples/timezone-conversion.md)\n- [Expiry Classification](./examples/expiry-classification.md)\n- [Date Ranges and Recurrence](./examples/date-ranges-and-recurrence.md)\n"
|
|
8
|
+
},
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "boundary-and-relative",
|
|
12
|
+
"code": "import { endOf, formatRelative, isBefore, isSame, parseInstant, startOf } from '@vielzeug/tempo'\n\n// Snap to calendar boundaries (tz required for non-ZonedDateTime inputs)\nconst event = parseInstant('2026-03-21T10:15:30Z')\n\nconst dayStart = startOf(event, 'day', { tz: 'UTC' })\nconst dayEnd = endOf(event, 'day', { tz: 'UTC' })\nconst weekStart = startOf(event, 'week', { tz: 'UTC', weekStartsOn: 1 })\n\nconsole.log('Day start:', dayStart.toString())\nconsole.log('Day end:', dayEnd.toString())\nconsole.log('Week start:', weekStart.toString())\n\n// Comparison helpers\nconst a = parseInstant('2026-03-21T10:00:00Z')\nconst b = parseInstant('2026-03-21T12:00:00Z')\n\nconsole.log('a < b:', isBefore(a, b))\nconsole.log('same day:', isSame(a, b, { unit: 'day', tz: 'UTC' }))\n\n// Relative formatting with a pinned base\nconst base = parseInstant('2026-03-21T10:00:00Z')\nconsole.log(formatRelative(b, { base, locale: 'en-US', numeric: 'always' }))\n// → 'in 2 hours'\nconsole.log(formatRelative(a, { base, locale: 'en-US' }))\n// → 'now'",
|
|
13
|
+
"name": "Boundary & Relative Time"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "dst-safe-arithmetic",
|
|
17
|
+
"code": "import { formatInstant, parseInstant, parseZoned, shift } from '@vielzeug/tempo'\n\nconst beforeDst = parseZoned('2026-03-08T01:30:00-05:00[America/New_York]')\nconst plusOneHour = shift(beforeDst, { hours: 1 })\n\nconsole.log('Before:', beforeDst.toString())\nconsole.log('After +1h:', plusOneHour.toString())\n// shift() always returns ZonedDateTime — call .toInstant() if you need an Instant\nconsole.log('As Instant:', plusOneHour.toInstant().toString())\nconsole.log('Formatted:', formatInstant(plusOneHour))\n\n// Works with Instant input too (tz required)\nconst instant = parseInstant('2026-03-08T06:30:00Z')\nconst shifted = shift(instant, { hours: 2 }, { tz: 'America/New_York' })\nconsole.log('Shifted ZDT:', shifted.toString())\nconsole.log('Back to Instant:', shifted.toInstant().toString())",
|
|
18
|
+
"name": "DST-Safe Arithmetic"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "duration-and-projection",
|
|
22
|
+
"code": "import { difference, formatDuration, inTz, parseDuration, parseZoned } from '@vielzeug/tempo'\n\n// --- formatDuration() ---\n// ISO 8601 string → human-readable\nconsole.log(formatDuration('P1Y2M3DT4H')) // '1 year, 2 months, 3 days, 4 hours'\nconsole.log(formatDuration('PT30M')) // '30 minutes'\nconsole.log(formatDuration({ seconds: 90 })) // '1 minute, 30 seconds'\nconsole.log(formatDuration({ microseconds: 500, nanoseconds: 250 }))\n// → '500 microseconds, 250 nanoseconds' (sub-ms units fully supported)\n\n// --- parseDuration() ---\nconst dur = parseDuration('P0Y2M3DT0H5M10S')\nconsole.log('months:', dur.months, 'days:', dur.days, 'minutes:', dur.minutes)\n\n// --- difference() produces a Duration ---\nconst a = parseZoned('2026-01-01T00:00:00[UTC]')\nconst b = parseZoned('2026-06-15T09:30:00[UTC]')\nconst d = difference(a, b, { largestUnit: 'month' })\nconsole.log(formatDuration(d)) // '5 months, 14 days, 9 hours, 30 minutes'\n\n// --- inTz() re-projection ---\n// Same absolute instant, different wall-clock\nconst berlin = parseZoned('2026-03-21T11:00:00+01:00[Europe/Berlin]')\nconst utc = inTz(berlin, 'UTC')\nconst tokyo = inTz(berlin, 'Asia/Tokyo')\n\nconsole.log('Berlin:', berlin.toString())\nconsole.log('UTC:', utc.toString()) // wall-clock: 10:00 UTC\nconsole.log('Tokyo:', tokyo.toString()) // wall-clock: 19:00 JST\nconsole.log('Same instant?', berlin.toInstant().epochMilliseconds === utc.toInstant().epochMilliseconds)",
|
|
23
|
+
"name": "Duration & Timezone Projection"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "error-handling",
|
|
27
|
+
"code": "import { TempoError, TempoInvalidTzError, TempoMissingTzError, inTz, parse, parsePlainDateTime, toInstant } from '@vielzeug/tempo';\n\n// ── TempoInvalidInputError: bad parse string ─────────────────────────────────\ntry {\n parse('not-a-date');\n} catch (e) {\n if (e instanceof TempoError) {\n console.log(e.name); // 'TempoInvalidInputError'\n console.log(e.message); // 'Unable to parse date/time string: \"not-a-date\". ...'\n }\n}\n\n// ── TempoMissingTzError: plain input without timezone ────────────────────────\ntry {\n toInstant(parsePlainDateTime('2026-03-21T10:00:00'));\n} catch (e) {\n if (e instanceof TempoMissingTzError) {\n console.log(e.name); // 'TempoMissingTzError'\n }\n}\n\n// ── TempoInvalidTzError: bad timezone string ─────────────────────────────────\ntry {\n inTz(parse('2026-03-21T10:00:00Z', 'instant'), 'Not/AZone');\n} catch (e) {\n if (e instanceof TempoInvalidTzError) {\n console.log(e.name); // 'TempoInvalidTzError'\n }\n}\n\n// ── Every subtype is also a TempoError (single catch-all) ───────────────────\ntry {\n parse('bad');\n} catch (e) {\n console.log(e instanceof Error); // true\n console.log(e instanceof TempoError); // true\n console.log(TempoError.is(e)); // true — static type guard, same check\n}\n",
|
|
28
|
+
"name": "TempoError — instanceof checks across the error hierarchy"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "expires-and-date-range",
|
|
32
|
+
"code": "import { dateRange, expires, humanize, now, parseInstant, parseZoned, recurrence, shift, timeDiff } from '@vielzeug/tempo'\n\n// --- expires() ---\nconst THRESHOLDS = {\n longExpired: { days: -30 },\n expired: { days: 0 },\n critical: { days: 3 },\n warning: { days: 14 },\n safe: { years: 100 },\n}\n\n// Use tempo helpers — no Temporal.* needed\nconst past = shift(now('UTC'), { hours: -1 }).toInstant()\nconst soon = shift(now('UTC'), { hours: 48 }).toInstant()\nconst later = shift(now('UTC'), { days: 10 }).toInstant()\n\nconsole.log('past:', expires(past, THRESHOLDS)) // 'expired'\nconsole.log('soon:', expires(soon, THRESHOLDS)) // 'critical'\nconsole.log('later:', expires(later, THRESHOLDS)) // 'warning'\n\n// --- expires() + timeDiff() composition ---\nconst key = expires(soon, THRESHOLDS)\nconst diff = timeDiff(now('UTC').toInstant(), soon)\nconsole.log('key:', key, '|', humanize(diff)) // 'critical | 2 days'\nconsole.log('localized:', humanize(diff, { locale: 'ar-EG' })) // Arabic numerals\n\n// --- timeDiff() ---\nconst a = parseInstant('2026-01-01T00:00:00Z')\nconst b = parseInstant('2027-06-15T00:00:00Z')\nconsole.log('timeDiff:', timeDiff(a, b).value, timeDiff(a, b).unit) // 1 year\n\n// --- dateRange() — tz inferred from ZonedDateTime, no options needed ---\nconst start = parseZoned('2026-03-01T00:00:00[America/New_York]')\nconst end = parseZoned('2026-03-05T00:00:00[America/New_York]')\n\nfor (const day of dateRange(start, end, { days: 1 })) {\n console.log(day.toPlainDate().toString(), day.timeZoneId)\n}\n\n// --- recurrence() — tz inferred from ZonedDateTime start ---\nconst weekly = recurrence(start, { frequency: 'weekly', count: 3 })\nfor (const date of weekly) {\n console.log('weekly:', date.toPlainDate().toString())\n}",
|
|
33
|
+
"name": "Expires & Date Range"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "meeting-duration",
|
|
37
|
+
"code": "import { difference, parseInstant, timeDiff, within } from '@vielzeug/tempo'\n\nconst start = parseInstant('2026-03-21T10:00:00Z')\nconst end = parseInstant('2026-03-21T12:45:00Z')\n\n// Two Instants with sub-day units — no tz needed\nconst duration = difference(start, end, {\n largestUnit: 'hour',\n smallestUnit: 'minute',\n})\n\nconsole.log('Duration ISO:', duration.toString())\nconsole.log('11:00 in range:', within(parseInstant('2026-03-21T11:00:00Z'), start, end))\n\n// Human-readable largest-unit diff\nconst diff = timeDiff(start, end)\nconsole.log('timeDiff:', diff.value, diff.unit)",
|
|
38
|
+
"name": "Meeting Duration"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "timezone-aware-scheduling",
|
|
42
|
+
"code": "import { format, formatZoned, inTz, parsePlainDateTime, toInstant } from '@vielzeug/tempo'\n\nconst meetingUtc = toInstant(parsePlainDateTime('2026-04-15T14:00:00'), { tz: 'UTC' })\nconst attendees = [\n { name: 'Alice', tz: 'America/New_York' },\n { name: 'Bruno', tz: 'Europe/Berlin' },\n { name: 'Keiko', tz: 'Asia/Tokyo' },\n]\n\nfor (const attendee of attendees) {\n const local = inTz(meetingUtc, attendee.tz)\n console.log(attendee.name + ':', format(local, { pattern: 'long', locale: 'en-US' }))\n console.log(' zoned:', formatZoned(meetingUtc, { tz: attendee.tz }))\n}",
|
|
43
|
+
"name": "Timezone-Aware Scheduling"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "timezone-conversion",
|
|
47
|
+
"code": "import { format, formatInstant, formatZoned, inTz, parse } from '@vielzeug/tempo'\n\n// parse() auto-detects the type; 'instant' narrows the return type\nconst utc = parse('2026-03-21T10:15:30Z', 'instant')\nconst tokyo = inTz(utc, 'Asia/Tokyo')\nconst berlin = inTz(utc, 'Europe/Berlin')\n\nconsole.log('UTC instant:', formatInstant(utc))\nconsole.log('Tokyo zoned:', formatZoned(tokyo))\nconsole.log('Berlin wall-clock:', format(berlin, { pattern: 'short', locale: 'de-DE' }))\n\n// Re-projecting a ZonedDateTime: same instant, new wall-clock\nconst berlinToNY = inTz(berlin, 'America/New_York')\nconsole.log('Berlin → New York:', berlinToNY.toString())",
|
|
48
|
+
"name": "Timezone Projection with inTz"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"typeSignatures": {
|
|
52
|
+
"Temporal": "export { Temporal } from '@js-temporal/polyfill';",
|
|
53
|
+
"TempoError": "export {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';",
|
|
54
|
+
"TempoInvalidInputError": "export {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';",
|
|
55
|
+
"TempoInvalidTzError": "export {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';",
|
|
56
|
+
"TempoMissingTzError": "export {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';",
|
|
57
|
+
"TempoUnsupportedInputError": "export {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';",
|
|
58
|
+
"difference": "export {\n difference,\n isValid,\n now,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDate,\n parsePlainDateTime,\n parseZoned,\n shift,\n} from './core';",
|
|
59
|
+
"isValid": "export {\n difference,\n isValid,\n now,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDate,\n parsePlainDateTime,\n parseZoned,\n shift,\n} from './core';",
|
|
60
|
+
"now": "export {\n difference,\n isValid,\n now,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDate,\n parsePlainDateTime,\n parseZoned,\n shift,\n} from './core';",
|
|
61
|
+
"nowInstant": "export {\n difference,\n isValid,\n now,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDate,\n parsePlainDateTime,\n parseZoned,\n shift,\n} from './core';",
|
|
62
|
+
"parse": "export {\n difference,\n isValid,\n now,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDate,\n parsePlainDateTime,\n parseZoned,\n shift,\n} from './core';",
|
|
63
|
+
"parseInstant": "export {\n difference,\n isValid,\n now,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDate,\n parsePlainDateTime,\n parseZoned,\n shift,\n} from './core';",
|
|
64
|
+
"parsePlainDate": "export {\n difference,\n isValid,\n now,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDate,\n parsePlainDateTime,\n parseZoned,\n shift,\n} from './core';",
|
|
65
|
+
"parsePlainDateTime": "export {\n difference,\n isValid,\n now,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDate,\n parsePlainDateTime,\n parseZoned,\n shift,\n} from './core';",
|
|
66
|
+
"parseZoned": "export {\n difference,\n isValid,\n now,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDate,\n parsePlainDateTime,\n parseZoned,\n shift,\n} from './core';",
|
|
67
|
+
"shift": "export {\n difference,\n isValid,\n now,\n nowInstant,\n parse,\n parseInstant,\n parsePlainDate,\n parsePlainDateTime,\n parseZoned,\n shift,\n} from './core';",
|
|
68
|
+
"inTz": "export { inTz, toInstant } from './_convert';",
|
|
69
|
+
"toInstant": "export { inTz, toInstant } from './_convert';",
|
|
70
|
+
"endOf": "export { endOf, startOf } from './boundary';",
|
|
71
|
+
"startOf": "export { endOf, startOf } from './boundary';",
|
|
72
|
+
"clamp": "export { clamp, isAfter, isBefore, isSame, within } from './compare';",
|
|
73
|
+
"isAfter": "export { clamp, isAfter, isBefore, isSame, within } from './compare';",
|
|
74
|
+
"isBefore": "export { clamp, isAfter, isBefore, isSame, within } from './compare';",
|
|
75
|
+
"isSame": "export { clamp, isAfter, isBefore, isSame, within } from './compare';",
|
|
76
|
+
"within": "export { clamp, isAfter, isBefore, isSame, within } from './compare';",
|
|
77
|
+
"format": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
78
|
+
"formatDuration": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
79
|
+
"formatInstant": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
80
|
+
"formatParts": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
81
|
+
"formatRange": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
82
|
+
"formatRangeParts": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
83
|
+
"formatRelative": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
84
|
+
"formatZoned": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
85
|
+
"humanize": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
86
|
+
"parseDuration": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
87
|
+
"expires": "export { expires, timeDiff } from './classify';",
|
|
88
|
+
"timeDiff": "export { expires, timeDiff } from './classify';",
|
|
89
|
+
"dateRange": "export { dateRange, recurrence } from './range';",
|
|
90
|
+
"recurrence": "export { dateRange, recurrence } from './range';",
|
|
91
|
+
"BoundaryOptions": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
92
|
+
"BoundaryUnit": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
93
|
+
"CalendarUnit": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
94
|
+
"CompareOptions": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
95
|
+
"DateTimeDisambiguation": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
96
|
+
"DifferenceOptions": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
97
|
+
"DisambiguationOptions": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
98
|
+
"DurationFormatOptions": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
99
|
+
"FormatOptions": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
100
|
+
"FormatPattern": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
101
|
+
"ParseAs": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
102
|
+
"RecurrenceRule": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
103
|
+
"RelativeFormatOptions": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
104
|
+
"RelativeTimeInput": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
105
|
+
"ShiftOptions": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
106
|
+
"TempoUnit": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
107
|
+
"TimeDiffResult": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
108
|
+
"TimeDiffUnit": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
109
|
+
"TimeInput": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
110
|
+
"TimeOptions": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';",
|
|
111
|
+
"WeekStartDay": "export type {\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n CompareOptions,\n DateTimeDisambiguation,\n DifferenceOptions,\n DisambiguationOptions,\n DurationFormatOptions,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeOptions,\n WeekStartDay,\n} from './types';"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "export { createIndexedDB } from './adapters/indexeddb';\nexport { createMemory } from './adapters/memory';\nexport { createLocalStorage, createSessionStorage } from './adapters/webstorage';\nexport { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';\nexport { defineMigration } from './migration';\nexport type { MigrationStep } from './migration';\nexport type { QueryBuilder } from './query';\nexport { scheduleExpiredPrune } from './prune';\nexport { isExpired, ttl } from './ttl';\nexport { table } from './types';\nexport type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';\n",
|
|
3
|
+
"docs": {
|
|
4
|
+
"index": "---\ntitle: Vault — Typed browser storage\ndescription: Typed browser storage with portable keys, TTL, observation, and explicit IndexedDB transactions.\npackage: vault\ncategory: Storage\nkeywords: [storage, indexeddb, localstorage, sessionstorage, ttl, browser]\nrelated: [courier, forge, ripple]\nexports: [createMemory, createLocalStorage, createSessionStorage, createIndexedDB, table, ttl, scheduleExpiredPrune, defineMigration, isExpired]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"vault\" />\n\n## Why Vault?\n\nVault gives browser storage one typed schema while keeping backend guarantees explicit. Use `VaultStore` for portable CRUD and observation; choose IndexedDB only when you need atomic transactions or cursor iteration.\n\n```ts\n// Before\nlocalStorage.setItem('theme', JSON.stringify({ value: 'dark' }));\nconst theme = JSON.parse(localStorage.getItem('theme') ?? '{}').value;\n\n// After\nawait store.put('preferences', { id: 'theme', value: 'dark' });\nconst theme = await store.get('preferences', 'theme');\n```\n\n| Feature | Vault | Raw Web Storage | Dexie |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"vault\" type=\"size\" /> | Browser built-in | Extra dependency |\n| Runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed schema and keys | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Portable Memory/Web Storage API | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | IndexedDB only |\n| Explicit atomic transactions | IndexedDB capability | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Vault when** you need typed browser persistence with one portable CRUD API and explicit IndexedDB-only capabilities.\n\n**Consider raw Web Storage when** you only persist one or two unstructured values. **Consider Dexie when** you need a broader IndexedDB ecosystem.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/vault\n```\n\n```sh [npm]\nnpm install @vielzeug/vault\n```\n\n```sh [yarn]\nyarn add @vielzeug/vault\n```\n\n:::\n\n## Quick Start\n\nDefine a schema, create a portable store, and dispose it with its owner.\n\n```ts\nimport { createLocalStorage, table } from '@vielzeug/vault';\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<{ id: string; theme: 'dark' | 'light' }>('id') },\n});\n\ntry {\n await store.put('preferences', { id: 'theme', theme: 'dark' });\n console.log(await store.get('preferences', 'theme'));\n} finally {\n await store.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `table()` defines typed records with portable string or number keys.\n- `createMemory()`, `createLocalStorage()`, and `createSessionStorage()` return portable `VaultStore` instances.\n- `observe()` emits current and changed table snapshots.\n- `ttl` creates validated expiration durations.\n- `createIndexedDB()` returns `IndexedDbVaultStore` with `batch()` and `iterate()`.\n- `defineMigration()` handles IndexedDB schema upgrades.\n- `scheduleExpiredPrune()` removes stale TTL entries on an owned schedule.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Forge](../forge/index.md) saves and restores form drafts through Vault stores.\n- [Ripple](../ripple/index.md) owns application state that can persist through Vault.\n- [Courier](../courier/index.md) can populate persistent cache data.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Vault — API Reference\ndescription: Typed browser storage with portable keys and capability-specific stores.\n---\n\n[[toc]]\n\n## API Overview\n\nVault has a portable `VaultStore<S>` and an IndexedDB-only `IndexedDbVaultStore<S>`.\n\n## Package Entry Point\n\n```ts\nimport { createIndexedDB, createLocalStorage, createMemory, createSessionStorage, table, ttl } from '@vielzeug/vault';\n```\n\n## Factories\n\n```ts\ncreateMemory({ schema, name? }): VaultStore<S>\ncreateLocalStorage({ name, schema }): VaultStore<S>\ncreateSessionStorage({ name, schema }): VaultStore<S>\ncreateIndexedDB({ name, schema, version?, migrate? }): IndexedDbVaultStore<S>\n```\n\nAll factories use the same fixed storage envelope: `{ value, expiresAt? }`. Primary-key fields must be `string` or finite `number`; Vault's tagged encoding preserves the distinction between `1` and `'1'` in every adapter.\n\n## VaultStore\n\nThe portable store supports CRUD (`get`, `put`, `getAll`, `putAll`, `delete`, `deleteMany`, `clear`), key and entry reads, `update`, `upsert`, `getOrDefault`, `query`, TTL pruning, `debug`, and lifecycle disposal.\n\n```ts\nconst stop = store.observe('users', (users) => render(users), { immediate: true });\nstop();\n```\n\n`observe()` is the only reactivity API. It emits the current table snapshot by default, then emits after mutations. Pass `{ immediate: false }` to wait for the first mutation or `{ signal }` for AbortSignal-owned cleanup.\n\n## IndexedDbVaultStore\n\n`createIndexedDB` adds cursor iteration and atomic transactions:\n\n```ts\nawait db.batch(['users'], async (tx) => {\n await tx.put('users', { id: 1, name: 'Ada' });\n await tx.delete('users', 2);\n});\n\nfor await (const user of db.iterate('users')) console.log(user);\n```\n\n`batch()` is unavailable on memory and Web Storage stores because only IndexedDB can guarantee atomic commits.\n\n## Schema and TTL\n\n```ts\nimport { table, ttl } from '@vielzeug/vault';\n\ntype Session = { id: string; userId: string };\nconst schema = { sessions: table<Session>('id').ttl(ttl.hours(1)).index('userId') };\n```\n\n`ttl.ms`, `seconds`, `minutes`, `hours`, and `days` return branded positive durations. IndexedDB indexes always target `value.<field>` because the storage envelope is fixed.\n\n## Migration from Vault 1\n\n`Adapter`, `MemoryAdapter`, `IndexedDbAdapter`, codecs, `watch`, `observeMany`, constructor signals, and `toReadableStream` are removed. Use `VaultStore`, `IndexedDbVaultStore`, fixed envelopes, and `observe()` instead. Existing custom-encoded data must be migrated or cleared before use with this release.\n",
|
|
6
|
+
"usage": "---\ntitle: Vault — Usage Guide\ndescription: Persist typed browser data, observe table snapshots, and use IndexedDB transactions.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a portable store with one schema and write a typed row.\n\n```ts\nimport { createLocalStorage, table } from '@vielzeug/vault';\n\ninterface Preference {\n id: string;\n theme: 'dark' | 'light';\n}\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n\nawait store.put('preferences', { id: 'theme', theme: 'dark' });\nconsole.log(await store.get('preferences', 'theme'));\n```\n\n## Create a Portable Store\n\nMemory, LocalStorage, and SessionStorage return `VaultStore`. They share portable string/number keys, fixed `{ value, expiresAt? }` envelopes, CRUD methods, queries, TTL, and `observe()`.\n\nUse a new storage name when upgrading from Vault 1. Old key and envelope formats are not read by Vault 2.\n\n```ts\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n```\n\n## Read and Change Records\n\nUse `update()` for an existing row and `upsert()` when the row may not exist.\n\n```ts\nconst updated = await store.update('preferences', 'theme', { theme: 'light' });\n\nawait store.upsert('preferences', 'locale', (current) => ({\n id: 'locale',\n theme: current?.theme ?? 'dark',\n}));\n\nconsole.log(updated);\n```\n\n`update()` returns `undefined` for a missing key. `upsert()` always writes the record returned by its callback.\n\n## Use TTL and Pruning\n\nUse `ttl.*` helpers for expiring rows. Schedule pruning when stale rows can accumulate without reads.\n\n```ts\nimport { scheduleExpiredPrune, ttl } from '@vielzeug/vault';\n\nawait store.put('preferences', { id: 'temporary', theme: 'dark' }, ttl.hours(1));\nconst stopPrune = scheduleExpiredPrune(store, { interval: ttl.hours(6), signal: store.disposalSignal });\n\nstopPrune();\n```\n\n## Observe a Table\n\nUse `observe()` for current and future snapshots. Tie subscription lifetime to an `AbortSignal` when a component or request owns it.\n\n```ts\nconst controller = new AbortController();\n\nstore.observe('preferences', (preferences) => {\n console.log(preferences);\n}, { signal: controller.signal });\n\ncontroller.abort();\n```\n\n## Use IndexedDB for Atomic Work\n\nChoose IndexedDB when multiple writes must commit together or when you need cursor iteration.\n\n```ts\nimport { createIndexedDB, table } from '@vielzeug/vault';\n\nconst db = createIndexedDB({\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n});\n\nawait db.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened' });\n await tx.put('events', { id: 2, type: 'saved' });\n});\n```\n\nOnly await `tx.*` operations inside a batch callback. Do not await timers, fetches, or other external asynchronous work; IndexedDB can commit an inactive transaction.\n\n## Handle Schema Migrations\n\nDeclare indexes in schema. Use `migrate` only for version upgrades and mirror Vault’s fixed `value.<field>` index path.\n\n```ts\nimport { createIndexedDB, table, type MigrationFn } from '@vielzeug/vault';\n\nconst schema = { users: table<{ id: number; name: string }>('id').index('name') };\nconst migrate: MigrationFn = ({ db, oldVersion, tx }) => {\n if (oldVersion < 2 && db.objectStoreNames.contains('users')) {\n tx.objectStore('users').createIndex('name', 'value.name');\n }\n};\n\ncreateIndexedDB({ name: 'app-v2', migrate, schema, version: 2 });\n```\n\n## Upgrade from Vault 1\n\n- Replace `Adapter` with `VaultStore`.\n- Replace `IndexedDbAdapter` with `IndexedDbVaultStore`.\n- Replace `watch`, `observeMany`, signals, and streams with per-table `observe()`.\n- Remove codecs and versioned codecs. Start a new storage namespace or migrate data outside Vault before construction.\n- Move atomic code to `createIndexedDB().batch()`.\n\n## Framework Integration\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const [rows, setRows] = useState<RecordOf<S, K>[]>([]);\n\n useEffect(() => store.observe(table, setRows), [store, table]);\n return rows;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const rows = shallowRef<RecordOf<S, K>[]>([]);\n const stop = store.observe(table, (next) => (rows.value = next));\n\n onUnmounted(stop);\n return rows;\n}\n```\n\n```ts [Svelte]\nimport { readable } from 'svelte/store';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function tableStore<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n return readable<RecordOf<S, K>[]>([], (set) => store.observe(table, set));\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Forge’s Vault helpers for explicit form-draft persistence. Keep Ripple signals as application state and persist selected changes through Vault writes.\n\n## Best Practices\n\n- Define one schema per storage namespace.\n- Use string or finite-number primary keys only.\n- Choose a new namespace for Vault 1 storage unless you migrate it yourself.\n- Use `observe()` for table snapshots.\n- Use IndexedDB for atomic work.\n- Keep external asynchronous work outside `batch()` callbacks.\n- Use `ttl.*` instead of raw durations.\n- Dispose stores when their owner ends.\n",
|
|
7
|
+
"examples": "---\ntitle: Vault — Examples\ndescription: Portable storage, observation, and IndexedDB transactions.\n---\n\n- [CRUD](./examples/crud.md)\n- [TTL](./examples/ttl.md)\n- [Querying](./examples/querying.md)\n- [Reactive observation](./examples/reactive.md)\n- [IndexedDB iteration](./examples/iterate.md)\n- [IndexedDB batch transactions](./examples/batch.md)\n- [Plugin validation](./examples/plugins.md)\n"
|
|
8
|
+
},
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "basic-setup",
|
|
12
|
+
"code": "import { createLocalStorage, table } from '@vielzeug/vault'\n\nconst schema = {\n users: table('id'),\n}\n\nconst db = createLocalStorage({ name: 'demo', schema })\n\nawait db.put('users', { id: 1, name: 'Alice', email: 'alice@example.com' })\nawait db.put('users', { id: 2, name: 'Bob', email: 'bob@example.com' })\n\nconsole.log('Get user 1:', await db.get('users', 1))\nconsole.log('All users:', await db.getAll('users'))\nconsole.log('Count:', await db.query('users').count())",
|
|
13
|
+
"name": "Basic Setup - Initialize Vault"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "bulk-operations",
|
|
17
|
+
"code": "import { createLocalStorage, table } from '@vielzeug/vault'\n\nconst schema = {\n items: table('id'),\n}\n\nconst db = createLocalStorage({ name: 'bulk-demo', schema })\n\nconst items = Array.from({ length: 10 }, (_, index) => ({\n id: index + 1,\n value: +(Math.random() * 1000).toFixed(2),\n}))\n\nawait db.putAll('items', items)\nconsole.log('Inserted', items.length, 'items')\n\n// getMany — fetch multiple by key in one call (missing keys return undefined)\nconst [first, missing, third] = await db.getMany('items', [1, 99, 3])\nconsole.log('getMany [1, 99, 3]:', first?.id, missing, third?.id)\n\n// deleteMany — remove multiple by key, returns count deleted\nconst deleted = await db.deleteMany('items', [1, 2, 3, 99])\nconsole.log('deleteMany [1,2,3,99] deleted:', deleted) // 3 (99 did not exist)\n\n// query-based delete for filter-driven removal\nconst queryDeleted = await db.query('items').filter((item) => item.id <= 6).delete()\nconsole.log('Query-deleted items with id ≤ 6:', queryDeleted)\n\nconsole.log('Remaining count:', await db.query('items').count())\nconsole.log('First remaining item:', await db.query('items').orderBy('id', 'asc').first())",
|
|
18
|
+
"name": "Bulk Operations"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "cache-first",
|
|
22
|
+
"code": "import { createLocalStorage, table, ttl } from '@vielzeug/vault'\n\nconst db = createLocalStorage({ name: 'cache-demo', schema: { cache: table('id') } })\n\nasync function getOrComputeConfig() {\n return db.getOrDefault('cache', 'config', () => ({\n id: 'config',\n data: 'computed value',\n fetchedAt: Date.now(),\n }), ttl.minutes(5))\n}\n\nconst first = await getOrComputeConfig()\nconst second = await getOrComputeConfig()\nconsole.log('Same cached record:', first.fetchedAt === second.fetchedAt)",
|
|
23
|
+
"name": "Cache-First with getOrDefault"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "crud-operations",
|
|
27
|
+
"code": "import { createLocalStorage, table } from '@vielzeug/vault'\n\nconst schema = {\n users: table('id'),\n}\n\nconst db = createLocalStorage({ name: 'demo', schema })\n\nawait db.put('users', { id: 1, name: 'Alice', email: 'alice@example.com', age: 25 })\nawait db.put('users', { id: 2, name: 'Bob', email: 'bob@example.com', age: 30 })\nconsole.log('Created 2 users')\n\nconsole.log('Get user 1:', await db.get('users', 1))\nconsole.log('Count:', await db.count('users'))\nconsole.log('isEmpty before clear:', await db.isEmpty('users')) // false\n\nawait db.update('users', 1, { age: 26, name: 'Alice Smith' })\nconsole.log('Updated user 1:', await db.get('users', 1))\n\nconsole.log('Deleted user 2:', await db.delete('users', 2))\nconsole.log('Remaining users:', await db.getAll('users'))\n\nawait db.clear('users')\nconsole.log('isEmpty after clear:', await db.isEmpty('users')) // true",
|
|
28
|
+
"name": "CRUD Operations"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "prune-schedule",
|
|
32
|
+
"code": "import { createMemory, scheduleExpiredPrune, table, ttl } from '@vielzeug/vault'\n\n// scheduleExpiredPrune runs pruneExpired() on an interval and stops\n// automatically when the adapter is disposed (VaultDisposedError).\n// Use onError to surface unexpected failures instead of silently swallowing them.\n\nconst schema = { sessions: table('token') }\nconst db = createMemory({ schema })\n\nconst stop = scheduleExpiredPrune(db, {\n interval: ttl.minutes(15),\n onError: (err) => console.error('[vault] prune failed:', err),\n})\n\n// Write a session that expires in 1 ms\nawait db.put('sessions', { token: 'abc', user: 1 }, ttl.ms(1))\nawait db.put('sessions', { token: 'def', user: 2 }) // no TTL — permanent\n\nconsole.log('before prune:', await db.count('sessions')) // 2 (lazy eviction: both exist physically)\n\n// Manual prune to demonstrate the API\nconst pruned = await db.pruneExpired()\nconsole.log('pruned:', pruned.sessions) // 1 (the expired session)\nconsole.log('after prune:', await db.count('sessions')) // 1\n\n// When the adapter is disposed, the schedule stops automatically\ndb.dispose()\nstop() // or call stop() explicitly before dispose",
|
|
33
|
+
"name": "TTL — scheduleExpiredPrune with onError"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "query-builder",
|
|
37
|
+
"code": "import { createLocalStorage, table } from '@vielzeug/vault'\n\nconst schema = {\n products: table('id'),\n}\n\nconst db = createLocalStorage({ name: 'shop', schema })\n\nawait db.putAll('products', [\n { id: 1, name: 'Laptop', price: 999, category: 'electronics', inStock: true },\n { id: 2, name: 'Mouse', price: 29, category: 'electronics', inStock: true },\n { id: 3, name: 'Desk', price: 299, category: 'furniture', inStock: false },\n { id: 4, name: 'Chair', price: 199, category: 'furniture', inStock: true },\n { id: 5, name: 'Monitor', price: 399, category: 'electronics', inStock: true },\n])\n\nconst pageSize = 2\nconst pageIndex = 0\n\n// Build a base query — reuse it for both the page slice and the total count\nconst q = db\n .query('products')\n .equals('category', 'electronics')\n .filter((p) => p.inStock)\n .orderBy('price', 'asc')\n\n// count() respects limit/offset — returns records in the current page\nconst page = await q.limit(pageSize).offset(pageIndex * pageSize).toArray()\nconst pageCount = await q.limit(pageSize).offset(pageIndex * pageSize).count()\n\n// totalCount() ignores limit/offset/orderBy — returns the full filtered set\nconst total = await q.totalCount()\n\nconsole.log('Page:', page.map((p) => p.name))\nconsole.log('Page count:', pageCount, '/ Total matching:', total)\nconsole.log('Page 1 of', Math.ceil(total / pageSize))\n\n// startsWith with case-insensitive flag\nconst mice = await db.query('products').startsWith('name', 'm', { ignoreCase: true }).toArray()\nconsole.log('Starts with m:', mice.map((p) => p.name))\n\n// predicate delete\nconst removed = await db.query('products').filter((p) => !p.inStock).delete()\nconsole.log('Removed out-of-stock:', removed)\n\n// first()\nconst cheapest = await db.query('products').orderBy('price', 'asc').first()\nconsole.log('Cheapest:', cheapest?.name, cheapest?.price)",
|
|
38
|
+
"name": "Query Builder — Filters, Pagination, totalCount"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "reactive-observe",
|
|
42
|
+
"code": "import { createMemory, table } from '@vielzeug/vault'\n\nconst db = createMemory({ schema: { users: table('id') } })\nconst snapshots = []\nconst stop = db.observe('users', (users) => snapshots.push(users.map((user) => user.name)))\n\nawait Promise.resolve()\nawait db.put('users', { id: 1, name: 'Ada' })\nawait Promise.resolve()\n\nconsole.log(snapshots) // [[], ['Ada']]\nstop()\ndb.dispose()",
|
|
43
|
+
"name": "Reactive — observe()"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "ttl-expiration",
|
|
47
|
+
"code": "import { createLocalStorage, table, ttl } from '@vielzeug/vault'\n\nconst schema = {\n cache: table('id'),\n}\n\nconst db = createLocalStorage({ name: 'cache-demo', schema })\n\n// ttl helpers produce a branded TtlMs value — raw numbers are rejected by the type system\nawait db.put('cache', { id: 'short', data: 'Expires in 1 second' }, ttl.seconds(1))\nawait db.put('cache', { id: 'long', data: 'Expires in 5 minutes' }, ttl.minutes(5))\nconsole.log('Stored records with TTL')\nconsole.log('Immediate read:', await db.get('cache', 'short'))\n\nawait new Promise((resolve) => setTimeout(resolve, 1500))\nconsole.log('After 1.5s:', await db.get('cache', 'short')) // expired — undefined\nconsole.log('Long-lived still here:', await db.get('cache', 'long'))\n\nconsole.log('ttl helpers:', {\n '100ms': ttl.ms(100),\n '5 minutes': ttl.minutes(5),\n '2 hours': ttl.hours(2),\n '7 days': ttl.days(7),\n})",
|
|
48
|
+
"name": "TTL & Expiration"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"typeSignatures": {
|
|
52
|
+
"createIndexedDB": "export { createIndexedDB } from './adapters/indexeddb';",
|
|
53
|
+
"createMemory": "export { createMemory } from './adapters/memory';",
|
|
54
|
+
"createLocalStorage": "export { createLocalStorage, createSessionStorage } from './adapters/webstorage';",
|
|
55
|
+
"createSessionStorage": "export { createLocalStorage, createSessionStorage } from './adapters/webstorage';",
|
|
56
|
+
"VaultDisposedError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
|
|
57
|
+
"VaultError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
|
|
58
|
+
"VaultMigrationError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
|
|
59
|
+
"VaultQuotaError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
|
|
60
|
+
"VaultScopeError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
|
|
61
|
+
"defineMigration": "export { defineMigration } from './migration';",
|
|
62
|
+
"MigrationStep": "export type { MigrationStep } from './migration';",
|
|
63
|
+
"QueryBuilder": "export type { QueryBuilder } from './query';",
|
|
64
|
+
"scheduleExpiredPrune": "export { scheduleExpiredPrune } from './prune';",
|
|
65
|
+
"isExpired": "export { isExpired, ttl } from './ttl';",
|
|
66
|
+
"ttl": "export { isExpired, ttl } from './ttl';",
|
|
67
|
+
"table": "export { table } from './types';",
|
|
68
|
+
"AnySchema": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
69
|
+
"BaseAdapterOptions": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
70
|
+
"DebugInfo": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
71
|
+
"DebugStats": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
72
|
+
"IndexedDbVaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
73
|
+
"KeyOf": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
74
|
+
"MetricsEvent": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
75
|
+
"MigrationContext": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
76
|
+
"MigrationFn": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
77
|
+
"Observer": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
78
|
+
"RecordOf": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
79
|
+
"RecordValidator": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
80
|
+
"SchemaEntry": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
81
|
+
"TableBuilder": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
82
|
+
"TableValidators": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
83
|
+
"TransactionContext": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
84
|
+
"TtlMs": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
85
|
+
"Unsubscribe": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
86
|
+
"VaultKey": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
87
|
+
"VaultLogger": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
|
|
88
|
+
"VaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IndexedDbVaultStore,\n KeyOf,\n MetricsEvent,\n MigrationContext,\n MigrationFn,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableBuilder,\n TableValidators,\n TransactionContext,\n TtlMs,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';"
|
|
89
|
+
}
|
|
90
|
+
}
|