@code-essentials/utils 1.0.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.
@@ -0,0 +1,21 @@
1
+ {
2
+ "configurations": [
3
+ {
4
+ "name": "test",
5
+ "request": "launch",
6
+ "runtimeArgs": [
7
+ "run-script",
8
+ "test"
9
+ ],
10
+ "runtimeExecutable": "pnpm",
11
+ "sourceMaps": true,
12
+ "pauseForSourceMap": true,
13
+ "preLaunchTask": "npm: build:debug",
14
+ "internalConsoleOptions": "neverOpen",
15
+ "skipFiles": [
16
+ "<node_internals>/**"
17
+ ],
18
+ "type": "node"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": "2.0.0",
3
+ "tasks": [
4
+ {
5
+ "label": "npm: build:debug",
6
+ "type": "shell",
7
+ "command": "npm",
8
+ "args": [
9
+ "run-script",
10
+ "build:debug",
11
+ ],
12
+ "problemMatcher": [
13
+ "$tsc"
14
+ ],
15
+ "presentation": {
16
+ "reveal": "silent",
17
+ "close": true,
18
+ },
19
+ "group": "build"
20
+ }
21
+ ]
22
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@code-essentials/utils",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "scripts": {
9
+ "clean": "rm -rf dist",
10
+ "prebuild": "pnpm run clean",
11
+ "prebuild:debug": "pnpm run clean",
12
+ "build": "tsc",
13
+ "build:debug": "tsc -p tsconfig.debug.json",
14
+ "pretest": "pnpm run build:debug",
15
+ "test": "ava"
16
+ },
17
+ "devDependencies": {
18
+ "@ava/typescript": "^6.0.0",
19
+ "@code-essentials/tsconfig": "^1.0.1",
20
+ "@types/node": "^24.5.2",
21
+ "ava": "^6.4.1",
22
+ "typescript": "^5.9.2"
23
+ },
24
+ "ava": {
25
+ "files": [
26
+ "src/**/*.spec.ts"
27
+ ],
28
+ "typescript": {
29
+ "rewritePaths": {
30
+ "src/": "dist/"
31
+ },
32
+ "extensions": [
33
+ "ts"
34
+ ],
35
+ "compile": false
36
+ }
37
+ },
38
+ "author": {
39
+ "name": "Isaac Valdez",
40
+ "email": "isaac_valdez@msn.com",
41
+ "url": "https://i12345.github.io"
42
+ },
43
+ "license": "MIT"
44
+ }
@@ -0,0 +1,146 @@
1
+ interface AsyncVariableResultBase<Type> {
2
+ readonly type: Type
3
+ }
4
+
5
+ interface AsyncVariableResultResolved<T> extends AsyncVariableResultBase<"resolved"> {
6
+ readonly value: T
7
+ }
8
+
9
+ interface AsyncVariableResultRejected extends AsyncVariableResultBase<"rejected"> {
10
+ readonly error: any
11
+ }
12
+
13
+ type AsyncVariableResult<T> =
14
+ | AsyncVariableResultResolved<T>
15
+ | AsyncVariableResultRejected
16
+
17
+ export class AsyncVariable<T> implements PromiseLike<T> {
18
+ #result: AsyncVariableResult<T> | undefined
19
+ #result_res!: (res: AsyncVariableResult<T>) => void
20
+ #result_p!: Promise<AsyncVariableResult<T>>
21
+ readonly #initialization: Promise<void>
22
+
23
+ get value(): T {
24
+ if (!this.#result)
25
+ throw new Error('incomplete')
26
+
27
+ if (this.#result.type === "rejected")
28
+ throw this.#result.error
29
+
30
+ return this.#result.value
31
+ }
32
+
33
+ set value(value) {
34
+ this.set(value)
35
+ }
36
+
37
+ get error() {
38
+ if (!this.#result)
39
+ throw new Error('incomplete')
40
+
41
+ if (this.#result.type === "resolved")
42
+ throw new Error(this.#result.type)
43
+
44
+ return this.#result.error
45
+ }
46
+
47
+ set error(error) {
48
+ this.reject(error)
49
+ }
50
+
51
+ get complete() {
52
+ return this.#result !== undefined
53
+ }
54
+
55
+ constructor() {
56
+ this.#initialization = new Promise(initialized =>
57
+ // prevents unhandled rejection
58
+ this.#result_p = new Promise(res => {
59
+ this.#result_res = res
60
+ initialized()
61
+ })
62
+ )
63
+ }
64
+
65
+ async init() {
66
+ await this.#initialization
67
+ }
68
+
69
+ async read() {
70
+ await this.#initialization
71
+ await this.#result_p
72
+ return this.value
73
+ }
74
+
75
+ then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined): PromiseLike<TResult1 | TResult2> {
76
+ return this.read().then(onfulfilled, onrejected)
77
+ }
78
+
79
+ async #complete(throw_if_set = true) {
80
+ await this.#initialization
81
+ if (this.complete) {
82
+ if (throw_if_set)
83
+ throw new Error('already set')
84
+ else
85
+ return false
86
+ }
87
+
88
+ return true
89
+ }
90
+
91
+ async set(value: T, throw_if_set = true) {
92
+ if (await this.#complete(throw_if_set)) {
93
+ this.#result_res(this.#result = {
94
+ type: "resolved",
95
+ value
96
+ })
97
+ }
98
+ }
99
+
100
+ async reject(error: unknown, throw_if_set = true) {
101
+ if (await this.#complete(throw_if_set)) {
102
+ this.#result_res(this.#result = {
103
+ type: "rejected",
104
+ error
105
+ })
106
+ }
107
+ }
108
+
109
+ static performCallback<R = void>(fn: (cb: (err?: unknown, res?: R) => void) => void): AsyncVariable<R> {
110
+ const av = new AsyncVariable<R>()
111
+
112
+ fn(async (err, res) => {
113
+ if (err)
114
+ await av.error(err)
115
+ else
116
+ await av.set(<R>res)
117
+ })
118
+
119
+ return av
120
+ }
121
+
122
+ perform(fn: () => Promise<T>): this {
123
+ fn().then(value => this.set(value)).catch(error => this.reject(error))
124
+
125
+ return this
126
+ }
127
+
128
+ timeout(milliseconds: number): this {
129
+ AsyncVariable.wait(milliseconds).then(() => {
130
+ if (!this.complete)
131
+ this.reject("timeout")
132
+ })
133
+
134
+ return this
135
+ }
136
+
137
+ static perform<T>(fn: () => Promise<T>): AsyncVariable<T> {
138
+ return new AsyncVariable<T>().perform(fn)
139
+ }
140
+
141
+ static wait(milliseconds: number) {
142
+ const res = new AsyncVariable<void>()
143
+ setTimeout(() => res.set(), milliseconds)
144
+ return res
145
+ }
146
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './types.js'
2
+ export * from './async-variable.js'
3
+ export * from './observable-list.js'
4
+ export * from './object.js'
5
+ export * from './lock.js'
@@ -0,0 +1,38 @@
1
+ import test from "ava"
2
+ import { Lock } from "./lock.js"
3
+
4
+ test('acquire when free', t => {
5
+ const lock = new Lock()
6
+ const context = lock.acquire()
7
+ context.release()
8
+ t.pass()
9
+ })
10
+
11
+ test('fail to acquire when already acquired', t => {
12
+ const lock = new Lock()
13
+ lock.acquire()
14
+ t.throws(() => lock.acquire(), { message: Lock.ERR_ACQUIRED })
15
+ })
16
+
17
+ test('disposable release', t => {
18
+ const lock = new Lock()
19
+
20
+ function acquire(pass: boolean) {
21
+ if (pass)
22
+ t.notThrows(() => lock.acquire())
23
+ else
24
+ t.throws(() => lock.acquire(), { message: Lock.ERR_ACQUIRED })
25
+ }
26
+
27
+ function acquire_0() {
28
+ using _context1 = lock.acquire()
29
+ acquire(false)
30
+ }
31
+
32
+ function acquire_1() {
33
+ acquire(true)
34
+ }
35
+
36
+ acquire_0()
37
+ acquire_1()
38
+ })
package/src/lock.ts ADDED
@@ -0,0 +1,50 @@
1
+ export class Lock<Of = void> {
2
+ #isAcquired = false
3
+
4
+ get isAcquired() {
5
+ return this.#isAcquired
6
+ }
7
+
8
+ acquire(of: Of): LockContext<Of> {
9
+ if (this.#isAcquired)
10
+ throw new Error(Lock.ERR_ACQUIRED)
11
+ this.#isAcquired = true
12
+ return new LockContext(this, of, this.release.bind(this))
13
+ }
14
+
15
+ protected release(_context: LockContext<Of>) {
16
+ if (!this.isAcquired)
17
+ throw new Error()
18
+
19
+ this.#isAcquired = false
20
+ }
21
+
22
+ static readonly ERR_ACQUIRED = 'lock currently acquired'
23
+ }
24
+
25
+ export class LockContext<Of> implements Disposable {
26
+ readonly #release: (conext: LockContext<Of>) => void
27
+ #isAcquired = true
28
+
29
+ constructor(
30
+ readonly lock: Lock<Of>,
31
+ readonly of: Of,
32
+ release: (conext: LockContext<Of>) => void
33
+ ) {
34
+ this.#release = release
35
+ }
36
+
37
+ release() {
38
+ if (!this.#isAcquired)
39
+ throw new Error(LockContext.ERR_LOCK_RELEASED)
40
+
41
+ this.#isAcquired = false
42
+ this.#release(this)
43
+ }
44
+
45
+ [Symbol.dispose]() {
46
+ this.release()
47
+ }
48
+
49
+ static readonly ERR_LOCK_RELEASED = "lock context already released"
50
+ }
@@ -0,0 +1,39 @@
1
+ import test from "ava"
2
+ import { replaceProperty } from "./object.js"
3
+
4
+ test("replace", t => {
5
+ interface A {
6
+ i: number
7
+ b: B
8
+ }
9
+
10
+ interface B {
11
+ j: number
12
+ c: C
13
+ }
14
+
15
+ interface C {
16
+ k: number
17
+ }
18
+
19
+ const a0: A = {
20
+ i: 0,
21
+ b: {
22
+ j: 0,
23
+ c: {
24
+ k: 0
25
+ },
26
+ }
27
+ }
28
+
29
+ t.is(a0.i, 0)
30
+
31
+ const a1 = replaceProperty(a0, ["i"], 1)
32
+
33
+ t.is(a0.i, 0)
34
+ t.is(a1.i, 1)
35
+
36
+ a0.b.j++
37
+ t.is(a0.b.j, 1)
38
+ t.is(a1.b.j, 1)
39
+ })
package/src/object.ts ADDED
@@ -0,0 +1,60 @@
1
+ export type Values<T extends object> = T[keyof T]
2
+
3
+ export type PropertyPath<T> =
4
+ | []
5
+ | T extends object ? Values<{
6
+ [K in keyof T]: [K] | [K, Values<PropertyPath<T[K]>>]
7
+ }> : []
8
+
9
+ export type PropertyType<T, Property extends PropertyPath<T>> = PropertyType_<T, Property>
10
+ type PropertyType_<T, Property extends any[], Constructed extends any[] = []> =
11
+ (Property & Constructed) extends never ?
12
+ Values<{
13
+ [K in keyof T as Property extends [...Constructed, K, ...any[]] ? K : never]:
14
+ PropertyType_<T[K], Property, [...Constructed, K]>
15
+ }> :
16
+ T
17
+
18
+ // export type PropertyType<T, Property extends PropertyPath<T> & PropertyKey[]> = PropertyType_<T, Property>
19
+ // type PropertyType_<T, Property extends PropertyKey[], Constructed extends PropertyKey[] = []> =
20
+ // (Property & Constructed) extends never ?
21
+ // Values<{
22
+ // [K in keyof T as Property extends [...Constructed, K, ...PropertyKey[]] ? K : never]:
23
+ // PropertyType_<T[K], Property, [...Constructed, K]>
24
+ // }> :
25
+ // T
26
+
27
+ // type A = {
28
+ // a: {
29
+ // a: 10,
30
+ // b: 1
31
+ // c: 2
32
+ // }
33
+ // b: {
34
+ // a: {
35
+ // c: 1
36
+ // }
37
+ // }
38
+ // }
39
+
40
+ // type a_path = PropertyPath<A>
41
+ // const ac = ["a", "c"] satisfies a_path
42
+ // // type a1 = (typeof ac) extends ["a", "c", ...PropertyKey[]] ? true : false
43
+ // type ac_type = PropertyType_<A, typeof ac>
44
+
45
+ export function replaceProperty<T, Property extends PropertyPath<T> = PropertyPath<T>>(
46
+ obj: T,
47
+ property: Property,
48
+ value: any
49
+ ): T {
50
+ if (property.length === 0)
51
+ return <T>((<any>value) ?? obj)
52
+
53
+ if (typeof obj !== 'object')
54
+ throw new Error()
55
+
56
+ const prototype = Object.create(obj)
57
+ const property0 = property[0]!
58
+ prototype[property0] = replaceProperty<any>((<any>obj)[property0]!, <any>property.slice(1), value)
59
+ return <T>prototype
60
+ }
@@ -0,0 +1,25 @@
1
+ import { ObservableList, ObservableListProtocols } from "./observable-list.js"
2
+ import test from "ava"
3
+
4
+ test("insert", t => {
5
+ const list = new ObservableList<string>()
6
+
7
+ const words = ["keyboard", "mouse", "display"]
8
+ t.plan(words.length)
9
+
10
+ let itemInserted = ""
11
+ const responder: ObservableListProtocols<string>["insert"] = (item) => t.is(item, itemInserted)
12
+
13
+ list.on("insert", responder)
14
+
15
+ list.push(itemInserted = words[0]!)
16
+ list.push(itemInserted = words[1]!)
17
+
18
+ list.off("insert", responder)
19
+
20
+ list.push("unrelated")
21
+
22
+ list.on("insert", responder)
23
+
24
+ list.push(itemInserted = words[2]!)
25
+ })
@@ -0,0 +1,114 @@
1
+ export interface ObservableListProtocols<T> {
2
+ insert(item: T, index: number): void
3
+ delete(item: T, index: number): void
4
+ reorder(item: T, index1: number): void
5
+ }
6
+
7
+ export class ObservableList<T> extends Array<T> {
8
+ constructor(length: number)
9
+ constructor(...items: T[])
10
+ constructor(...itemsOrArrayLength: any[]) {
11
+ super(...itemsOrArrayLength)
12
+ }
13
+
14
+ override push(...items: T[]): number {
15
+ const length0 = this.length
16
+ const result = super.push(...items)
17
+
18
+ items.forEach((item, i) => this.#emit("insert", item, length0 + i))
19
+
20
+ return result
21
+ }
22
+
23
+ override pop(): T | undefined {
24
+ const index = this.length - 1
25
+ const result = super.pop()
26
+
27
+ if (index >= 0)
28
+ this.#emit("delete", result!, index)
29
+
30
+ return result
31
+ }
32
+
33
+ override reverse(): T[] {
34
+ const final_index = this.length - 1
35
+ const result = super.reverse()
36
+
37
+ this.forEach((item, i) => {
38
+ const index0 = final_index - i
39
+ const index1 = i
40
+
41
+ if (index0 !== index1)
42
+ this.#emit("reorder", item, index1)
43
+ })
44
+
45
+ return result
46
+ }
47
+
48
+ override sort(compareFn?: ((a: T, b: T) => number) | undefined): this {
49
+ const result = super.sort(compareFn)
50
+
51
+ this.forEach((item, i) => this.#emit("reorder", item, i))
52
+
53
+ return result
54
+ }
55
+
56
+ override shift(): T | undefined {
57
+ const length0 = this.length
58
+ const result = super.shift()
59
+
60
+ if (length0 > 0)
61
+ this.#emit("delete", result!, 0)
62
+
63
+ this.forEach((item, i) => this.#emit("reorder", item, i))
64
+
65
+ return result
66
+ }
67
+
68
+ override splice(start: number, deleteCount = 0, ...items: T[]): T[] {
69
+ const deleted = super.splice(start, deleteCount, ...items)
70
+
71
+ deleted.forEach((item, i) => this.#emit("delete", item, start + i))
72
+
73
+ if ((deleteCount ?? 0) !== (items?.length ?? 0))
74
+ for (let i = start + (items?.length ?? 0); i < this.length; i++)
75
+ this.#emit("reorder", this[i]!, i)
76
+
77
+ items.forEach((item, i) => this.#emit("insert", item, start + i))
78
+
79
+ return deleted
80
+ }
81
+
82
+ override unshift(...items: T[]): number {
83
+ const result = super.unshift(...items)
84
+
85
+ for (let i = items.length; i < this.length; i++)
86
+ this.#emit("reorder", this[i]!, i)
87
+
88
+ items.forEach((item, i) => this.#emit("insert", item, i))
89
+
90
+ return result
91
+ }
92
+
93
+ #responders: { [Protocol in keyof ObservableListProtocols<T>]: Set<ObservableListProtocols<T>[Protocol]> } = {
94
+ insert: new Set(),
95
+ delete: new Set(),
96
+ reorder: new Set(),
97
+ }
98
+
99
+ #emit<Protocol extends keyof ObservableListProtocols<T>>(protocol: Protocol, ...parameters: Parameters<ObservableListProtocols<T>[Protocol]>): ReturnType<ObservableListProtocols<T>[Protocol]>[] {
100
+ return [...this.#responders[protocol]].map(responder => (<any>responder)(...parameters))
101
+ }
102
+
103
+ on<Protocol extends keyof ObservableListProtocols<T>>(protocol: Protocol, responder: ObservableListProtocols<T>[Protocol]) {
104
+ this.#responders[protocol].add(responder)
105
+ }
106
+
107
+ off<Protocol extends keyof ObservableListProtocols<T>>(protocol: Protocol, responder: ObservableListProtocols<T>[Protocol]) {
108
+ this.#responders[protocol].delete(responder)
109
+ }
110
+
111
+ responders<Protocol extends keyof ObservableListProtocols<T>>(protocol: Protocol) {
112
+ return this.#responders[protocol]
113
+ }
114
+ }
package/src/types.ts ADDED
@@ -0,0 +1,21 @@
1
+ export type Deleteable<T, K extends keyof T = keyof T> = {
2
+ -readonly [K1 in K]?: T[K1]
3
+ }
4
+
5
+ export type PartlyDeleteable<T, K extends keyof T> = {
6
+ [K1 in keyof T as (K1 extends K ? never : K1)]: T[K1]
7
+ } & Deleteable<T, K>
8
+
9
+ export type UndefinedIf<T, Condition extends boolean = boolean> = Condition extends true ? undefined : T
10
+
11
+ export function undefinedIf<T, Condition extends boolean = boolean>(condition: Condition, item: () => T): UndefinedIf<T, Condition> {
12
+ return <UndefinedIf<T, Condition>>(condition ? undefined : item())
13
+ }
14
+
15
+ export type Prefixed<Prefix extends string, T extends object> = {
16
+ [K in string & keyof T as `${Prefix}${K}`]: T[K]
17
+ }
18
+
19
+ export function prefixed<Prefix extends string, T extends object>(prefix: Prefix, o: T): Prefixed<Prefix, T> {
20
+ return <Prefixed<Prefix, T>>Object.fromEntries(Object.entries(o).map(([k, v]) => [`${prefix}${k}`, v] as const))
21
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@code-essentials/tsconfig/tsconfig.debug.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ },
7
+ "include": ["src", "src/**/*.spec.ts"],
8
+ "exclude": ["node_modules"],
9
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@code-essentials/tsconfig/tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["node_modules", "src/**/*.spec.ts"],
9
+ }