@jvs-milkdown/ctx 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/LICENSE +21 -0
- package/README.md +11 -0
- package/lib/context/container.d.ts +9 -0
- package/lib/context/container.d.ts.map +1 -0
- package/lib/context/index.d.ts +3 -0
- package/lib/context/index.d.ts.map +1 -0
- package/lib/context/slice.d.ts +23 -0
- package/lib/context/slice.d.ts.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +300 -0
- package/lib/index.js.map +1 -0
- package/lib/inspector/index.d.ts +3 -0
- package/lib/inspector/index.d.ts.map +1 -0
- package/lib/inspector/inspector.d.ts +37 -0
- package/lib/inspector/inspector.d.ts.map +1 -0
- package/lib/inspector/meta.d.ts +8 -0
- package/lib/inspector/meta.d.ts.map +1 -0
- package/lib/plugin/ctx.d.ts +26 -0
- package/lib/plugin/ctx.d.ts.map +1 -0
- package/lib/plugin/index.d.ts +3 -0
- package/lib/plugin/index.d.ts.map +1 -0
- package/lib/plugin/types.d.ts +9 -0
- package/lib/plugin/types.d.ts.map +1 -0
- package/lib/timer/clock.d.ts +9 -0
- package/lib/timer/clock.d.ts.map +1 -0
- package/lib/timer/index.d.ts +3 -0
- package/lib/timer/index.d.ts.map +1 -0
- package/lib/timer/timer.d.ts +19 -0
- package/lib/timer/timer.d.ts.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +39 -0
- package/src/context/container.spec.ts +54 -0
- package/src/context/container.ts +48 -0
- package/src/context/index.ts +2 -0
- package/src/context/slice.spec.ts +116 -0
- package/src/context/slice.ts +108 -0
- package/src/index.ts +4 -0
- package/src/inspector/index.ts +2 -0
- package/src/inspector/inspector.ts +124 -0
- package/src/inspector/meta.ts +17 -0
- package/src/plugin/ctx.spec.ts +110 -0
- package/src/plugin/ctx.ts +136 -0
- package/src/plugin/index.ts +2 -0
- package/src/plugin/types.ts +43 -0
- package/src/timer/clock.spec.ts +27 -0
- package/src/timer/clock.ts +29 -0
- package/src/timer/index.ts +2 -0
- package/src/timer/timer.spec.ts +29 -0
- package/src/timer/timer.ts +109 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Container, Slice, SliceType } from '../context'
|
|
2
|
+
import type { Meta } from '../inspector'
|
|
3
|
+
import type { Clock, TimerType } from '../timer'
|
|
4
|
+
|
|
5
|
+
import { Inspector } from '../inspector'
|
|
6
|
+
|
|
7
|
+
/// The ctx object that can be accessed in plugin and action.
|
|
8
|
+
export class Ctx {
|
|
9
|
+
/// @internal
|
|
10
|
+
readonly #container: Container
|
|
11
|
+
/// @internal
|
|
12
|
+
readonly #clock: Clock
|
|
13
|
+
/// @internal
|
|
14
|
+
readonly #meta?: Meta
|
|
15
|
+
/// @internal
|
|
16
|
+
readonly #inspector?: Inspector
|
|
17
|
+
|
|
18
|
+
/// Create a ctx object with container and clock.
|
|
19
|
+
constructor(container: Container, clock: Clock, meta?: Meta) {
|
|
20
|
+
this.#container = container
|
|
21
|
+
this.#clock = clock
|
|
22
|
+
this.#meta = meta
|
|
23
|
+
if (meta) this.#inspector = new Inspector(container, clock, meta)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Get metadata of the ctx.
|
|
27
|
+
get meta() {
|
|
28
|
+
return this.#meta
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Get the inspector of the ctx.
|
|
32
|
+
get inspector() {
|
|
33
|
+
return this.#inspector
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Produce a new ctx with metadata.
|
|
37
|
+
/// The new ctx will link to the same container and clock with the current ctx.
|
|
38
|
+
/// If the metadata is empty, it will return the current ctx.
|
|
39
|
+
readonly produce = (meta?: Meta) => {
|
|
40
|
+
if (meta && Object.keys(meta).length)
|
|
41
|
+
return new Ctx(this.#container, this.#clock, { ...meta })
|
|
42
|
+
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Add a slice into the ctx.
|
|
47
|
+
readonly inject = <T>(sliceType: SliceType<T>, value?: T) => {
|
|
48
|
+
const slice = sliceType.create(this.#container.sliceMap)
|
|
49
|
+
if (value != null) slice.set(value)
|
|
50
|
+
|
|
51
|
+
this.#inspector?.onInject(sliceType)
|
|
52
|
+
|
|
53
|
+
return this
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Remove a slice from the ctx.
|
|
57
|
+
readonly remove = <T, N extends string = string>(
|
|
58
|
+
sliceType: SliceType<T, N> | N
|
|
59
|
+
) => {
|
|
60
|
+
this.#container.remove(sliceType)
|
|
61
|
+
this.#inspector?.onRemove(sliceType)
|
|
62
|
+
return this
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Add a timer into the ctx.
|
|
66
|
+
readonly record = (timerType: TimerType) => {
|
|
67
|
+
timerType.create(this.#clock.store)
|
|
68
|
+
this.#inspector?.onRecord(timerType)
|
|
69
|
+
return this
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Remove a timer from the ctx.
|
|
73
|
+
readonly clearTimer = (timerType: TimerType) => {
|
|
74
|
+
this.#clock.remove(timerType)
|
|
75
|
+
this.#inspector?.onClear(timerType)
|
|
76
|
+
return this
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Check if the ctx has a slice.
|
|
80
|
+
readonly isInjected = <T, N extends string = string>(
|
|
81
|
+
sliceType: SliceType<T, N> | N
|
|
82
|
+
) => this.#container.has(sliceType)
|
|
83
|
+
|
|
84
|
+
/// Check if the ctx has a timer.
|
|
85
|
+
readonly isRecorded = (timerType: TimerType) => this.#clock.has(timerType)
|
|
86
|
+
|
|
87
|
+
/// Get a slice from the ctx.
|
|
88
|
+
readonly use = <T, N extends string = string>(
|
|
89
|
+
sliceType: SliceType<T, N> | N
|
|
90
|
+
): Slice<T, N> => {
|
|
91
|
+
this.#inspector?.onUse(sliceType)
|
|
92
|
+
return this.#container.get(sliceType)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Get a slice value from the ctx.
|
|
96
|
+
readonly get = <T, N extends string>(sliceType: SliceType<T, N> | N) =>
|
|
97
|
+
this.use(sliceType).get()
|
|
98
|
+
|
|
99
|
+
/// Get a slice value from the ctx.
|
|
100
|
+
readonly set = <T, N extends string>(
|
|
101
|
+
sliceType: SliceType<T, N> | N,
|
|
102
|
+
value: T
|
|
103
|
+
) => this.use(sliceType).set(value)
|
|
104
|
+
|
|
105
|
+
/// Update a slice value from the ctx by a callback.
|
|
106
|
+
readonly update = <T, N extends string>(
|
|
107
|
+
sliceType: SliceType<T, N> | N,
|
|
108
|
+
updater: (prev: T) => T
|
|
109
|
+
) => this.use(sliceType).update(updater)
|
|
110
|
+
|
|
111
|
+
/// Get a timer from the ctx.
|
|
112
|
+
readonly timer = (timer: TimerType) => this.#clock.get(timer)
|
|
113
|
+
|
|
114
|
+
/// Resolve a timer from the ctx.
|
|
115
|
+
readonly done = (timer: TimerType) => {
|
|
116
|
+
this.timer(timer).done()
|
|
117
|
+
this.#inspector?.onDone(timer)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Start a timer from the ctx.
|
|
121
|
+
readonly wait = (timer: TimerType) => {
|
|
122
|
+
const promise = this.timer(timer).start()
|
|
123
|
+
this.#inspector?.onWait(timer, promise)
|
|
124
|
+
return promise
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Start a list of timers from the ctx, the list is stored in a slice in the ctx.
|
|
128
|
+
/// This is equivalent to
|
|
129
|
+
///
|
|
130
|
+
/// ```typescript
|
|
131
|
+
/// Promise.all(ctx.get(slice).map(x => ctx.wait(x))).
|
|
132
|
+
/// ```
|
|
133
|
+
readonly waitTimers = async (slice: SliceType<TimerType[]>) => {
|
|
134
|
+
await Promise.all(this.get(slice).map((x) => this.wait(x)))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Meta } from '../inspector'
|
|
2
|
+
import type { Ctx } from './ctx'
|
|
3
|
+
|
|
4
|
+
/// @internal
|
|
5
|
+
export type Cleanup = () => void | Promise<void>
|
|
6
|
+
|
|
7
|
+
/// @internal
|
|
8
|
+
export type RunnerReturnType = void | Promise<void> | Cleanup | Promise<Cleanup>
|
|
9
|
+
|
|
10
|
+
/// @internal
|
|
11
|
+
export type CtxRunner = () => RunnerReturnType
|
|
12
|
+
|
|
13
|
+
/// The type of the plugin.
|
|
14
|
+
///
|
|
15
|
+
/// ```typescript
|
|
16
|
+
/// // A full plugin example
|
|
17
|
+
/// const plugin1 = (ctx: Ctx) => {
|
|
18
|
+
/// // setup
|
|
19
|
+
/// return async () => {
|
|
20
|
+
/// // run
|
|
21
|
+
/// return async () => {
|
|
22
|
+
/// // cleanup
|
|
23
|
+
/// }
|
|
24
|
+
/// }
|
|
25
|
+
/// }
|
|
26
|
+
///
|
|
27
|
+
/// // A plugin doesn't need to return a cleanup function
|
|
28
|
+
/// const plugin2 = (ctx: Ctx) => {
|
|
29
|
+
/// // setup
|
|
30
|
+
/// return async () => {
|
|
31
|
+
/// // run
|
|
32
|
+
/// }
|
|
33
|
+
/// }
|
|
34
|
+
///
|
|
35
|
+
/// // A plugin doesn't need to be async
|
|
36
|
+
/// const plugin3 = (ctx: Ctx) => {
|
|
37
|
+
/// // setup
|
|
38
|
+
/// return () => {
|
|
39
|
+
/// // run
|
|
40
|
+
/// }
|
|
41
|
+
/// }
|
|
42
|
+
/// ```
|
|
43
|
+
export type MilkdownPlugin = { meta?: Meta } & ((ctx: Ctx) => CtxRunner)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { Clock } from './clock'
|
|
4
|
+
import { TimerType } from './timer'
|
|
5
|
+
|
|
6
|
+
describe('timing/clock', () => {
|
|
7
|
+
it('createClock', () => {
|
|
8
|
+
const clock = new Clock()
|
|
9
|
+
const timer = new TimerType('timer')
|
|
10
|
+
const timerNotRegistered = new TimerType('not')
|
|
11
|
+
|
|
12
|
+
timer.create(clock.store)
|
|
13
|
+
|
|
14
|
+
expect(clock.get(timer)).toBe(clock.store.get(timer.id))
|
|
15
|
+
expect(() => clock.get(timerNotRegistered)).toThrow()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('remove', () => {
|
|
19
|
+
const clock = new Clock()
|
|
20
|
+
const timer = new TimerType('timer')
|
|
21
|
+
timer.create(clock.store)
|
|
22
|
+
expect(clock.get(timer)).toBe(clock.store.get(timer.id))
|
|
23
|
+
|
|
24
|
+
clock.remove(timer)
|
|
25
|
+
expect(() => clock.get(timer)).toThrow()
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { timerNotFound } from '@jvs-milkdown/exception'
|
|
2
|
+
|
|
3
|
+
import type { Timer, TimerType } from './timer'
|
|
4
|
+
|
|
5
|
+
/// @internal
|
|
6
|
+
export type TimerMap = Map<symbol, Timer>
|
|
7
|
+
|
|
8
|
+
/// Container is a map of timers.
|
|
9
|
+
export class Clock {
|
|
10
|
+
/// @internal
|
|
11
|
+
readonly store: TimerMap = new Map()
|
|
12
|
+
|
|
13
|
+
/// Get a timer from the clock by timer type.
|
|
14
|
+
get = (timer: TimerType) => {
|
|
15
|
+
const meta = this.store.get(timer.id)
|
|
16
|
+
if (!meta) throw timerNotFound(timer.name)
|
|
17
|
+
return meta
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Remove a timer from the clock by timer type.
|
|
21
|
+
remove = (timer: TimerType) => {
|
|
22
|
+
this.store.delete(timer.id)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check if the clock has a timer by timer type.
|
|
26
|
+
has = (timer: TimerType) => {
|
|
27
|
+
return this.store.has(timer.id)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { TimerType } from './timer'
|
|
4
|
+
|
|
5
|
+
describe('timing/timing', () => {
|
|
6
|
+
it('createTimer', async () => {
|
|
7
|
+
const timerType = new TimerType('timer')
|
|
8
|
+
const map = new Map()
|
|
9
|
+
const timer = timerType.create(map)
|
|
10
|
+
setTimeout(() => {
|
|
11
|
+
timer.done()
|
|
12
|
+
}, 10)
|
|
13
|
+
|
|
14
|
+
await expect(timer.start()).resolves.toBeUndefined()
|
|
15
|
+
|
|
16
|
+
// should still can be waited after it's resolved
|
|
17
|
+
await expect(timer.start()).resolves.toBeUndefined()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('timeout', async () => {
|
|
21
|
+
const timerType = new TimerType('timer', 10)
|
|
22
|
+
const map = new Map()
|
|
23
|
+
const timer = timerType.create(map)
|
|
24
|
+
|
|
25
|
+
await expect(timer.start()).rejects.toStrictEqual(
|
|
26
|
+
new Error('Timing timer timeout.')
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { TimerMap } from './clock'
|
|
2
|
+
|
|
3
|
+
export type TimerStatus = 'pending' | 'resolved' | 'rejected'
|
|
4
|
+
|
|
5
|
+
/// Timer is a promise that can be resolved by calling done.
|
|
6
|
+
export class Timer {
|
|
7
|
+
/// The type of the timer.
|
|
8
|
+
readonly type: TimerType
|
|
9
|
+
|
|
10
|
+
/// @internal
|
|
11
|
+
#promise: Promise<void> | null = null
|
|
12
|
+
/// @internal
|
|
13
|
+
#listener: EventListener | null = null
|
|
14
|
+
/// @internal
|
|
15
|
+
readonly #eventUniqId: symbol
|
|
16
|
+
/// @internal
|
|
17
|
+
#status: TimerStatus = 'pending'
|
|
18
|
+
|
|
19
|
+
/// @internal
|
|
20
|
+
constructor(clock: TimerMap, type: TimerType) {
|
|
21
|
+
this.#eventUniqId = Symbol(type.name)
|
|
22
|
+
this.type = type
|
|
23
|
+
clock.set(type.id, this)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// The status of the timer.
|
|
27
|
+
/// Can be `pending`, `resolved` or `rejected`.
|
|
28
|
+
get status() {
|
|
29
|
+
return this.#status
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Start the timer, which will return a promise.
|
|
33
|
+
/// If the timer is already started, it will return the same promise.
|
|
34
|
+
/// If the timer is not resolved in the timeout, it will reject the promise.
|
|
35
|
+
start = () => {
|
|
36
|
+
this.#promise ??= new Promise((resolve, reject) => {
|
|
37
|
+
this.#listener = (e: Event) => {
|
|
38
|
+
if (!(e instanceof CustomEvent)) return
|
|
39
|
+
|
|
40
|
+
if (e.detail.id === this.#eventUniqId) {
|
|
41
|
+
this.#status = 'resolved'
|
|
42
|
+
this.#removeListener()
|
|
43
|
+
e.stopImmediatePropagation()
|
|
44
|
+
resolve()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.#waitTimeout(() => {
|
|
49
|
+
if (this.#status === 'pending') this.#status = 'rejected'
|
|
50
|
+
|
|
51
|
+
this.#removeListener()
|
|
52
|
+
reject(new Error(`Timing ${this.type.name} timeout.`))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
this.#status = 'pending'
|
|
56
|
+
addEventListener(this.type.name, this.#listener)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return this.#promise
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Resolve the timer.
|
|
63
|
+
done = () => {
|
|
64
|
+
const event = new CustomEvent(this.type.name, {
|
|
65
|
+
detail: { id: this.#eventUniqId },
|
|
66
|
+
})
|
|
67
|
+
dispatchEvent(event)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// @internal
|
|
71
|
+
#removeListener = () => {
|
|
72
|
+
if (this.#listener) removeEventListener(this.type.name, this.#listener)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// @internal
|
|
76
|
+
#waitTimeout = (ifTimeout: () => void) => {
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
ifTimeout()
|
|
79
|
+
}, this.type.timeout)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Timer type can be used to create timers in different clocks.
|
|
84
|
+
export class TimerType {
|
|
85
|
+
/// The unique id of the timer type.
|
|
86
|
+
readonly id: symbol
|
|
87
|
+
/// The name of the timer type.
|
|
88
|
+
readonly name: string
|
|
89
|
+
/// The timeout of the timer type.
|
|
90
|
+
readonly timeout: number
|
|
91
|
+
|
|
92
|
+
/// Create a timer type with a name and a timeout.
|
|
93
|
+
/// The name should be unique in the clock.
|
|
94
|
+
constructor(name: string, timeout = 3000) {
|
|
95
|
+
this.id = Symbol(`Timer-${name}`)
|
|
96
|
+
this.name = name
|
|
97
|
+
this.timeout = timeout
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Create a timer with a clock.
|
|
101
|
+
create = (clock: TimerMap): Timer => {
|
|
102
|
+
return new Timer(clock, this)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Create a timer type with a name and a timeout.
|
|
107
|
+
/// This is equivalent to `new TimerType(name, timeout)`.
|
|
108
|
+
export const createTimer = (name: string, timeout = 3000) =>
|
|
109
|
+
new TimerType(name, timeout)
|