@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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/lib/context/container.d.ts +9 -0
  4. package/lib/context/container.d.ts.map +1 -0
  5. package/lib/context/index.d.ts +3 -0
  6. package/lib/context/index.d.ts.map +1 -0
  7. package/lib/context/slice.d.ts +23 -0
  8. package/lib/context/slice.d.ts.map +1 -0
  9. package/lib/index.d.ts +5 -0
  10. package/lib/index.d.ts.map +1 -0
  11. package/lib/index.js +300 -0
  12. package/lib/index.js.map +1 -0
  13. package/lib/inspector/index.d.ts +3 -0
  14. package/lib/inspector/index.d.ts.map +1 -0
  15. package/lib/inspector/inspector.d.ts +37 -0
  16. package/lib/inspector/inspector.d.ts.map +1 -0
  17. package/lib/inspector/meta.d.ts +8 -0
  18. package/lib/inspector/meta.d.ts.map +1 -0
  19. package/lib/plugin/ctx.d.ts +26 -0
  20. package/lib/plugin/ctx.d.ts.map +1 -0
  21. package/lib/plugin/index.d.ts +3 -0
  22. package/lib/plugin/index.d.ts.map +1 -0
  23. package/lib/plugin/types.d.ts +9 -0
  24. package/lib/plugin/types.d.ts.map +1 -0
  25. package/lib/timer/clock.d.ts +9 -0
  26. package/lib/timer/clock.d.ts.map +1 -0
  27. package/lib/timer/index.d.ts +3 -0
  28. package/lib/timer/index.d.ts.map +1 -0
  29. package/lib/timer/timer.d.ts +19 -0
  30. package/lib/timer/timer.d.ts.map +1 -0
  31. package/lib/tsconfig.tsbuildinfo +1 -0
  32. package/package.json +39 -0
  33. package/src/context/container.spec.ts +54 -0
  34. package/src/context/container.ts +48 -0
  35. package/src/context/index.ts +2 -0
  36. package/src/context/slice.spec.ts +116 -0
  37. package/src/context/slice.ts +108 -0
  38. package/src/index.ts +4 -0
  39. package/src/inspector/index.ts +2 -0
  40. package/src/inspector/inspector.ts +124 -0
  41. package/src/inspector/meta.ts +17 -0
  42. package/src/plugin/ctx.spec.ts +110 -0
  43. package/src/plugin/ctx.ts +136 -0
  44. package/src/plugin/index.ts +2 -0
  45. package/src/plugin/types.ts +43 -0
  46. package/src/timer/clock.spec.ts +27 -0
  47. package/src/timer/clock.ts +29 -0
  48. package/src/timer/index.ts +2 -0
  49. package/src/timer/timer.spec.ts +29 -0
  50. 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,2 @@
1
+ export * from './ctx'
2
+ export * from './types'
@@ -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,2 @@
1
+ export * from './clock'
2
+ export * from './timer'
@@ -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)