@pippenly/ts-utils 1.0.4 → 1.0.5

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 CHANGED
@@ -40,6 +40,8 @@ For two way binding, `model(sourceResin, element, options?)` exists, where eleme
40
40
  throttle / debounce options and custom get/set if needed.
41
41
 
42
42
  ```ts
43
+ import { resin, bind, model, watchEffect } from "@pippenly/ts-utils/dom";
44
+
43
45
  const name = resin("");
44
46
  const m = model(name, document.querySelector<HTMLInputElement>("#name")!, {
45
47
  debounce: 300
@@ -59,6 +61,74 @@ const ageModel = model(age, document.querySelector<HTMLInputElement>("#age")!, {
59
61
  });
60
62
  ```
61
63
 
64
+ # Scope
65
+ Creating a scope with a dependency injection system
66
+ ```ts
67
+ import { asyncScope, Err, Ok, token, type AsyncRunnable, type Result } from "@pippenly/ts-utils";
68
+
69
+ interface Logger {
70
+ info: (message: string) => void;
71
+ warn: (message: string) => void;
72
+ error: (message: string) => void;
73
+ }
74
+
75
+ interface HttpClient {
76
+ get: (url: string) => Promise<any>;
77
+ post: (url: string, data: any) => Promise<any>;
78
+ }
79
+
80
+ type HttpRunnable = AsyncRunnable<typeof httpScope, Result<any, string>>;
81
+
82
+ const httpScope = asyncScope(Logger, HttpClient)
83
+ .provide(Logger, new ConsoleLogger())
84
+ .provide(HttpClient, new BrowserHttpClient());
85
+
86
+ const sendMessage: HttpRunnable = async (get): AsyncResult<any, string> => {
87
+ const logger = get(Logger);
88
+ const httpClient = get(HttpClient);
89
+ logger.info("Sending message...");
90
+ try {
91
+ const response = await httpClient.post("/api/messages", { text: "Hello, World!" });
92
+ logger.info("Message sent successfully: " + JSON.stringify(response));
93
+ return Ok(response);
94
+ } catch (error) {
95
+ logger.error("Failed to send message: " + error);
96
+ return Err("Failed to send message");
97
+ }
98
+ }
99
+
100
+ const sendHealth: HttpRunnable = async (get): AsyncResult<any, string> => {
101
+ const logger = get(Logger);
102
+ const httpClient = get(HttpClient);
103
+ logger.info("Checking server health...");
104
+ try {
105
+ const response = await httpClient.get("/api/health");
106
+ logger.info("Server health: " + JSON.stringify(response));
107
+ return Ok(response);
108
+ }
109
+ catch (error) {
110
+ logger.error("Failed to check server health: " + error);
111
+ return Err("Failed to check server health");
112
+ }
113
+ }
114
+
115
+ async function run() {
116
+ const sendMessageResult = await httpScope.run(sendMessage);
117
+ if (sendMessageResult.ok) {
118
+ console.log("Message response:", sendMessageResult.value);
119
+ } else {
120
+ console.error("Error sending message:", sendMessageResult.error);
121
+ }
122
+
123
+ const healthResult = await httpScope.run(sendHealth);
124
+ if (healthResult.ok) {
125
+ console.log("Health response:", healthResult.value);
126
+ } else {
127
+ console.error("Error checking health:", healthResult.error);
128
+ }
129
+ }
130
+
131
+ ```
62
132
 
63
133
  ## Utilities
