Publish
Rung five: the hub pattern — how a generator with dozens of model
families stays sane. Each destination is a complete form in its own module (S3 even exports
a standalone s3Form); the hub declares the discriminator as an ordinary field and branch('destination', pairs) dispatches on it, with the option list built from the modules, so adding a destination is one import plus one pair. Each arm
contributes that destination's fields — the state IS a discriminated union, and the panel below narrows it with a
plain if. The shared retries key is scoped per destination:
webhook and email each remember their own.
destination
region
storageClass
The union, narrowed with a plain if
Delivering to
(bucket unset) in us-east-1 as standard. — `s.retries` does not compile here.Intent — note retries@email vs retries@webhook
{}view source — publish-form.ts (29 lines)
import { branch, defineGraph } from '$lib/index.js';
import { enumOf } from '$lib/defs/index.js';
import { s3Graph, s3Meta } from './s3.js';
import { emailGraph, emailMeta } from './email.js';
import { webhookGraph, webhookMeta } from './webhook.js';
// The HUB: separately-defined destination graphs tied together by one
// discriminator FIELD, declared like any other field and dispatched by a
// keyed branch. The member keys type the arms, so
// `Extract<PublishState, { destination: 's3' }>` is exactly the s3 shape.
const DESTINATIONS = [s3Meta, emailMeta, webhookMeta];
const DESTINATION = enumOf({
options: DESTINATIONS.map((d) => ({ value: d.key, label: d.label })),
default: 's3',
});
export const publishForm = defineGraph()
.field('destination', DESTINATION)
.use(
branch('destination', [
[['s3'], s3Graph],
[['email'], emailGraph],
[['webhook'], webhookGraph],
] as const)
);
export type PublishState = ReturnType<typeof publishForm.resolve>;view source — s3.ts (37 lines)
import { z } from 'zod';
import { defineGraph } from '$lib/index.js';
import { enumOf } from '$lib/defs/index.js';
// One DESTINATION: a complete graph in its own module. Each field is one
// definition; the availability rule is one gate declaration whose condition
// reads the field above it.
export const s3Graph = defineGraph()
.field('bucket', {
input: z.string().optional(),
output: z.string().regex(/^[a-z0-9.-]{3,63}$/, 'Lowercase letters, digits, dots, dashes'),
default: '',
})
.field('region', enumOf({
options: [
{ value: 'us-east-1', label: 'us-east-1' },
{ value: 'eu-central-1', label: 'eu-central-1' },
{ value: 'ap-northeast-1', label: 'ap-northeast-1' },
],
default: 'us-east-1',
}))
.field('storageClass', (c) =>
enumOf({
options: [
{ value: 'standard', label: 'Standard' },
{ value: 'glacier', label: 'Glacier' },
],
default: 'standard',
gate: { glacier: c.region === 'ap-northeast-1' && 'class_unavailable_in_region' },
})
);
export const s3Meta = { key: 's3', label: 'S3' } as const;
// A destination is mountable ALONE — the graph and the standalone form are
// the same code. The hub is one consumer of it, not its owner.
export const s3Form = s3Graph;view source — email.ts (34 lines)
import { z } from 'zod';
import { defineGraph } from '$lib/index.js';
import { boolOf, enumOf, slider } from '$lib/defs/index.js';
export const emailGraph = defineGraph()
.field('recipients', {
input: z.string().optional(),
output: z
.string()
.min(1, 'At least one recipient')
.refine(
(s) => s.split(',').every((part) => z.string().email().safeParse(part.trim()).success),
'Comma-separated email addresses'
),
default: '',
})
.field('digest', boolOf())
.field('digestFrequency', (c) =>
c.digest
? enumOf({
options: [
{ value: 'immediate', label: 'Immediate' },
{ value: 'daily', label: 'Daily digest' },
{ value: 'weekly', label: 'Weekly digest' },
],
default: 'daily',
})
: null
)
// Same `retries` key as the webhook destination — scope keeps each
// destination's remembered value separate.
.field('retries', { ...slider({ min: 0, max: 10, default: 3 }), scope: 'email' });
export const emailMeta = { key: 'email', label: 'Email' } as const;view source — webhook.ts (25 lines)
import { z } from 'zod';
import { defineGraph } from '$lib/index.js';
import { enumOf, slider } from '$lib/defs/index.js';
export const webhookGraph = defineGraph()
.field('url', {
input: z.string().optional(),
output: z.string().url('A full https:// URL'),
default: '',
})
.field('secret', {
input: z.string().optional(),
output: z.string().min(8, 'Signing secret needs 8+ characters'),
default: '',
})
.field('method', enumOf({
options: [
{ value: 'POST', label: 'POST' },
{ value: 'PUT', label: 'PUT' },
],
default: 'POST',
}))
.field('retries', { ...slider({ min: 0, max: 10, default: 3 }), scope: 'webhook' });
export const webhookMeta = { key: 'webhook', label: 'Webhook' } as const;