@pippenly/ts-utils 1.0.1 → 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 +70 -0
- package/package.json +1 -1
- package/src/dom/resin.ts +3 -0
- package/src/index.ts +2 -0
- package/src/result/sync/index.ts +5 -0
- package/src/scope/index.ts +1 -0
- package/src/scope/scope.ts +121 -0
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
package/src/dom/resin.ts
CHANGED
|
@@ -13,6 +13,7 @@ type BoundResin<T, E extends HTMLElement> = Resin<T>
|
|
|
13
13
|
|
|
14
14
|
type BindingOptions<T, E extends HTMLElement, Mapped = T> = {
|
|
15
15
|
bindTo?: 'innerText' | 'textContent' | 'innerHTML' | 'value';
|
|
16
|
+
stringParse?: 'naive' | 'sanitized'; // try to prevent XSS when using bindTo with innerHTML
|
|
16
17
|
map?: (value: T) => Mapped;
|
|
17
18
|
tap?: (value: Mapped, element: E) => void;
|
|
18
19
|
if?: (value: Mapped) => boolean;
|
|
@@ -128,6 +129,8 @@ export function bind<T, E extends HTMLElement>(resin: Resin<T>, element: E, opti
|
|
|
128
129
|
element.textContent = String(mappedValue);
|
|
129
130
|
break;
|
|
130
131
|
case "innerHTML":
|
|
132
|
+
console.warn("Using bindTo 'innerHTML' can lead to XSS vulnerabilities if the value is not properly sanitized.");
|
|
133
|
+
// TODO: Add sanitizer dependency
|
|
131
134
|
element.innerHTML = String(mappedValue);
|
|
132
135
|
break;
|
|
133
136
|
case "value":
|
package/src/index.ts
CHANGED
package/src/result/sync/index.ts
CHANGED
|
@@ -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());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './scope.js';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { type Result, Ok, Err, type InferOk, type InferErr, type AsyncResult } from "@/result/index.js";
|
|
2
|
+
|
|
3
|
+
type Brand<K> = {
|
|
4
|
+
__brand: K;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type SimpleError = {
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type InjectorError = SimpleError & Brand<"InjectorError">;
|
|
12
|
+
|
|
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;
|
|
28
|
+
[Symbol.dispose]: () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
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>;
|
|
34
|
+
[Symbol.asyncDispose]: () => Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function scope<TDeps extends ReadonlyArray<Token<any>>>(...deps: TDeps): Scope<TDeps> {
|
|
38
|
+
return createScope(deps, new Map());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function asyncScope<TDeps extends ReadonlyArray<Token<any>>>(...deps: TDeps): AsyncScope<TDeps> {
|
|
42
|
+
return createAsyncScope(deps, new Map());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createScope<TDeps extends ReadonlyArray<Token<any>>>(
|
|
46
|
+
deps: TDeps,
|
|
47
|
+
instances: Map<symbol, any>,
|
|
48
|
+
): Scope<TDeps> {
|
|
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.`);
|
|
54
|
+
}
|
|
55
|
+
if (!instances.has(t.id)) {
|
|
56
|
+
throw new Error(`Dependency "${t.name}" has not been provided.`);
|
|
57
|
+
}
|
|
58
|
+
return instances.get(t.id)!;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
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.`);
|
|
65
|
+
}
|
|
66
|
+
const next = new Map(instances);
|
|
67
|
+
next.set(t.id, instance);
|
|
68
|
+
return createScope(deps, next);
|
|
69
|
+
},
|
|
70
|
+
run<R>(fn: (get: <C extends TDeps[number]>(t: C) => TokenType<C>) => R): R {
|
|
71
|
+
return fn(find);
|
|
72
|
+
},
|
|
73
|
+
[Symbol.dispose]() {
|
|
74
|
+
instances.clear();
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createAsyncScope<TDeps extends ReadonlyArray<Token<any>>>(
|
|
80
|
+
deps: TDeps,
|
|
81
|
+
instances: Map<symbol, any>,
|
|
82
|
+
): AsyncScope<TDeps> {
|
|
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.`);
|
|
88
|
+
}
|
|
89
|
+
if (!instances.has(t.id)) {
|
|
90
|
+
throw new Error(`Dependency "${t.name}" has not been provided.`);
|
|
91
|
+
}
|
|
92
|
+
return instances.get(t.id)!;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
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.`);
|
|
99
|
+
}
|
|
100
|
+
const next = new Map(instances);
|
|
101
|
+
next.set(t.id, instance);
|
|
102
|
+
return createAsyncScope(deps, next);
|
|
103
|
+
},
|
|
104
|
+
async run<R>(fn: (get: <C extends TDeps[number]>(t: C) => TokenType<C>) => Promise<R>): Promise<R> {
|
|
105
|
+
return await fn(find);
|
|
106
|
+
},
|
|
107
|
+
async [Symbol.asyncDispose]() {
|
|
108
|
+
instances.clear();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
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>;
|