@distilled.cloud/fly-io 1.0.0-rc.11 → 1.0.0-rc.13
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 +34 -0
- package/lib/protocol.d.ts +2 -2
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +39 -24
- package/lib/protocol.js.map +1 -1
- package/lib/protocol.test.d.ts +2 -0
- package/lib/protocol.test.d.ts.map +1 -0
- package/lib/protocol.test.js +125 -0
- package/lib/protocol.test.js.map +1 -0
- package/lib/response-validation.test.d.ts +4 -0
- package/lib/response-validation.test.d.ts.map +1 -0
- package/lib/response-validation.test.js +32 -0
- package/lib/response-validation.test.js.map +1 -0
- package/lib/services/machines.d.ts +206 -58
- package/lib/services/machines.d.ts.map +1 -1
- package/lib/services/machines.js +200 -50
- package/lib/services/machines.js.map +1 -1
- package/lib/sprites-mint.test.d.ts +2 -0
- package/lib/sprites-mint.test.d.ts.map +1 -0
- package/lib/sprites-mint.test.js +170 -0
- package/lib/sprites-mint.test.js.map +1 -0
- package/package.json +4 -4
- package/src/protocol.test.ts +234 -0
- package/src/protocol.ts +81 -42
- package/src/response-validation.test.ts +46 -0
- package/src/services/machines.ts +479 -117
- package/src/sprites-mint.test.ts +270 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import * as API from "@distilled.cloud/core/api";
|
|
3
|
+
import * as Cause from "effect/Cause";
|
|
4
|
+
import * as Deferred from "effect/Deferred";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Exit from "effect/Exit";
|
|
7
|
+
import * as Fiber from "effect/Fiber";
|
|
8
|
+
import * as Redacted from "effect/Redacted";
|
|
9
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
10
|
+
import type * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
11
|
+
import * as HttpClientResponse from "effect/http/HttpClientResponse";
|
|
12
|
+
import { Credentials } from "./credentials.ts";
|
|
13
|
+
import { SpritesProtocol } from "./protocol.ts";
|
|
14
|
+
import { ListSpritesRequest } from "./services/sprites.ts";
|
|
15
|
+
|
|
16
|
+
const mintFixture = (apiKey: string) =>
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const protocol = yield* API.Protocol;
|
|
19
|
+
const attempts = yield* Effect.forEach([0, 1], () =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
return {
|
|
22
|
+
started: yield* Deferred.make<void>(),
|
|
23
|
+
response: yield* Deferred.make<{ status: number; body: unknown }>(),
|
|
24
|
+
};
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
const discoveries: HttpClientRequest.HttpClientRequest[] = [];
|
|
28
|
+
const mints: HttpClientRequest.HttpClientRequest[] = [];
|
|
29
|
+
let credentialReads = 0;
|
|
30
|
+
const client = HttpClient.make((request) =>
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
if (request.url === "https://fly.test/v1/tokens/current") {
|
|
33
|
+
discoveries.push(request);
|
|
34
|
+
return HttpClientResponse.fromWeb(
|
|
35
|
+
request,
|
|
36
|
+
Response.json({ tokens: [{ org_slug: "mint-test" }] }),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
request.method !== "POST" ||
|
|
41
|
+
request.url !==
|
|
42
|
+
"https://api.sprites.dev/v1/organizations/mint-test/tokens"
|
|
43
|
+
) {
|
|
44
|
+
return yield* Effect.die("Unexpected test transport request");
|
|
45
|
+
}
|
|
46
|
+
const attempt = attempts[mints.length];
|
|
47
|
+
mints.push(request);
|
|
48
|
+
if (!attempt) return yield* Effect.die("Unexpected mint attempt");
|
|
49
|
+
yield* Deferred.succeed(attempt.started, undefined);
|
|
50
|
+
const { status, body } = yield* Deferred.await(attempt.response);
|
|
51
|
+
return HttpClientResponse.fromWeb(
|
|
52
|
+
request,
|
|
53
|
+
Response.json(body, { status }),
|
|
54
|
+
);
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
const encode = protocol
|
|
58
|
+
.encode({
|
|
59
|
+
input: {},
|
|
60
|
+
inputAst: ListSpritesRequest.ast,
|
|
61
|
+
config: { input: ListSpritesRequest },
|
|
62
|
+
})
|
|
63
|
+
.pipe(
|
|
64
|
+
Effect.provideService(HttpClient.HttpClient, client),
|
|
65
|
+
Effect.provideService(
|
|
66
|
+
Credentials,
|
|
67
|
+
Effect.sync(() => {
|
|
68
|
+
credentialReads++;
|
|
69
|
+
return {
|
|
70
|
+
apiKey: Redacted.make(apiKey),
|
|
71
|
+
apiBaseUrl: "https://fly.test",
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
return {
|
|
77
|
+
encode,
|
|
78
|
+
discoveries,
|
|
79
|
+
mints,
|
|
80
|
+
credentialReads: () => credentialReads,
|
|
81
|
+
started: (attempt = 0) => Deferred.await(attempts[attempt]!.started),
|
|
82
|
+
respond: (body: unknown, status = 200, attempt = 0) =>
|
|
83
|
+
Deferred.succeed(attempts[attempt]!.response, { status, body }),
|
|
84
|
+
};
|
|
85
|
+
}).pipe(Effect.provide(SpritesProtocol));
|
|
86
|
+
|
|
87
|
+
describe("Sprites token mint coordination", () => {
|
|
88
|
+
test("shares concurrent minting and reuses the successful token without exposing it in request JSON", () =>
|
|
89
|
+
Effect.runPromise(
|
|
90
|
+
Effect.gen(function* () {
|
|
91
|
+
const fixture = yield* mintFixture("mint-concurrent-fly-secret");
|
|
92
|
+
const owner = yield* fixture.encode.pipe(
|
|
93
|
+
Effect.forkChild({ startImmediately: true }),
|
|
94
|
+
);
|
|
95
|
+
yield* fixture.started();
|
|
96
|
+
const waiters = yield* Effect.forEach(Array.from({ length: 20 }), () =>
|
|
97
|
+
fixture.encode.pipe(Effect.forkChild({ startImmediately: true })),
|
|
98
|
+
);
|
|
99
|
+
expect(fixture.credentialReads()).toBe(21);
|
|
100
|
+
expect(fixture.discoveries).toHaveLength(1);
|
|
101
|
+
expect(fixture.mints).toHaveLength(1);
|
|
102
|
+
yield* fixture.respond({ token: "mint-concurrent-sprites-secret" });
|
|
103
|
+
const requests = yield* Effect.forEach([owner, ...waiters], Fiber.join);
|
|
104
|
+
requests.push(yield* fixture.encode);
|
|
105
|
+
for (const request of requests) {
|
|
106
|
+
expect(request.url).toBe("https://api.sprites.dev/v1/sprites");
|
|
107
|
+
expect(request.headers.authorization).toBe(
|
|
108
|
+
"Bearer mint-concurrent-sprites-secret",
|
|
109
|
+
);
|
|
110
|
+
expect(JSON.stringify(request)).not.toContain(
|
|
111
|
+
"mint-concurrent-sprites-secret",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
expect(fixture.mints[0]!.headers.authorization).toBe(
|
|
115
|
+
"FlyV1 mint-concurrent-fly-secret",
|
|
116
|
+
);
|
|
117
|
+
for (const request of [...fixture.discoveries, ...fixture.mints]) {
|
|
118
|
+
expect(JSON.stringify(request)).not.toContain(
|
|
119
|
+
"mint-concurrent-fly-secret",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
expect(fixture.credentialReads()).toBe(22);
|
|
123
|
+
expect(fixture.discoveries).toHaveLength(1);
|
|
124
|
+
expect(fixture.mints).toHaveLength(1);
|
|
125
|
+
}).pipe(Effect.timeout("2 seconds")),
|
|
126
|
+
));
|
|
127
|
+
|
|
128
|
+
test("shares a failed mint and allows a subsequent successful retry", () =>
|
|
129
|
+
Effect.runPromise(
|
|
130
|
+
Effect.gen(function* () {
|
|
131
|
+
const fixture = yield* mintFixture("mint-retry-fly-secret");
|
|
132
|
+
const owner = yield* fixture.encode.pipe(
|
|
133
|
+
Effect.forkChild({ startImmediately: true }),
|
|
134
|
+
);
|
|
135
|
+
yield* fixture.started();
|
|
136
|
+
const waiter = yield* fixture.encode.pipe(
|
|
137
|
+
Effect.forkChild({ startImmediately: true }),
|
|
138
|
+
);
|
|
139
|
+
expect(fixture.credentialReads()).toBe(2);
|
|
140
|
+
expect(fixture.mints).toHaveLength(1);
|
|
141
|
+
yield* fixture.respond({ message: "synthetic mint failure" }, 500);
|
|
142
|
+
const exits = yield* Fiber.awaitAll([owner, waiter]);
|
|
143
|
+
for (const exit of exits) {
|
|
144
|
+
expect(Exit.isFailure(exit)).toBe(true);
|
|
145
|
+
if (Exit.isFailure(exit)) {
|
|
146
|
+
expect(Cause.pretty(exit.cause)).toContain(
|
|
147
|
+
"synthetic mint failure",
|
|
148
|
+
);
|
|
149
|
+
expect(Cause.pretty(exit.cause)).not.toContain(
|
|
150
|
+
"mint-retry-fly-secret",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
expect(exits[0]).toEqual(exits[1]);
|
|
155
|
+
const retry = yield* fixture.encode.pipe(
|
|
156
|
+
Effect.forkChild({ startImmediately: true }),
|
|
157
|
+
);
|
|
158
|
+
yield* fixture.started(1);
|
|
159
|
+
yield* fixture.respond({ token: "mint-retry-sprites-secret" }, 200, 1);
|
|
160
|
+
expect((yield* Fiber.join(retry)).headers.authorization).toBe(
|
|
161
|
+
"Bearer mint-retry-sprites-secret",
|
|
162
|
+
);
|
|
163
|
+
expect((yield* fixture.encode).headers.authorization).toBe(
|
|
164
|
+
"Bearer mint-retry-sprites-secret",
|
|
165
|
+
);
|
|
166
|
+
expect(fixture.discoveries).toHaveLength(2);
|
|
167
|
+
expect(fixture.mints).toHaveLength(2);
|
|
168
|
+
}).pipe(Effect.timeout("2 seconds")),
|
|
169
|
+
));
|
|
170
|
+
|
|
171
|
+
test("isolates pending and successful tokens for separate credentials", () =>
|
|
172
|
+
Effect.runPromise(
|
|
173
|
+
Effect.gen(function* () {
|
|
174
|
+
const first = yield* mintFixture("mint-isolation-first");
|
|
175
|
+
const second = yield* mintFixture("mint-isolation-second");
|
|
176
|
+
const firstFiber = yield* first.encode.pipe(
|
|
177
|
+
Effect.forkChild({ startImmediately: true }),
|
|
178
|
+
);
|
|
179
|
+
const secondFiber = yield* second.encode.pipe(
|
|
180
|
+
Effect.forkChild({ startImmediately: true }),
|
|
181
|
+
);
|
|
182
|
+
yield* first.started();
|
|
183
|
+
yield* second.started();
|
|
184
|
+
expect(first.mints[0]!.headers.authorization).toBe(
|
|
185
|
+
"FlyV1 mint-isolation-first",
|
|
186
|
+
);
|
|
187
|
+
expect(second.mints[0]!.headers.authorization).toBe(
|
|
188
|
+
"FlyV1 mint-isolation-second",
|
|
189
|
+
);
|
|
190
|
+
yield* second.respond({ token: "sprites-isolation-second" });
|
|
191
|
+
expect((yield* Fiber.join(secondFiber)).headers.authorization).toBe(
|
|
192
|
+
"Bearer sprites-isolation-second",
|
|
193
|
+
);
|
|
194
|
+
yield* first.respond({ token: "sprites-isolation-first" });
|
|
195
|
+
expect((yield* Fiber.join(firstFiber)).headers.authorization).toBe(
|
|
196
|
+
"Bearer sprites-isolation-first",
|
|
197
|
+
);
|
|
198
|
+
expect((yield* first.encode).headers.authorization).toBe(
|
|
199
|
+
"Bearer sprites-isolation-first",
|
|
200
|
+
);
|
|
201
|
+
expect((yield* second.encode).headers.authorization).toBe(
|
|
202
|
+
"Bearer sprites-isolation-second",
|
|
203
|
+
);
|
|
204
|
+
expect(first.mints).toHaveLength(1);
|
|
205
|
+
expect(second.mints).toHaveLength(1);
|
|
206
|
+
}).pipe(Effect.timeout("2 seconds")),
|
|
207
|
+
));
|
|
208
|
+
|
|
209
|
+
test("interrupting a waiter does not cancel or evict the active mint", () =>
|
|
210
|
+
Effect.runPromise(
|
|
211
|
+
Effect.gen(function* () {
|
|
212
|
+
const fixture = yield* mintFixture("mint-interrupted-waiter");
|
|
213
|
+
const owner = yield* fixture.encode.pipe(
|
|
214
|
+
Effect.forkChild({ startImmediately: true }),
|
|
215
|
+
);
|
|
216
|
+
yield* fixture.started();
|
|
217
|
+
const waiter = yield* fixture.encode.pipe(
|
|
218
|
+
Effect.forkChild({ startImmediately: true }),
|
|
219
|
+
);
|
|
220
|
+
expect(fixture.credentialReads()).toBe(2);
|
|
221
|
+
yield* Fiber.interrupt(waiter);
|
|
222
|
+
expect(Exit.hasInterrupts(yield* Fiber.await(waiter))).toBe(true);
|
|
223
|
+
const replacementWaiter = yield* fixture.encode.pipe(
|
|
224
|
+
Effect.forkChild({ startImmediately: true }),
|
|
225
|
+
);
|
|
226
|
+
expect(fixture.credentialReads()).toBe(3);
|
|
227
|
+
expect(fixture.mints).toHaveLength(1);
|
|
228
|
+
yield* fixture.respond({ token: "sprites-after-waiter-interruption" });
|
|
229
|
+
for (const fiber of [owner, replacementWaiter]) {
|
|
230
|
+
expect((yield* Fiber.join(fiber)).headers.authorization).toBe(
|
|
231
|
+
"Bearer sprites-after-waiter-interruption",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
expect(fixture.discoveries).toHaveLength(1);
|
|
235
|
+
}).pipe(Effect.timeout("2 seconds")),
|
|
236
|
+
));
|
|
237
|
+
|
|
238
|
+
test("owner interruption releases all waiters and allows another mint", () =>
|
|
239
|
+
Effect.runPromise(
|
|
240
|
+
Effect.gen(function* () {
|
|
241
|
+
const fixture = yield* mintFixture("mint-interrupted-owner");
|
|
242
|
+
const owner = yield* fixture.encode.pipe(
|
|
243
|
+
Effect.forkChild({ startImmediately: true }),
|
|
244
|
+
);
|
|
245
|
+
yield* fixture.started();
|
|
246
|
+
const waiters = yield* Effect.forEach([0, 1, 2], () =>
|
|
247
|
+
fixture.encode.pipe(Effect.forkChild({ startImmediately: true })),
|
|
248
|
+
);
|
|
249
|
+
expect(fixture.credentialReads()).toBe(4);
|
|
250
|
+
expect(fixture.mints).toHaveLength(1);
|
|
251
|
+
yield* Fiber.interrupt(owner);
|
|
252
|
+
for (const exit of yield* Fiber.awaitAll([owner, ...waiters])) {
|
|
253
|
+
expect(Exit.hasInterrupts(exit)).toBe(true);
|
|
254
|
+
}
|
|
255
|
+
const retry = yield* fixture.encode.pipe(
|
|
256
|
+
Effect.forkChild({ startImmediately: true }),
|
|
257
|
+
);
|
|
258
|
+
yield* fixture.started(1);
|
|
259
|
+
yield* fixture.respond({ token: "sprites-after-interruption" }, 200, 1);
|
|
260
|
+
expect((yield* Fiber.join(retry)).headers.authorization).toBe(
|
|
261
|
+
"Bearer sprites-after-interruption",
|
|
262
|
+
);
|
|
263
|
+
expect((yield* fixture.encode).headers.authorization).toBe(
|
|
264
|
+
"Bearer sprites-after-interruption",
|
|
265
|
+
);
|
|
266
|
+
expect(fixture.discoveries).toHaveLength(2);
|
|
267
|
+
expect(fixture.mints).toHaveLength(2);
|
|
268
|
+
}).pipe(Effect.timeout("2 seconds")),
|
|
269
|
+
));
|
|
270
|
+
});
|