refactor(danmuku): [PKG-DANMUKU-06] migrate runtime and preserve public types

This commit is contained in:
Harvey Zhao committed 2026-09-13 22:06:00 +08:00
1 parent abc8008da7
commit 24b883d862
64 files changed
+14020 -633

No files matched your search

+12
View File
@@ -70,3 +70,15 @@ volume/rate, independent instances and cleanup. Published-only tests retain defe
See [audio validation](../refactor/audio-validation.md) for actual media tests and limitations.
Document PiP: `yarn test:dpip` runs 48 historical window/DOM/lifecycle cases. Native DOM iframe checks use `yarn test:browser test/browser/dpip.spec.js`; controlled window APIs do not establish native Document PiP support. See [validation notes](../refactor/dpip-validation.md).
Danmuku: `yarn test:danmuku` covers historical behavior and current input, scheduling,
settings, rendering and heatmap boundaries. `ARTPLAYER_DANMUKU_ARTIFACT` selects a
built candidate for integration cases; direct internal tests remain source tests.
The candidate Worker helper executes the selected artifact's Blob or data URL bytes;
only source-mode controlled imports compile `worker.ts` separately. The frozen
published helper remains unchanged. `yarn test:danmuku-types` checks the unchanged
npm root, the optional accurate `/runtime` entry, implementation assignability and
semantic editor declarations. `yarn test:danmuku-types-package` packs and installs
the packages outside the workspace, verifies exact member bytes and frozen offline
reinstallation, and checks historical and current compiler consumers. These type
checks do not replace the native browser suite or final distribution acceptance.
+11
View File
@@ -128,3 +128,14 @@ both response modes. A passing diagnostic only proves observation plus successfu
playback after release; inspect its progressed/waiting flags before claiming buffering.
These capability observation timeouts do not relax the integration assertions.
Archive the report and results (including traces) before starting another browser suite.
Danmuku input, scheduler, resources and heatmap-density specs accept
`ARTPLAYER_DANMUKU_ARTIFACT` for a built main/legacy file; unset it to compile the
current JS/TS entry. Run each format sequentially and wait for the command's final
exit before copying reports or launching the next format. A visible last test line
does not prove reporter/trace teardown is finished. These runs share the server port
and output directory, so simultaneous invocations can corrupt failure evidence.
For a timeout, inspect the retained trace before changing the test or implementation;
browser startup and pre-plugin playback delays are different from plugin execution.
The density regression retains 16000 rows, real playback and SVG/pixel geometry at
two widths. A diagnostic pass does not waive the full matrix or long-load acceptance.
+3 -1
View File
@@ -2,7 +2,9 @@ import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Deterministic owned-node tests use the Node runner.
import test from 'node:test'
import { parseHTML } from 'linkedom'
import Renderer from '../packages/artplayer-plugin-danmuku/src/renderer.js'
import { loadModules } from './helpers/load.js'
const { Renderer } = await loadModules({ Renderer: 'packages/artplayer-plugin-danmuku/src/renderer' })
function fixture(t) {
const { document } = parseHTML('<html><body><div id="layer"><i id="foreign"></i></div></body></html>')
+2 -6
View File
@@ -1,9 +1,8 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Deterministic candidate scheduling uses the Node runner.
import test from 'node:test'
import vm from 'node:vm'
import { danmukuCandidate, danmukuCandidateEnvironment } from './helpers/danmuku-candidate.js'
import { danmukuCandidate, danmukuCandidateEnvironment, danmukuWorkerCode } from './helpers/danmuku-candidate.js'
import { deferred } from './helpers/danmuku.js'
const implementation = await danmukuCandidate()
@@ -44,10 +43,7 @@ async function fixture(options = {}, { automatic = false } = {}) {
return child
}
const worker = env.workers[0]
const actualBlob = env.urls.get(worker.url)
const workerCode = actualBlob
? await actualBlob.text()
: fs.readFileSync(new URL('../packages/artplayer-plugin-danmuku/src/worker.js', import.meta.url), 'utf8')
const workerCode = await danmukuWorkerCode(implementation, env, worker)
const responses = []
const workerContext = vm.createContext({ postMessage: data => responses.push(structuredClone(data)), URL: { revokeObjectURL() {} }, location: { href: worker.url } })
workerContext.self = workerContext
+10 -6
View File
@@ -1,9 +1,8 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Controlled client faults and real worker geometry use the Node runner.
import test from 'node:test'
import vm from 'node:vm'
import { danmukuCandidate, danmukuCandidateEnvironment } from './helpers/danmuku-candidate.js'
import { danmukuCandidate, danmukuCandidateEnvironment, danmukuWorkerCode } from './helpers/danmuku-candidate.js'
import { loadModules } from './helpers/load.js'
const { WorkerClient } = await loadModules({ WorkerClient: 'packages/artplayer-plugin-danmuku/src/worker-client' })
@@ -223,13 +222,18 @@ test('danmuku worker client: independent clients never share pending ownership',
second.client.dispose()
})
// Unlike the client double above, this executes the actual unmodified worker
// script. Numeric geometry inputs are controlled; no browser layout is claimed.
const workerCode = fs.readFileSync(new URL('../packages/artplayer-plugin-danmuku/src/worker.js', import.meta.url), 'utf8')
// Unlike the client double above, this executes the actual source or artifact
// worker. Numeric geometry inputs are controlled; no browser layout is claimed.
const workerEnvironment = danmukuCandidateEnvironment(implementation)
workerEnvironment.factory({ danmuku: [] })(workerEnvironment.art)
const actualWorker = workerEnvironment.workers[0]
const workerCode = await danmukuWorkerCode(implementation, workerEnvironment, actualWorker)
workerEnvironment.destroy()
function calculateGeometry(input) {
const replies = []
const context = vm.createContext({ input, postMessage: data => replies.push(data) })
const context = vm.createContext({ input, postMessage: data => replies.push(data), URL: { revokeObjectURL() {} }, location: { href: actualWorker.url } })
context.self = context
vm.runInContext(workerCode, context, { timeout: 1000 })
vm.runInContext('onmessage({ data: input })', context, { timeout: 1000 })
assert.equal(replies.length, 1)
+24
View File
@@ -1,3 +1,4 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
@@ -6,8 +7,31 @@ import { build } from 'esbuild'
import { ensureArchive, readMember, refactorDir } from '../../refactor/scripts/releases.mjs'
import { getEntryFile } from '../../scripts/projects.js'
import { danmukuEnvironment } from './danmuku.js'
import { resolveSource } from './load.js'
const workspace = fileURLToPath(new URL('../../', import.meta.url))
let sourceWorker
export async function danmukuWorkerCode(implementation, env, worker) {
const blob = env.urls.get(worker.url)
if (blob)
return blob.text()
// Production worker-loader emits a percent-encoded data URL. Execute those
// exact shipped bytes instead of silently substituting the current source.
const dataPrefix = 'data:text/javascript;charset=utf-8,'
if (typeof worker.url === 'string' && worker.url.startsWith(dataPrefix))
return decodeURIComponent(worker.url.slice(dataPrefix.length))
assert.equal(implementation.name, 'candidate-source', 'Artifact Worker tests must execute the Worker embedded in that artifact')
sourceWorker ??= build({
entryPoints: [resolveSource('packages/artplayer-plugin-danmuku/src/worker')],
bundle: true,
write: false,
format: 'iife',
platform: 'browser',
target: 'es2020',
}).then(result => result.outputFiles[0].text)
return sourceWorker
}
export async function danmukuCandidate() {
const coreRelease = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8')).releases.find(item => item.name === 'artplayer')
+22
View File
@@ -0,0 +1,22 @@
import type { Option, Result } from 'artplayer-plugin-danmuku'
import danmuku from 'artplayer-plugin-danmuku'
import legacy from 'artplayer-plugin-danmuku/legacy'
const legacyFactory: typeof danmuku = legacy
const rootFactory: typeof legacy = danmuku
const option: Option = { danmuku: [], points: [{ time: 1, value: 2 }] }
danmuku(option)
legacy(option)
declare const result: Result
const synchronous: Result = result.emit({ text: 'historical declaration' })
void [legacyFactory, rootFactory, synchronous]
// @ts-expect-error The published root requires its option argument.
danmuku()
// @ts-expect-error The published root requires danmuku in Option.
danmuku({})
// @ts-expect-error Preserve historical object points at the root.
danmuku({ danmuku: [], points: [[1, 2]] })
// @ts-expect-error Root emit keeps its published synchronous Result shape.
const asynchronous: Promise<Result> = result.emit({ text: 'old emit' })
void asynchronous
+108
View File
@@ -0,0 +1,108 @@
import type { Danmu, EventMap, Icons, Input, Item, NormalizedDanmu, NormalizedOption, Owner, Point, RuntimeFactory, RuntimeOption, RuntimeResult, Slider } from 'artplayer-plugin-danmuku/runtime'
import Artplayer from 'artplayer'
import runtime from 'artplayer-plugin-danmuku/runtime'
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
type Assert<T extends true> = T
type FactoryArguments = Assert<Equal<Parameters<typeof runtime>, [option: RuntimeOption]>>
type FactoryReturns = Assert<Equal<ReturnType<ReturnType<typeof runtime>>, RuntimeResult>>
type EmitReturns = Assert<Equal<ReturnType<RuntimeResult['emit']>, Promise<Owner>>>
type LoadReturns = Assert<Equal<ReturnType<RuntimeResult['load']>, Promise<Owner>>>
type CommandReturns = Assert<Equal<ReturnType<RuntimeResult['config']>, Owner>>
type OwnerHasNoFacadeKeys = Assert<Equal<Extract<'name' | 'mount', keyof Owner>, never>>
type VisiblePayload = Assert<Equal<EventMap['artplayerPluginDanmuku:visible'], [item: Item]>>
type LoadedPayload = Assert<Equal<EventMap['artplayerPluginDanmuku:loaded'], [queue: Item[]]>>
function expectOption(_option: NormalizedOption) {}
const row: Danmu = { text: 'extended input', id: 'message-1', author: { name: 'test' }, style: { fontWeight: 'bold' } }
const input: Input = () => [row]
const asyncInput: Input = async () => [row]
const point: Point = [10, 5]
const margin: Slider<[number | string, number | string]> = { steps: [{ name: 'half', value: [10, '50%'], hide: true, show: false }] }
const options: RuntimeOption = {
danmuku: input,
points: [point],
MARGIN: margin,
SPEED: { steps: [{ name: 'fast', value: 2.5, hide: true }] },
filter(danmu) {
expectOption(this)
const normalized: NormalizedDanmu = danmu
const time: number = danmu.time
const extension: unknown = danmu.author
// @ts-expect-error Queue allocation has not run when filter is invoked.
const state: string = danmu.$state
void [normalized, time, extension, state]
return true
},
async beforeEmit(danmu) {
expectOption(this)
const text: string = danmu.text
void text
return true
},
async beforeVisible(item) {
expectOption(this)
const state: 'wait' | 'ready' | 'emit' | 'stop' = item.$state
const index: number = item.$index
const node: HTMLDivElement | null = item.$ref
const extra: unknown = item.author
void [state, index, node, extra]
return true
},
}
const art = new Artplayer({ container: '#player', url: '/video.mp4', plugins: [runtime(options)] })
const factory: RuntimeFactory = runtime
const result = runtime({})(art)
const registered: 'artplayerPluginDanmuku' = result.name
const icon: string = runtime.icons.$check_on
const icons: Icons = runtime.icons
runtime.icons = icons
runtime.icons.$on = '<svg></svg>'
const promise: Promise<Owner> = result.emit(row)
const loading: Promise<Owner> = result.load(asyncInput)
const replacing: Promise<Owner> = result.load()
const owner: Owner = result.config({ opacity: 0.5 })
const stopped: Owner = owner.stop().start().suspend().continue().reset()
const configured: NormalizedOption = result.option
result.option.visible = false
result.mount(document.createElement('div'))
result.mount('#danmuku-mount')
const loadedPayload: EventMap['artplayerPluginDanmuku:loaded'] = [owner.queue]
const pointsPayload: EventMap['artplayerPluginDanmuku:points'] = [[point]]
const failedPayload: EventMap['artplayerPluginDanmuku:error'] = [new Error('failed')]
async function consume() {
const emitted = await result.emit({ text: 'await owner' })
const rows: Item[] = emitted.queue
const next: Owner = emitted.hide().show()
// @ts-expect-error The resolved owner does not expose the facade name.
void emitted.name
// @ts-expect-error The resolved owner does not own setting.mount.
emitted.mount('#mount')
return [rows, next]
}
// @ts-expect-error The factory still requires its options object.
runtime()
// @ts-expect-error Undefined is not a valid options object.
runtime(undefined)
// @ts-expect-error An emitted row requires text.
result.emit({ id: 'missing-text' })
// @ts-expect-error The runtime heatmap uses mutable pairs, not historical object points.
runtime({ points: [{ time: 1, value: 2 }] })
// @ts-expect-error Margin sliders carry pairs rather than scalar numbers.
runtime({ MARGIN: { steps: [{ value: 10 }] } })
// @ts-expect-error A live mount needs an explicit destination.
result.mount()
// @ts-expect-error Facade option is a getter, although the returned options object is mutable.
result.option = configured
// @ts-expect-error Facade isHide is a getter.
result.isHide = true
// @ts-expect-error Facade isStop is a getter.
result.isStop = true
// @ts-expect-error emit returns a Promise; synchronous chaining is not available.
result.emit(row).hide()
// @ts-expect-error The factory has icons but no default self-reference.
void runtime.default
export type { CommandReturns, EmitReturns, FactoryArguments, FactoryReturns, LoadedPayload, LoadReturns, OwnerHasNoFacadeKeys, VisiblePayload }
void [factory, registered, icon, promise, loading, replacing, stopped, loadedPayload, pointsPayload, failedPayload, consume]