@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.
- package/.vscode/launch.json +21 -0
- package/.vscode/tasks.json +22 -0
- package/package.json +44 -0
- package/src/async-variable.ts +146 -0
- package/src/index.ts +5 -0
- package/src/lock.spec.ts +38 -0
- package/src/lock.ts +50 -0
- package/src/object.spec.ts +39 -0
- package/src/object.ts +60 -0
- package/src/observable-list.spec.ts +25 -0
- package/src/observable-list.ts +114 -0
- package/src/types.ts +21 -0
- package/tsconfig.debug.json +9 -0
- package/tsconfig.json +9 -0
|
@@ -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
package/src/lock.spec.ts
ADDED
|
@@ -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
|
+
}
|