@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 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.1",
3
+ "version": "1.0.5",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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
@@ -1,2 +1,4 @@
1
1
  export * from './result/index.js';
2
2
  export * from './utils/index.js';
3
+ export * from './scope/index.js';
4
+ export * from './dom/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());
@@ -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>;