skyr 0.1.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 +513 -0
- package/package.json +19 -0
- package/src/di.test.ts +299 -0
- package/src/di.ts +624 -0
- package/src/index.ts +11 -0
- package/src/result.test.ts +1052 -0
- package/src/result.ts +663 -0
- package/src/types.test.ts +254 -0
package/README.md
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# skyr
|
|
2
|
+
|
|
3
|
+
> Skyr is a thick, protein-rich, and nutritious traditional Icelandic dairy
|
|
4
|
+
> product.
|
|
5
|
+
|
|
6
|
+
**skyr** is a lightweight TypeScript library for functional error handling with
|
|
7
|
+
seamless async support and optional dependency injection.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# npm (coming soon)
|
|
13
|
+
npm install skyr
|
|
14
|
+
|
|
15
|
+
# bun
|
|
16
|
+
bun add skyr
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Basic Results
|
|
22
|
+
|
|
23
|
+
A `Result` can either be **ok** or it can **fail**. When ok, the result contains
|
|
24
|
+
a value of whatever type you need. When failed, it contains a `Failure` - a
|
|
25
|
+
structured error with a `code`, `message`, and optional `cause` for debugging.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { fail, ok } from "skyr";
|
|
29
|
+
|
|
30
|
+
type User = { id: string; email: string; passwordHash: string };
|
|
31
|
+
|
|
32
|
+
// Simple validation
|
|
33
|
+
function validateEmail(email: string) {
|
|
34
|
+
if (!email.includes("@")) {
|
|
35
|
+
return fail("INVALID_EMAIL", "Email must contain @");
|
|
36
|
+
}
|
|
37
|
+
return ok(email);
|
|
38
|
+
} // => Result<string, Failure<"INVALID_EMAIL">>
|
|
39
|
+
|
|
40
|
+
// Check the result
|
|
41
|
+
const result = validateEmail("user@example.com");
|
|
42
|
+
|
|
43
|
+
if (result.isOk()) {
|
|
44
|
+
console.log("Valid email:", result.unwrap());
|
|
45
|
+
} else {
|
|
46
|
+
console.log("Error:", result.unwrap().message);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use `.isOk()` and `.hasFailed()` to check which variant you have, then
|
|
51
|
+
`.unwrap()` to extract the value or the failure.
|
|
52
|
+
|
|
53
|
+
### Transforming Results
|
|
54
|
+
|
|
55
|
+
Transform ok values with `.map()`, which automatically skips if the result has
|
|
56
|
+
failed.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
function normalizeEmail(email: string) {
|
|
60
|
+
return email.toLowerCase().trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = validateEmail("User@Example.com")
|
|
64
|
+
.map(normalizeEmail)
|
|
65
|
+
.map((email) => `Welcome, ${email}!`); // Result<string, Failure<"INVALID_EMAIL">>
|
|
66
|
+
|
|
67
|
+
// Pattern matching handles both cases
|
|
68
|
+
const message = result.match({
|
|
69
|
+
ok: (greeting) => greeting,
|
|
70
|
+
failed: (error) => `Error: ${error.message}`,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.log(message);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Handling Specific Errors
|
|
77
|
+
|
|
78
|
+
Use `.mapFailure()` with an object to handle specific error codes—perfect for
|
|
79
|
+
recovery or transforming specific failures while letting others pass through.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
type AppError =
|
|
83
|
+
| Failure<"NOT_FOUND">
|
|
84
|
+
| Failure<"TIMEOUT">
|
|
85
|
+
| Failure<"AUTH_FAILED">;
|
|
86
|
+
|
|
87
|
+
declare function fetchUser(id: string): Result<User, AppError>;
|
|
88
|
+
|
|
89
|
+
const result = fetchUser("123").mapFailure({
|
|
90
|
+
// TypeScript autocompletes available codes: "NOT_FOUND", "TIMEOUT", "AUTH_FAILED"
|
|
91
|
+
"NOT_FOUND": () => ok(guestUser), // Recovery
|
|
92
|
+
"TIMEOUT": (err) => fail("RETRY")(err.message), // Transform
|
|
93
|
+
// AUTH_FAILED not handled - passes through unchanged
|
|
94
|
+
});
|
|
95
|
+
// Result<User | GuestUser, Failure<"RETRY"> | Failure<"AUTH_FAILED">>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Each handler:
|
|
99
|
+
- Gets autocomplete for available error codes
|
|
100
|
+
- Receives the narrowed `Failure<"CODE">` type
|
|
101
|
+
- Can return `ok(value)` for recovery or `fail(code)(message)` to transform
|
|
102
|
+
- Unhandled codes automatically pass through
|
|
103
|
+
|
|
104
|
+
This works with async results too:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const result = await fetchUserAsync("123").mapFailure({
|
|
108
|
+
"NOT_FOUND": () => ok(guestUser),
|
|
109
|
+
"TIMEOUT": () => fromThrowable(retryFetch("123")),
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Async Operations
|
|
114
|
+
|
|
115
|
+
When you work with Promises, skyr automatically returns an `AsyncResult` - and
|
|
116
|
+
it stays async until you await it.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { wrapThrowable } from "skyr";
|
|
120
|
+
|
|
121
|
+
// Simulate database lookup (might throw or reject)
|
|
122
|
+
async function findUserInDb(email: string) {
|
|
123
|
+
const user = await db.query("SELECT * FROM users WHERE email = ?", [email]);
|
|
124
|
+
if (!user) throw new Error("User not found");
|
|
125
|
+
return user;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Wrap throwing async function to return Results
|
|
129
|
+
const findUser = wrapThrowable(
|
|
130
|
+
findUserInDb,
|
|
131
|
+
(cause) => ({
|
|
132
|
+
code: "USER_NOT_FOUND",
|
|
133
|
+
message: "Unable to locate user",
|
|
134
|
+
cause,
|
|
135
|
+
}),
|
|
136
|
+
); // => (email: string) => AsyncResult<User, Failure<"USER_NOT_FOUND">>
|
|
137
|
+
|
|
138
|
+
// Chain async operations
|
|
139
|
+
const userResult = validateEmail("user@example.com")
|
|
140
|
+
.map(normalizeEmail)
|
|
141
|
+
.map((email) => findUser(email)); // AsyncResult<User, Failure<"INVALID_EMAIL"> | Failure<"USER_NOT_FOUND">>
|
|
142
|
+
|
|
143
|
+
// Await to get the Result
|
|
144
|
+
const user = await userResult.unwrap();
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`wrapThrowable()` wraps a function to catch any errors and convert them to
|
|
148
|
+
Failures. Once a Result becomes async, it stays async—this is called "async
|
|
149
|
+
poison".
|
|
150
|
+
|
|
151
|
+
### Better Type Inference with `fn()`
|
|
152
|
+
|
|
153
|
+
The `fn()` helper provides cleaner type inference for functions.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { fn } from "skyr";
|
|
157
|
+
|
|
158
|
+
const validatePassword = fn((password: string) => {
|
|
159
|
+
if (password.length < 8) {
|
|
160
|
+
return fail("WEAK_PASSWORD", "Password must be at least 8 characters");
|
|
161
|
+
}
|
|
162
|
+
return ok(password);
|
|
163
|
+
}); // (password: string) => Result<string, Failure<"WEAK_PASSWORD">>
|
|
164
|
+
|
|
165
|
+
const result = validatePassword("secret"); // Result<string, Failure<"WEAK_PASSWORD">>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Generator Syntax
|
|
169
|
+
|
|
170
|
+
Generators let you write error-handling code that reads like regular code,
|
|
171
|
+
automatically short-circuiting on failures.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
const checkPassword = fn((password: string, hash: string) => {
|
|
175
|
+
const isValid = password === hash; // Simplified
|
|
176
|
+
if (!isValid) {
|
|
177
|
+
return fail("INVALID_PASSWORD", "Password does not match");
|
|
178
|
+
}
|
|
179
|
+
return ok(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Using generators with yield*
|
|
183
|
+
const loginUser = fn(function* (email: string, password: string) {
|
|
184
|
+
// yield* unwraps ok values or short-circuits on fail
|
|
185
|
+
const validatedEmail = yield* validateEmail(email);
|
|
186
|
+
const normalizedEmail = normalizeEmail(validatedEmail);
|
|
187
|
+
|
|
188
|
+
// Async works seamlessly
|
|
189
|
+
const user = yield* findUser(normalizedEmail);
|
|
190
|
+
|
|
191
|
+
// If this fails, the whole function returns the failure
|
|
192
|
+
yield* checkPassword(password, user.passwordHash);
|
|
193
|
+
|
|
194
|
+
return ok(user);
|
|
195
|
+
}); // Fn with dependencies that returns Result<User, Failure<"INVALID_EMAIL"> | ...>
|
|
196
|
+
|
|
197
|
+
const result = await loginUser("user@example.com", "secret123");
|
|
198
|
+
// Result<User, Failure<"INVALID_EMAIL"> | Failure<"USER_NOT_FOUND"> | Failure<"INVALID_PASSWORD">>
|
|
199
|
+
|
|
200
|
+
if (result.isOk()) {
|
|
201
|
+
console.log("Logged in:", result.unwrap());
|
|
202
|
+
} else {
|
|
203
|
+
console.log("Login failed:", result.unwrap().message);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
With `yield*`, you unwrap ok values automatically. If any operation fails, the
|
|
208
|
+
entire function short-circuits and returns that failure—no nested `if`
|
|
209
|
+
statements needed.
|
|
210
|
+
|
|
211
|
+
### Composing Functions
|
|
212
|
+
|
|
213
|
+
Yield other Result-returning functions and their errors propagate automatically.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const createSession = fn(function* (user: User) {
|
|
217
|
+
const sessionId = crypto.randomUUID();
|
|
218
|
+
return ok({ sessionId, userId: user.id });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const loginUser = fn(function* (email: string, password: string) {
|
|
222
|
+
const validatedEmail = yield* validateEmail(email);
|
|
223
|
+
const normalizedEmail = yield* ok(normalizeEmail(validatedEmail));
|
|
224
|
+
const user = yield* findUser(normalizedEmail);
|
|
225
|
+
|
|
226
|
+
yield* checkPassword(password, user.passwordHash);
|
|
227
|
+
const session = yield* createSession(user);
|
|
228
|
+
|
|
229
|
+
return ok({ user, session });
|
|
230
|
+
}); // All error types collected: Failure<"INVALID_EMAIL"> | Failure<"USER_NOT_FOUND"> | Failure<"INVALID_PASSWORD">
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Dependency Injection
|
|
234
|
+
|
|
235
|
+
Declare dependencies without implementing them yet—write complete business logic
|
|
236
|
+
first.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Declare dependencies (just the types!)
|
|
240
|
+
const Database = fn.dependency<{
|
|
241
|
+
findUser: (email: string) => Promise<User | null>;
|
|
242
|
+
createSession: (userId: string) => Promise<{ sessionId: string }>;
|
|
243
|
+
}>()("database");
|
|
244
|
+
|
|
245
|
+
const Logger = fn.dependency<{
|
|
246
|
+
info: (message: string) => void;
|
|
247
|
+
error: (message: string, error: unknown) => void;
|
|
248
|
+
}>()("logger");
|
|
249
|
+
|
|
250
|
+
// Use dependencies in your code
|
|
251
|
+
const loginUser = fn(function* (email: string, password: string) {
|
|
252
|
+
// Get dependencies with yield* fn.require()
|
|
253
|
+
const db = yield* fn.require(Database);
|
|
254
|
+
const logger = yield* fn.require(Logger);
|
|
255
|
+
|
|
256
|
+
logger.info(`Login attempt for ${email}`);
|
|
257
|
+
|
|
258
|
+
const validatedEmail = yield* validateEmail(email);
|
|
259
|
+
const user = yield* fromThrowable(() => db.findUser(validatedEmail));
|
|
260
|
+
|
|
261
|
+
if (!user) {
|
|
262
|
+
logger.error("User not found", { email });
|
|
263
|
+
return fail("USER_NOT_FOUND", "No user found");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
yield* checkPassword(password, user.passwordHash);
|
|
267
|
+
|
|
268
|
+
const session = yield* fromThrowable(() => db.createSession(user.id));
|
|
269
|
+
|
|
270
|
+
logger.info(`User ${user.id} logged in`);
|
|
271
|
+
|
|
272
|
+
return ok({ user, session });
|
|
273
|
+
}); // Note: loginUser requires Database and Logger dependencies
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The code is complete and type-safe, but we haven't implemented `Database` or
|
|
277
|
+
`Logger` yet.
|
|
278
|
+
|
|
279
|
+
### Type-Safe Dependency Tracking
|
|
280
|
+
|
|
281
|
+
TypeScript tracks which dependencies are required and prevents calling functions
|
|
282
|
+
until all are provided.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// Compiler error: missing dependencies
|
|
286
|
+
const result = loginUser("user@example.com", "password");
|
|
287
|
+
// ^^^^^^^^^ Type error!
|
|
288
|
+
|
|
289
|
+
// Must inject dependencies first
|
|
290
|
+
const runLogin = loginUser
|
|
291
|
+
.inject(
|
|
292
|
+
Database.impl({
|
|
293
|
+
findUser: async (email) => {/* real implementation */},
|
|
294
|
+
createSession: async (userId) => {/* real implementation */},
|
|
295
|
+
}),
|
|
296
|
+
Logger.impl({
|
|
297
|
+
info: (msg) => console.log(msg),
|
|
298
|
+
error: (msg, err) => console.error(msg, err),
|
|
299
|
+
}),
|
|
300
|
+
); // runLogin now requires no dependencies
|
|
301
|
+
|
|
302
|
+
// Now callable
|
|
303
|
+
const result = await runLogin("user@example.com", "password");
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Partial injection works too:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
const partialLogin = loginUser.inject(
|
|
310
|
+
Database.impl({/* ... */}),
|
|
311
|
+
); // partialLogin still requires: Logger
|
|
312
|
+
|
|
313
|
+
const fullLogin = partialLogin.inject(
|
|
314
|
+
Logger.impl({/* ... */}),
|
|
315
|
+
); // fullLogin requires: (none)
|
|
316
|
+
|
|
317
|
+
await fullLogin("user@example.com", "password"); // âś“
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Different Implementations
|
|
321
|
+
|
|
322
|
+
Swap implementations for testing, development, and production.
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Production
|
|
326
|
+
const productionDb = Database.impl({
|
|
327
|
+
findUser: async (email) => {
|
|
328
|
+
const result = await pool.query("SELECT * FROM users WHERE email = $1", [
|
|
329
|
+
email,
|
|
330
|
+
]);
|
|
331
|
+
return result.rows[0] || null;
|
|
332
|
+
},
|
|
333
|
+
createSession: async (userId) => {
|
|
334
|
+
const sessionId = crypto.randomUUID();
|
|
335
|
+
await pool.query("INSERT INTO sessions (id, user_id) VALUES ($1, $2)", [
|
|
336
|
+
sessionId,
|
|
337
|
+
userId,
|
|
338
|
+
]);
|
|
339
|
+
return { sessionId };
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const productionLogger = Logger.impl({
|
|
344
|
+
info: (msg) => winston.info(msg),
|
|
345
|
+
error: (msg, err) => winston.error(msg, { error: err }),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Testing (mocks)
|
|
349
|
+
const testDb = Database.impl({
|
|
350
|
+
findUser: async (email) => {
|
|
351
|
+
if (email === "test@example.com") {
|
|
352
|
+
return { id: "test-123", email, passwordHash: "hashed" };
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
},
|
|
356
|
+
createSession: async () => ({ sessionId: "test-session" }),
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const testLogger = Logger.impl({
|
|
360
|
+
info: () => {}, // Silent in tests
|
|
361
|
+
error: () => {},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Use in production
|
|
365
|
+
const prodLogin = loginUser.inject(productionDb, productionLogger);
|
|
366
|
+
|
|
367
|
+
// Use in tests
|
|
368
|
+
const testLogin = loginUser.inject(testDb, testLogger);
|
|
369
|
+
|
|
370
|
+
// Same code, different implementations
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Nested Dependencies
|
|
374
|
+
|
|
375
|
+
Functions can call other functions with dependencies, and those dependencies
|
|
376
|
+
propagate automatically.
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
const RateLimiter = fn.dependency<{
|
|
380
|
+
checkLimit: (email: string) => Promise<boolean>;
|
|
381
|
+
}>()("rateLimiter");
|
|
382
|
+
|
|
383
|
+
// This function has its own dependency
|
|
384
|
+
const checkRateLimit = fn(function* (email: string) {
|
|
385
|
+
const limiter = yield* fn.require(RateLimiter);
|
|
386
|
+
const allowed = yield* fromThrowable(() => limiter.checkLimit(email));
|
|
387
|
+
|
|
388
|
+
if (!allowed) {
|
|
389
|
+
return fail("RATE_LIMITED", "Too many login attempts");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return ok(true);
|
|
393
|
+
}); // Requires: RateLimiter
|
|
394
|
+
|
|
395
|
+
// Call functions with dependencies using .yield()
|
|
396
|
+
const loginUser = fn(function* (email: string, password: string) {
|
|
397
|
+
const db = yield* fn.require(Database);
|
|
398
|
+
const logger = yield* fn.require(Logger);
|
|
399
|
+
|
|
400
|
+
yield* checkRateLimit.yield(email);
|
|
401
|
+
|
|
402
|
+
// ... rest of login logic
|
|
403
|
+
|
|
404
|
+
return ok({ user, session });
|
|
405
|
+
}); // Requires: Database, Logger, RateLimiter (inherited from checkRateLimit)
|
|
406
|
+
|
|
407
|
+
const runLogin = loginUser.inject(
|
|
408
|
+
Database.impl({/* ... */}),
|
|
409
|
+
Logger.impl({/* ... */}),
|
|
410
|
+
RateLimiter.impl({/* ... */}), // Required!
|
|
411
|
+
);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Use `.yield()` to call functions with dependencies. Dependencies automatically
|
|
415
|
+
propagate to parent functions and are tracked in the type system.
|
|
416
|
+
|
|
417
|
+
## Testing
|
|
418
|
+
|
|
419
|
+
Inject mocks without any mocking libraries.
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import { test, expect } from "bun:test";
|
|
423
|
+
|
|
424
|
+
test("loginUser - successful login", async () => {
|
|
425
|
+
const mockDb = Database.impl({
|
|
426
|
+
findUser: async () => ({
|
|
427
|
+
id: "123",
|
|
428
|
+
email: "test@example.com",
|
|
429
|
+
passwordHash: "hash",
|
|
430
|
+
}),
|
|
431
|
+
createSession: async () => ({ sessionId: "session-123" }),
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const mockLogger = Logger.impl({ info: () => {}, error: () => {} });
|
|
435
|
+
|
|
436
|
+
const login = loginUser.inject(mockDb, mockLogger);
|
|
437
|
+
const result = await login("test@example.com", "password");
|
|
438
|
+
|
|
439
|
+
expect(result.isOk()).toBe(true);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("loginUser - user not found", async () => {
|
|
443
|
+
const mockDb = Database.impl({
|
|
444
|
+
findUser: async () => null,
|
|
445
|
+
createSession: async () => ({ sessionId: "session-123" }),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const mockLogger = Logger.impl({ info: () => {}, error: () => {} });
|
|
449
|
+
|
|
450
|
+
const login = loginUser.inject(mockDb, mockLogger);
|
|
451
|
+
const result = await login("test@example.com", "password");
|
|
452
|
+
|
|
453
|
+
expect(result.hasFailed()).toBe(true);
|
|
454
|
+
if (result.hasFailed()) {
|
|
455
|
+
expect(result.unwrap().code).toBe("USER_NOT_FOUND");
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## API Reference
|
|
461
|
+
|
|
462
|
+
### Core Functions
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
ok(value) // Create ok result
|
|
466
|
+
fail(code, message, cause?) // Create failed result
|
|
467
|
+
fromThrowable(fn, mapper?) // Convert throwing code to Result
|
|
468
|
+
wrapThrowable(fn, mapper?) // Wrap function to return Results
|
|
469
|
+
|
|
470
|
+
result.map(fn) // Transform ok value
|
|
471
|
+
result.mapFailure(fn) // Transform failure
|
|
472
|
+
result.mapFailure({ CODE: handler }) // Handle specific error codes
|
|
473
|
+
result.match({ ok, failed }) // Pattern match both cases
|
|
474
|
+
result.unwrap() // Get value or failure
|
|
475
|
+
result.unwrapOr(default) // Get value or default
|
|
476
|
+
result.isOk() // Check if ok
|
|
477
|
+
result.hasFailed() // Check if failed
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Generator Functions
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
const myFn = fn(function* (arg) {
|
|
484
|
+
const value = yield* someResult; // Unwrap or short-circuit
|
|
485
|
+
return ok(result);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
myFn(arg); // Call (if no dependencies)
|
|
489
|
+
myFn.run(arg); // Explicit run
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Dependency Injection
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
// Declare
|
|
496
|
+
const Service = fn.dependency<Type>()("key");
|
|
497
|
+
|
|
498
|
+
// Require
|
|
499
|
+
const service = yield * fn.require(Service);
|
|
500
|
+
|
|
501
|
+
// Implement
|
|
502
|
+
const impl = Service.impl({/* implementation */});
|
|
503
|
+
|
|
504
|
+
// Inject
|
|
505
|
+
const runnable = myFn.inject(impl1, impl2);
|
|
506
|
+
|
|
507
|
+
// Compose
|
|
508
|
+
yield * childFn.yield(args);
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## License
|
|
512
|
+
|
|
513
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skyr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"module": "src/index.ts",
|
|
6
|
+
"files": ["src/*", "README.md", "package.json"],
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "bun test",
|
|
9
|
+
"check": "bun build ./src/index.ts --target=node --outfile=/dev/null",
|
|
10
|
+
"lint": "biome check .",
|
|
11
|
+
"format": "biome format --write .",
|
|
12
|
+
"format:check": "biome format ."
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@biomejs/biome": "^1.9.4",
|
|
16
|
+
"@types/bun": "latest",
|
|
17
|
+
"typescript": "^5.9.3"
|
|
18
|
+
}
|
|
19
|
+
}
|