@homeflare/alchemy 0.59.0 → 0.61.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.
@@ -1,128 +0,0 @@
1
- /**
2
- * `guardDuplicateIds` driven through a REAL `Alchemy.Stack(...)` body with real `Resource()`
3
- * classes — the same shape `litellm/fake-stack.ts` and `github/repo-apply-order-fake-stack.ts`
4
- * use for the same reason their own headers give: a check built only against this module's own
5
- * logic (duplicate-id.test.ts) proves the registry works, not that wiring it onto a resource
6
- * class via a `Proxy` actually intercepts the call Alchemy itself makes when a stack body
7
- * `yield*`s it.
8
- *
9
- * ⚠️ `dup-experiment.ts` (this PR's own throwaway script, not checked in) first measured the
10
- * UNGUARDED failure mode this guards against: two different `Resource()` classes sharing a
11
- * logical id compile a stack whose only entry at that fqn is the FIRST type, with no trace the
12
- * second one ever ran. These tests are the regression guard for exactly that, once wrapped.
13
- */
14
- import { describe, expect, test } from 'bun:test';
15
- import * as Alchemy from 'alchemy';
16
- import { push as pushNamespace } from 'alchemy/Namespace';
17
- import type { Resource, ResourceLike } from 'alchemy/Resource';
18
- import * as Effect from 'effect/Effect';
19
- import * as Layer from 'effect/Layer';
20
- import { guardDuplicateIds } from './guard.ts';
21
-
22
- interface Props {
23
- readonly value: number;
24
- }
25
- interface TestResource extends Resource<string, Props, object> {}
26
-
27
- /** A plain `Resource()` class, wrapped, for a given vendor-style type string. */
28
- const guarded = (type: string) => guardDuplicateIds(Alchemy.Resource<TestResource>(type));
29
-
30
- type StackBody = Effect.Effect<unknown, unknown, unknown>;
31
-
32
- /** Same cast-at-the-typed-boundary shape as `litellm/fake-stack.ts`; see that file's own header. */
33
- const runStack = (body: StackBody) => {
34
- const state = Alchemy.inMemoryState();
35
- const stack = Alchemy.Stack as unknown as (
36
- stackName: string,
37
- options: { providers: Layer.Layer<never>; state: typeof state },
38
- body: StackBody,
39
- ) => Effect.Effect<
40
- { readonly resources: Readonly<Record<string, ResourceLike>> },
41
- unknown,
42
- never
43
- >;
44
- return Effect.runPromiseExit(
45
- stack('GuardTestStack', { providers: Layer.empty, state }, body).pipe(
46
- Effect.provideService(Alchemy.Stage, 'test'),
47
- Effect.scoped,
48
- ),
49
- );
50
- };
51
-
52
- describe('guardDuplicateIds, through a real stack', () => {
53
- test('distinct ids both register', async () => {
54
- const A = guarded('Test.A');
55
- const B = guarded('Test.B');
56
- const exit = await runStack(
57
- Effect.gen(function* () {
58
- yield* A('one', { value: 1 });
59
- yield* B('two', { value: 2 });
60
- }),
61
- );
62
- expect(exit._tag).toBe('Success');
63
- if (exit._tag !== 'Success') return;
64
- expect(Object.keys(exit.value.resources).sort()).toEqual(['one', 'two']);
65
- });
66
-
67
- test('the same guarded type re-registering its own id stays a noop, not a death', async () => {
68
- const A = guarded('Test.A');
69
- const exit = await runStack(
70
- Effect.gen(function* () {
71
- yield* A('shared', { value: 1 });
72
- yield* A('shared', { value: 1 });
73
- }),
74
- );
75
- expect(exit._tag).toBe('Success');
76
- });
77
-
78
- test('two different guarded types sharing an id die naming both, before either resource "wins"', async () => {
79
- const Retirement = guarded('Nix.JobRetirement');
80
- const Caddy = guarded('Caddy.Config');
81
- const exit = await runStack(
82
- Effect.gen(function* () {
83
- yield* Retirement('caddy', { value: 1 });
84
- yield* Caddy('caddy', { value: 2 });
85
- }),
86
- );
87
- expect(exit._tag).toBe('Failure');
88
- if (exit._tag !== 'Failure') return;
89
- const reasons = (exit.cause as { reasons: readonly { _tag: string; defect?: unknown }[] })
90
- .reasons;
91
- const reason = reasons[0];
92
- expect(reason === undefined ? undefined : reason._tag).toBe('Die');
93
- const defect = reason === undefined ? undefined : (reason.defect as Error | undefined);
94
- expect(defect === undefined ? '' : String(defect.message)).toContain(
95
- 'REFUSING: duplicate resource id "caddy" in scope (root): ' +
96
- 'first declared by Nix.JobRetirement, again by Caddy.Config',
97
- );
98
- });
99
-
100
- test('one guarded, one unguarded type sharing an id still dies (the guarded side still claims it)', async () => {
101
- const Guarded = guarded('Test.Guarded');
102
- const Unguarded = Alchemy.Resource<TestResource>('Test.Unguarded');
103
- const exit = await runStack(
104
- Effect.gen(function* () {
105
- yield* Guarded('shared', { value: 1 });
106
- yield* Unguarded('shared', { value: 2 });
107
- }),
108
- );
109
- // The unguarded side never calls `claimResourceId`, so Alchemy's own idempotent-registration
110
- // branch runs instead (Resource.ts) and this SUCCEEDS as a silent noop — exactly the gap this
111
- // PR documents as out of scope for a kit-only fix (docs/duplicate-resource-id.md). Asserting
112
- // the current behaviour, not endorsing it: `guardDuplicateIds` only protects pairs where BOTH
113
- // sides are wrapped.
114
- expect(exit._tag).toBe('Success');
115
- });
116
-
117
- test('the same id in a nested namespace is a different scope', async () => {
118
- const Retirement = guarded('Nix.JobRetirement');
119
- const Caddy = guarded('Caddy.Config');
120
- const exit = await runStack(
121
- Effect.gen(function* () {
122
- yield* Retirement('caddy', { value: 1 });
123
- yield* pushNamespace('child', Caddy('caddy', { value: 2 }));
124
- }),
125
- );
126
- expect(exit._tag).toBe('Success');
127
- });
128
- });
@@ -1,49 +0,0 @@
1
- /**
2
- * `guardDuplicateIds` — wrap a Resource class so every `("id", props)` call it makes first
3
- * claims that id through `duplicate-id.ts`'s per-stack registry, naming both resource types if
4
- * a second, different-typed resource already claimed it. See duplicate-id.ts's header for why
5
- * the check has to live HERE (at the moment of registration) rather than after the stack body
6
- * has finished running.
7
- *
8
- * Usage, in a resource's own file, right where it's exported today:
9
- *
10
- * ```ts
11
- * export const CaddyConfig = guardDuplicateIds(Resource<CaddyConfig>('Caddy.Config', { … }));
12
- * ```
13
- *
14
- * ★ A `Proxy` around the REAL class, not a reimplementation. `Resource()`'s return value is a
15
- * callable object wearing an `Effectable.Prototype` (`Effect.isEffect(MyResource) === true`,
16
- * for the bare-tag platform-resource pattern) plus `.ref`, `.Type`, `.Provider`, `.Self`,
17
- * `.Aliases` — reconstructing that shape risks silently dropping one of them. A `Proxy` whose
18
- * `get` trap is the default (`Reflect.get`) forwards every property read untouched; only the
19
- * `apply` trap — the `MyResource("id", props)` call form — is new behaviour.
20
- * ⚠️ THE "REGISTER METHODS" OVERLOAD IS DELIBERATELY LEFT ALONE. `ResourceClass` is also callable
21
- * with a single object argument to attach methods (`Resource()`'s own `typeof args[0] ===
22
- * 'object' ? Object.assign(...) : constructor(...)` branch) — that call mutates and returns the
23
- * SAME class, carries no logical id, and has nothing to claim. Only a string first argument
24
- * (the `(id, props)` form) goes through `claimResourceId`.
25
- */
26
- import type { ResourceClass, ResourceLike } from 'alchemy/Resource';
27
- import * as Effect from 'effect/Effect';
28
- import { claimResourceId } from './duplicate-id.ts';
29
-
30
- export const guardDuplicateIds = <R extends ResourceLike>(
31
- resourceClass: ResourceClass<R>,
32
- ): ResourceClass<R> =>
33
- new Proxy(resourceClass, {
34
- apply(target, thisArg, args: unknown[]) {
35
- const id = args[0];
36
- if (typeof id !== 'string') {
37
- // The "attach methods" overload — not a registration, nothing to claim.
38
- return Reflect.apply(target as (...a: unknown[]) => unknown, thisArg, args);
39
- }
40
- return Effect.gen(function* () {
41
- yield* claimResourceId(resourceClass.Type, id);
42
- return yield* Reflect.apply(
43
- target as (...a: unknown[]) => unknown,
44
- thisArg,
45
- args,
46
- ) as Effect.Effect<R>;
47
- });
48
- },
49
- }) as ResourceClass<R>;