Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/tsgo-0-63-brands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@systemfsoftware/effect-atom-react": patch
"@systemfsoftware/effect-atom": patch
"@systemfsoftware/storybook-gherkin": minor
---

- `DuplicateCapture` carries its capture name in `capture` and `OutlineDuplicateRowName` carries its row name in `rowName`, where both carried it in `name`. A `name` payload field shadows the error's own `name`, so logs showed the field value instead of the error type. Read `capture` or `rowName`.
- Every `Schema.Class`, `TaggedClass` and `TaggedError` the packages export is branded, so two classes with the same shape are no longer assignable to each other.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const signed = (name: string, suffix: string) =>
})

const flagged = (suffix: string) =>
Effect.flatMap(signed('Ray', suffix), (message) => Effect.as(moderateOnPage({ message, event: 'Flag' }), message))
Effect.tap(signed('Ray', suffix), (message) => moderateOnPage({ message, event: 'Flag' }))

Feature('Flagging and vouching for guestbook entries from the page', { timeout: CHROMIUM_TIMEOUT_MS })
.withLayer(ChromiumLive)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Feature('Signing the guestbook from its page', { timeout: CHROMIUM_TIMEOUT_MS })
Gherkin.Do.pipe(
When('a guest signs the guestbook')(
'message',
() => Effect.flatMap(uniqueMessage, (message) => Effect.as(signOnPage({ name: 'Ada', message }), message)),
() => Effect.tap(uniqueMessage, (message) => signOnPage({ name: 'Ada', message })),
),
When('another visitor opens the guestbook in their own browser')('listed', () => listedOnPage),
Then('the entry is in the list')((s, expect) => expect(s.listed).toContain(`Ada ${s.message}`)),
Expand Down
16 changes: 11 additions & 5 deletions apps/site/src/api/check-health.workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,27 @@ import * as S from 'effect/Schema'

const CheckHealthTypeId: unique symbol = Symbol()

export class ProbeAnswered extends S.TaggedClass<ProbeAnswered>()('ProbeAnswered', {}) {}
export class ProbeAnswered
extends S.TaggedClass<ProbeAnswered, { readonly brand: unique symbol }>()('ProbeAnswered', {})
{}

export class ProbeUnanswered extends S.TaggedClass<ProbeUnanswered>()('ProbeUnanswered', {}) {}
export class ProbeUnanswered
extends S.TaggedClass<ProbeUnanswered, { readonly brand: unique symbol }>()('ProbeUnanswered', {})
{}

export class CheckHealth extends S.Class<CheckHealth>('CheckHealth')({
export class CheckHealth extends S.Class<CheckHealth, { readonly brand: unique symbol }>('CheckHealth')({
probe: S.Union([ProbeAnswered, ProbeUnanswered]),
}) {
static readonly [Workflow.InstrumentationBrand]: Record<never, never> = {}
}

class Healthy extends S.TaggedClass<Healthy>()('Healthy', {}) {
class Healthy extends S.TaggedClass<Healthy, { readonly brand: unique symbol }>()('Healthy', {}) {
readonly [CheckHealthTypeId] = CheckHealthTypeId
}

export class DatabaseUnreachable extends S.TaggedError<DatabaseUnreachable>()('DatabaseUnreachable', {}) {
export class DatabaseUnreachable
extends S.TaggedError<DatabaseUnreachable, { readonly brand: unique symbol }>()('DatabaseUnreachable', {})
{
override get message(): string {
return 'The database did not answer the health probe.'
}
Expand Down
38 changes: 24 additions & 14 deletions apps/site/src/features/guestbook/guestbook.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,40 +32,50 @@ export type GuestbookEntry = S.Schema.Type<typeof GuestbookEntry>
export const GuestbookEntries = S.Array(GuestbookEntry)
export type GuestbookEntries = S.Schema.Type<typeof GuestbookEntries>

export class EntryNotFound extends S.TaggedError<EntryNotFound>()('EntryNotFound', { id: EntryId }) {
export class EntryNotFound
extends S.TaggedError<EntryNotFound, { readonly brand: unique symbol }>()('EntryNotFound', { id: EntryId })
{
override get message(): string {
return `There is no entry ${this.id}.`
}
}

export class StoredStateInvalid extends S.TaggedError<StoredStateInvalid>()('StoredStateInvalid', {
detail: S.String,
}) {
export class StoredStateInvalid
extends S.TaggedError<StoredStateInvalid, { readonly brand: unique symbol }>()('StoredStateInvalid', {
detail: S.String,
})
{
override get message(): string {
return `This entry's stored state is not one the lifecycle knows: ${this.detail}`
}
}

export class TransitionConflict extends S.TaggedError<TransitionConflict>()('TransitionConflict', {
id: EntryId,
from: EntryState,
}) {
export class TransitionConflict
extends S.TaggedError<TransitionConflict, { readonly brand: unique symbol }>()('TransitionConflict', {
id: EntryId,
from: EntryState,
})
{
override get message(): string {
return `Entry ${this.id} changed after it was read as ${this.from}; reload and try again.`
}
}

export class GuestbookUnavailable extends S.TaggedError<GuestbookUnavailable>()('GuestbookUnavailable', {
detail: S.String,
}) {
export class GuestbookUnavailable
extends S.TaggedError<GuestbookUnavailable, { readonly brand: unique symbol }>()('GuestbookUnavailable', {
detail: S.String,
})
{
override get message(): string {
return `The guestbook's database failed: ${this.detail}`
}
}

export class GuestbookRowInvalid extends S.TaggedError<GuestbookRowInvalid>()('GuestbookRowInvalid', {
detail: S.String,
}) {
export class GuestbookRowInvalid
extends S.TaggedError<GuestbookRowInvalid, { readonly brand: unique symbol }>()('GuestbookRowInvalid', {
detail: S.String,
})
{
override get message(): string {
return `A guestbook row did not decode: ${this.detail}`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,29 @@ import { EntryState, LifecycleEvent, StoredEntryState } from './guestbook.schema

const ModerateGuestbookEntryTypeId: unique symbol = Symbol()

export class ModerateGuestbookEntry extends S.Class<ModerateGuestbookEntry>('ModerateGuestbookEntry')({
state: StoredEntryState,
event: LifecycleEvent,
}) {
export class ModerateGuestbookEntry
extends S.Class<ModerateGuestbookEntry, { readonly brand: unique symbol }>('ModerateGuestbookEntry')({
state: StoredEntryState,
event: LifecycleEvent,
})
{
static readonly [Workflow.InstrumentationBrand]: Record<never, never> = {}
}

export class EntryMoved extends S.TaggedClass<EntryMoved>()('EntryMoved', {
export class EntryMoved extends S.TaggedClass<EntryMoved, { readonly brand: unique symbol }>()('EntryMoved', {
from: EntryState,
to: EntryState,
actions: S.Int,
}) {
readonly [ModerateGuestbookEntryTypeId] = ModerateGuestbookEntryTypeId
}

export class IllegalTransition extends S.TaggedError<IllegalTransition>()('IllegalTransition', {
state: EntryState,
event: LifecycleEvent,
}) {
export class IllegalTransition
extends S.TaggedError<IllegalTransition, { readonly brand: unique symbol }>()('IllegalTransition', {
state: EntryState,
event: LifecycleEvent,
})
{
override get message(): string {
return `An entry that is ${this.state} can't take ${this.event}.`
}
Expand Down
16 changes: 10 additions & 6 deletions apps/site/src/features/guestbook/sign-guestbook.workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,43 @@ import { GuestMessage, GuestName } from './guestbook.schema.ts'

const SignGuestbookTypeId: unique symbol = Symbol()

export class SignGuestbook extends S.Class<SignGuestbook>('SignGuestbook')({
export class SignGuestbook extends S.Class<SignGuestbook, { readonly brand: unique symbol }>('SignGuestbook')({
name: S.String,
message: S.String,
}) {
static readonly [Workflow.InstrumentationBrand]: Record<never, never> = {}
}

export class EntryAccepted extends S.TaggedClass<EntryAccepted>()('EntryAccepted', {
export class EntryAccepted extends S.TaggedClass<EntryAccepted, { readonly brand: unique symbol }>()('EntryAccepted', {
guest: GuestName,
message: GuestMessage,
}) {
readonly [SignGuestbookTypeId] = SignGuestbookTypeId
}

export class NameMissing extends S.TaggedError<NameMissing>()('NameMissing', {}) {
export class NameMissing extends S.TaggedError<NameMissing, { readonly brand: unique symbol }>()('NameMissing', {}) {
override get message(): string {
return 'Write your name.'
}
}

export class NameTooLong extends S.TaggedError<NameTooLong>()('NameTooLong', {}) {
export class NameTooLong extends S.TaggedError<NameTooLong, { readonly brand: unique symbol }>()('NameTooLong', {}) {
override get message(): string {
return 'Your name is too long for the guestbook.'
}
}

export class MessageMissing extends S.TaggedError<MessageMissing>()('MessageMissing', {}) {
export class MessageMissing
extends S.TaggedError<MessageMissing, { readonly brand: unique symbol }>()('MessageMissing', {})
{
override get message(): string {
return 'Write a message.'
}
}

export class MessageTooLong extends S.TaggedError<MessageTooLong>()('MessageTooLong', {}) {
export class MessageTooLong
extends S.TaggedError<MessageTooLong, { readonly brand: unique symbol }>()('MessageTooLong', {})
{
override get message(): string {
return 'Your message is too long for the guestbook.'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,34 @@ const ResolveAtomSuspenseDecisionTypeId: unique symbol = Symbol.for(
)
type ResolveAtomSuspenseDecisionTypeId = typeof ResolveAtomSuspenseDecisionTypeId

export class AtomSuspended extends Schema.TaggedClass<AtomSuspended>()('AtomSuspended', {}) {
export class AtomSuspended
extends Schema.TaggedClass<AtomSuspended, { readonly brand: unique symbol }>()('AtomSuspended', {})
{
readonly [ResolveAtomSuspenseDecisionTypeId] = ResolveAtomSuspenseDecisionTypeId
}

export class AtomReady extends Schema.TaggedClass<AtomReady>()('AtomReady', {}) {
export class AtomReady extends Schema.TaggedClass<AtomReady, { readonly brand: unique symbol }>()('AtomReady', {}) {
readonly [ResolveAtomSuspenseDecisionTypeId] = ResolveAtomSuspenseDecisionTypeId
}

export class AtomRefused extends Schema.TaggedClass<AtomRefused>()('AtomRefused', {}) {
export class AtomRefused
extends Schema.TaggedClass<AtomRefused, { readonly brand: unique symbol }>()('AtomRefused', {})
{
readonly [ResolveAtomSuspenseDecisionTypeId] = ResolveAtomSuspenseDecisionTypeId
}

export const ResolveAtomSuspenseDecision = Schema.Union([AtomSuspended, AtomReady, AtomRefused])
export type ResolveAtomSuspenseDecision = typeof ResolveAtomSuspenseDecision.Type

export class ResolveAtomSuspense extends Schema.TaggedClass<ResolveAtomSuspense>()('ResolveAtomSuspense', {
initial: Schema.Boolean,
waiting: Schema.Boolean,
suspendOnWaiting: Schema.Boolean,
failure: Schema.Boolean,
includeFailure: Schema.Boolean,
}) {
export class ResolveAtomSuspense
extends Schema.TaggedClass<ResolveAtomSuspense, { readonly brand: unique symbol }>()('ResolveAtomSuspense', {
initial: Schema.Boolean,
waiting: Schema.Boolean,
suspendOnWaiting: Schema.Boolean,
failure: Schema.Boolean,
includeFailure: Schema.Boolean,
})
{
static readonly [Workflow.InstrumentationBrand] = {} as const
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as Schema from 'effect/Schema'

export class Unavailable extends Schema.TaggedError<Unavailable>()('Unavailable', {}) {}
export class Unavailable
extends Schema.TaggedError<Unavailable, { readonly brand: unique symbol }>()('Unavailable', {})
{}
10 changes: 6 additions & 4 deletions packages/atom/effect-atom/src/advance-batch-phase.workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ const BatchStepDecisionTypeId: unique symbol = Symbol.for(
)
type BatchStepDecisionTypeId = typeof BatchStepDecisionTypeId

export class RebuildAndNotify extends Schema.TaggedClass<RebuildAndNotify>()('RebuildAndNotify', {}) {
export class RebuildAndNotify
extends Schema.TaggedClass<RebuildAndNotify, { readonly brand: unique symbol }>()('RebuildAndNotify', {})
{
readonly [BatchStepDecisionTypeId] = BatchStepDecisionTypeId
}

export class ResetBatch extends Schema.TaggedClass<ResetBatch>()('ResetBatch', {}) {
export class ResetBatch extends Schema.TaggedClass<ResetBatch, { readonly brand: unique symbol }>()('ResetBatch', {}) {
readonly [BatchStepDecisionTypeId] = BatchStepDecisionTypeId
}

export class StayNested extends Schema.TaggedClass<StayNested>()('StayNested', {}) {
export class StayNested extends Schema.TaggedClass<StayNested, { readonly brand: unique symbol }>()('StayNested', {}) {
readonly [BatchStepDecisionTypeId] = BatchStepDecisionTypeId
}

Expand All @@ -25,7 +27,7 @@ export type BatchStepDecision = typeof BatchStepDecision.Type
export const BatchEvent = Schema.Literals(['commit-request', 'finish-request'])
export type BatchEvent = Schema.Schema.Type<typeof BatchEvent>

export class BatchStep extends Schema.TaggedClass<BatchStep>()('BatchStep', {
export class BatchStep extends Schema.TaggedClass<BatchStep, { readonly brand: unique symbol }>()('BatchStep', {
depth: Schema.Int,
event: BatchEvent,
}) {
Expand Down
32 changes: 21 additions & 11 deletions packages/atom/effect-atom/src/advance-node-state.workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,31 @@ const NodeStateDecisionTypeId: unique symbol = Symbol.for(
)
type NodeStateDecisionTypeId = typeof NodeStateDecisionTypeId

export class BecomesValid extends Schema.TaggedClass<BecomesValid>()('BecomesValid', {}) {
export class BecomesValid
extends Schema.TaggedClass<BecomesValid, { readonly brand: unique symbol }>()('BecomesValid', {})
{
readonly [NodeStateDecisionTypeId] = NodeStateDecisionTypeId
}

export class BecomesStale extends Schema.TaggedClass<BecomesStale>()('BecomesStale', {}) {
export class BecomesStale
extends Schema.TaggedClass<BecomesStale, { readonly brand: unique symbol }>()('BecomesStale', {})
{
readonly [NodeStateDecisionTypeId] = NodeStateDecisionTypeId
}

export class BecomesChecking extends Schema.TaggedClass<BecomesChecking>()('BecomesChecking', {}) {
export class BecomesChecking
extends Schema.TaggedClass<BecomesChecking, { readonly brand: unique symbol }>()('BecomesChecking', {})
{
readonly [NodeStateDecisionTypeId] = NodeStateDecisionTypeId
}

export class TakeBuiltValue extends Schema.TaggedClass<TakeBuiltValue>()('TakeBuiltValue', {}) {
export class TakeBuiltValue
extends Schema.TaggedClass<TakeBuiltValue, { readonly brand: unique symbol }>()('TakeBuiltValue', {})
{
readonly [NodeStateDecisionTypeId] = NodeStateDecisionTypeId
}

export class KeepsPhase extends Schema.TaggedClass<KeepsPhase>()('KeepsPhase', {}) {
export class KeepsPhase extends Schema.TaggedClass<KeepsPhase, { readonly brand: unique symbol }>()('KeepsPhase', {}) {
readonly [NodeStateDecisionTypeId] = NodeStateDecisionTypeId
}

Expand All @@ -46,12 +54,14 @@ export const NodeStateEvent = Schema.Literals([
])
export type NodeStateEvent = Schema.Schema.Type<typeof NodeStateEvent>

export class AdvanceNodeState extends Schema.TaggedClass<AdvanceNodeState>()('AdvanceNodeState', {
phase: NodePhase,
event: NodeStateEvent,
invalidatedDuringBuild: Schema.Boolean,
preserveInitialValueOnBuild: Schema.Boolean,
}) {
export class AdvanceNodeState
extends Schema.TaggedClass<AdvanceNodeState, { readonly brand: unique symbol }>()('AdvanceNodeState', {
phase: NodePhase,
event: NodeStateEvent,
invalidatedDuringBuild: Schema.Boolean,
preserveInitialValueOnBuild: Schema.Boolean,
})
{
static readonly [Workflow.InstrumentationBrand] = {} as const
}

Expand Down
18 changes: 13 additions & 5 deletions packages/atom/effect-atom/src/judge-async-read.workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,27 @@ const AsyncReadDecisionTypeId: unique symbol = Symbol.for(
)
type AsyncReadDecisionTypeId = typeof AsyncReadDecisionTypeId

export class SuspendImmediately extends Schema.TaggedClass<SuspendImmediately>()('SuspendImmediately', {}) {
export class SuspendImmediately
extends Schema.TaggedClass<SuspendImmediately, { readonly brand: unique symbol }>()('SuspendImmediately', {})
{
readonly [AsyncReadDecisionTypeId] = AsyncReadDecisionTypeId
}

export class AwaitNextResult extends Schema.TaggedClass<AwaitNextResult>()('AwaitNextResult', {}) {
export class AwaitNextResult
extends Schema.TaggedClass<AwaitNextResult, { readonly brand: unique symbol }>()('AwaitNextResult', {})
{
readonly [AsyncReadDecisionTypeId] = AsyncReadDecisionTypeId
}

export class KeepAwaiting extends Schema.TaggedClass<KeepAwaiting>()('KeepAwaiting', {}) {
export class KeepAwaiting
extends Schema.TaggedClass<KeepAwaiting, { readonly brand: unique symbol }>()('KeepAwaiting', {})
{
readonly [AsyncReadDecisionTypeId] = AsyncReadDecisionTypeId
}

export class ResolveWithResult extends Schema.TaggedClass<ResolveWithResult>()('ResolveWithResult', {}) {
export class ResolveWithResult
extends Schema.TaggedClass<ResolveWithResult, { readonly brand: unique symbol }>()('ResolveWithResult', {})
{
readonly [AsyncReadDecisionTypeId] = AsyncReadDecisionTypeId
}

Expand All @@ -37,7 +45,7 @@ export type AsyncReadStage = Schema.Schema.Type<typeof AsyncReadStage>
export const AsyncResultPhase = Schema.Literals(['initial', 'success', 'failure'])
export type AsyncResultPhase = Schema.Schema.Type<typeof AsyncResultPhase>

export class AsyncRead extends Schema.TaggedClass<AsyncRead>()('AsyncRead', {
export class AsyncRead extends Schema.TaggedClass<AsyncRead, { readonly brand: unique symbol }>()('AsyncRead', {
stage: AsyncReadStage,
phase: AsyncResultPhase,
waiting: Schema.Boolean,
Expand Down
Loading
Loading