64
134
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pippenly/ts-utils",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,3 +1,5 @@
1
+ import type { AsyncResult } from "../async/index.js";
2
+
1
3
  export type Ok<T> = {
2
4
  ok: true;
3
5
  value: T;
@@ -19,6 +21,9 @@ export function Err<E>(error: E, code?: number): Err<E> {
19
21
  return { ok: false, error, code };
20
22
  }
21
23
 
24
+ export type InferOk<R> = R extends Result<infer T, any> | AsyncResult<infer T, any> ? T : never;
25
+ export type InferErr<R> = R extends Result<any, infer E> | AsyncResult<any, infer E> ? E : never;
26
+
22
27
  export function tryCatch<T, E>(fn: () => T, onErr: (e: unknown) => E): Result<T, E> {
23
28
  try {
24
29
  return Ok(fn());
@@ -1,4 +1,4 @@
1
- import { type Result, Ok, Err } from "@/result/index.js";
1
+ import { type Result, Ok, Err, type InferOk, type InferErr, type AsyncResult } from "@/result/index.js";
2
2
 
3
3
  type Brand<K> = {
4
4
  __brand: K;
@@ -8,59 +8,66 @@ type SimpleError = {
8
8
  message: string;
9
9
  };
10
10
 
11
- type Ctor<T> = new (...args: any[]) => T;
12
-
13
11
  export type InjectorError = SimpleError & Brand<"InjectorError">;
14
12
 
15
- export type Scope<TDeps extends readonly Ctor<any>[]> = {
16
- provide: <C extends TDeps[number]>(ctor: C, instance: InstanceType<C>) => Result<Scope<TDeps>, InjectorError>;
17
- run: <R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => R) => R;
18
- finalize: <R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => R) => R;
13
+ export type Token<T> = {
14
+ readonly id: symbol;
15
+ readonly name: string;
16
+ readonly _type: T;
17
+ };
18
+
19
+ export function token<T>(name: string): Token<T> {
20
+ return { id: Symbol(name), name } as Token<T>;
21
+ }
22
+
23
+ type TokenType<T> = T extends Token<infer U> ? U : never;
24
+
25
+ export type Scope<TDeps extends readonly Token<any>[]> = {
26
+ provide: <C extends TDeps[number]>(token: C, instance: TokenType<C>) => Scope<TDeps>;
27
+ run: <R>(fn: (get: <C extends TDeps[number]>(token: C) => TokenType<C>) => R) => R;
19
28
  [Symbol.dispose]: () => void;
20
29
  };
21
30
 
22
- export type AsyncScope<TDeps extends readonly Ctor<any>[]> = {
23
- provide: <C extends TDeps[number]>(ctor: C, instance: InstanceType<C>) => Result<AsyncScope<TDeps>, InjectorError>;
24
- run: <R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => Promise<R>) => Promise<R>;
25
- finalize: <R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => Promise<R>) => Promise<R>;
31
+ export type AsyncScope<TDeps extends readonly Token<any>[]> = {
32
+ provide: <C extends TDeps[number]>(token: C, instance: TokenType<C>) => AsyncScope<TDeps>;
33
+ run: <R>(fn: (get: <C extends TDeps[number]>(token: C) => TokenType<C>) => Promise<R>) => Promise<R>;
26
34
  [Symbol.asyncDispose]: () => Promise<void>;
27
35
  };
28
36
 
29
- export function scope<TDeps extends ReadonlyArray<Ctor<any>>>(...deps: TDeps): Scope<TDeps> {
37
+ export function scope<TDeps extends ReadonlyArray<Token<any>>>(...deps: TDeps): Scope<TDeps> {
30
38
  return createScope(deps, new Map());
31
39
  }
32
40
 
33
- export function asyncScope<TDeps extends ReadonlyArray<Ctor<any>>>(...deps: TDeps): AsyncScope<TDeps> {
41
+ export function asyncScope<TDeps extends ReadonlyArray<Token<any>>>(...deps: TDeps): AsyncScope<TDeps> {
34
42
  return createAsyncScope(deps, new Map());
35
43
  }
36
44
 
37
- function createScope<TDeps extends ReadonlyArray<Ctor<any>>>(
45
+ function createScope<TDeps extends ReadonlyArray<Token<any>>>(
38
46
  deps: TDeps,
39
- instances: Map<Ctor<any>, any>,
47
+ instances: Map<symbol, any>,
40
48
  ): Scope<TDeps> {
41
- const find = <C extends TDeps[number]>(ctor: C): Result<InstanceType<C>, InjectorError> => {
42
- if (!deps.includes(ctor)) {
43
- return Err(taggedError("InjectorError", `Dependency ${ctor.name} is not registered in this scope.`));
49
+ const ids = new Set(deps.map(d => d.id));
50
+
51
+ const find = <C extends TDeps[number]>(t: C): TokenType<C> => {
52
+ if (!ids.has(t.id)) {
53
+ throw new Error(`Dependency "${t.name}" is not registered in this scope.`);
44
54
  }
45
- if (!instances.has(ctor)) {
46
- return Err(taggedError("InjectorError", `Dependency ${ctor.name} has not been provided.`));
55
+ if (!instances.has(t.id)) {
56
+ throw new Error(`Dependency "${t.name}" has not been provided.`);
47
57
  }
48
- return Ok(instances.get(ctor)!);
58
+ return instances.get(t.id)!;
49
59
  };
50
60
 
51
61
  return {
52
- provide<C extends TDeps[number]>(ctor: C, instance: InstanceType<C>): Result<Scope<TDeps>, InjectorError> {
53
- if (!deps.includes(ctor)) {
54
- return Err(taggedError("InjectorError", `Cannot provide instance for ${ctor.name}: not registered in this scope.`));
62
+ provide<C extends TDeps[number]>(t: C, instance: TokenType<C>): Scope<TDeps> {
63
+ if (!ids.has(t.id)) {
64
+ throw new Error(`Cannot provide "${t.name}": not registered in this scope.`);
55
65
  }
56
66
  const next = new Map(instances);
57
- next.set(ctor, instance);
58
- return Ok(createScope(deps, next));
59
- },
60
- run<R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => R): R {
61
- return fn(find);
67
+ next.set(t.id, instance);
68
+ return createScope(deps, next);
62
69
  },
63
- finalize<R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => R): R {
70
+ run<R>(fn: (get: <C extends TDeps[number]>(t: C) => TokenType<C>) => R): R {
64
71
  return fn(find);
65
72
  },
66
73
  [Symbol.dispose]() {
@@ -69,34 +76,33 @@ function createScope<TDeps extends ReadonlyArray<Ctor<any>>>(
69
76
  };
70
77
  }
71
78
 
72
- function createAsyncScope<TDeps extends ReadonlyArray<Ctor<any>>>(
79
+ function createAsyncScope<TDeps extends ReadonlyArray<Token<any>>>(
73
80
  deps: TDeps,
74
- instances: Map<Ctor<any>, any>,
81
+ instances: Map<symbol, any>,
75
82
  ): AsyncScope<TDeps> {
76
- const find = <C extends TDeps[number]>(ctor: C): Result<InstanceType<C>, InjectorError> => {
77
- if (!deps.includes(ctor)) {
78
- return Err(taggedError("InjectorError", `Dependency ${ctor.name} is not registered in this scope.`));
83
+ const ids = new Set(deps.map(d => d.id));
84
+
85
+ const find = <C extends TDeps[number]>(t: C): TokenType<C> => {
86
+ if (!ids.has(t.id)) {
87
+ throw new Error(`Dependency "${t.name}" is not registered in this scope.`);
79
88
  }
80
- if (!instances.has(ctor)) {
81
- return Err(taggedError("InjectorError", `Dependency ${ctor.name} has not been provided.`));
89
+ if (!instances.has(t.id)) {
90
+ throw new Error(`Dependency "${t.name}" has not been provided.`);
82
91
  }
83
- return Ok(instances.get(ctor)!);
92
+ return instances.get(t.id)!;
84
93
  };
85
94
 
86
95
  return {
87
- provide<C extends TDeps[number]>(ctor: C, instance: InstanceType<C>): Result<AsyncScope<TDeps>, InjectorError> {
88
- if (!deps.includes(ctor)) {
89
- return Err(taggedError("InjectorError", `Cannot provide instance for ${ctor.name}: not registered in this scope.`));
96
+ provide<C extends TDeps[number]>(t: C, instance: TokenType<C>): AsyncScope<TDeps> {
97
+ if (!ids.has(t.id)) {
98
+ throw new Error(`Cannot provide "${t.name}": not registered in this scope.`);
90
99
  }
91
100
  const next = new Map(instances);
92
- next.set(ctor, instance);
93
- return Ok(createAsyncScope(deps, next));
101
+ next.set(t.id, instance);
102
+ return createAsyncScope(deps, next);
94
103
  },
95
- async run<R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => Promise<R>): Promise<R> {
96
- return fn(find);
97
- },
98
- async finalize<R>(fn: (get: <C extends TDeps[number]>(ctor: C) => Result<InstanceType<C>, InjectorError>) => Promise<R>): Promise<R> {
99
- return fn(find);
104
+ async run<R>(fn: (get: <C extends TDeps[number]>(t: C) => TokenType<C>) => Promise<R>): Promise<R> {
105
+ return await fn(find);
100
106
  },
101
107
  async [Symbol.asyncDispose]() {
102
108
  instances.clear();
@@ -104,9 +110,12 @@ function createAsyncScope<TDeps extends ReadonlyArray<Ctor<any>>>(
104
110
  };
105
111
  }
106
112
 
107
- function taggedError<K extends string>(brand: K, message: string): Brand<K> & SimpleError {
108
- return {
109
- __brand: brand,
110
- message,
111
- };
112
- }
113
+ type InferDeps<T> = T extends Scope<infer D> | AsyncScope<infer D> ? D : never;
114
+
115
+ export type Runnable<TScope extends Scope<any>, TResult> = (
116
+ get: <C extends InferDeps<TScope>[number]>(token: C) => TokenType<C>
117
+ ) => TResult;
118
+
119
+ export type AsyncRunnable<TScope extends AsyncScope<any>, TResult> = (
120
+ get: <C extends InferDeps<TScope>[number]>(token: C) => TokenType<C>
121
+ ) => Promise<TResult>;