@flighthq/midi 0.4.1-next.515.b0834fe
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.md +9 -0
- package/README.md +17 -0
- package/dist/contract.d.ts +5 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +5 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/midiAccess.d.ts +7 -0
- package/dist/midiAccess.d.ts.map +1 -0
- package/dist/midiAccess.js +91 -0
- package/dist/midiAccess.js.map +1 -0
- package/dist/midiPermission.d.ts +3 -0
- package/dist/midiPermission.d.ts.map +1 -0
- package/dist/midiPermission.js +9 -0
- package/dist/midiPermission.js.map +1 -0
- package/dist/midiPort.d.ts +17 -0
- package/dist/midiPort.d.ts.map +1 -0
- package/dist/midiPort.js +224 -0
- package/dist/midiPort.js.map +1 -0
- package/dist/midiResource.d.ts +33 -0
- package/dist/midiResource.d.ts.map +1 -0
- package/dist/midiResource.js +36 -0
- package/dist/midiResource.js.map +1 -0
- package/dist/midiSubscription.d.ts +14 -0
- package/dist/midiSubscription.d.ts.map +1 -0
- package/dist/midiSubscription.js +188 -0
- package/dist/midiSubscription.js.map +1 -0
- package/package.json +50 -0
- package/src/midiAccess.test.ts +130 -0
- package/src/midiPermission.test.ts +27 -0
- package/src/midiPort.test.ts +259 -0
- package/src/midiResource.test.ts +106 -0
- package/src/midiSubscription.test.ts +283 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity/contract';
|
|
2
|
+
import type { MidiAccess, MidiInputPort, MidiOutputPort } from '@flighthq/types/contract';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getMidiAccessResourceState,
|
|
7
|
+
getMidiPortResourceState,
|
|
8
|
+
retainMidiAccessResourceState,
|
|
9
|
+
retainMidiInputPortResourceState,
|
|
10
|
+
retainMidiOutputPortResourceState,
|
|
11
|
+
} from './midiResource';
|
|
12
|
+
|
|
13
|
+
describe('getMidiAccessResourceState', () => {
|
|
14
|
+
it('returns only state retained for the exact access identity', () => {
|
|
15
|
+
const retained = createEntity({}) as MidiAccess;
|
|
16
|
+
const other = createEntity({}) as MidiAccess;
|
|
17
|
+
retainMidiAccessResourceState(retained, accessOperations());
|
|
18
|
+
expect(getMidiAccessResourceState(retained)?.disposed).toBe(false);
|
|
19
|
+
expect(getMidiAccessResourceState(other)).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('getMidiPortResourceState', () => {
|
|
24
|
+
it('does not collapse distinct port identities with matching metadata', () => {
|
|
25
|
+
const first = inputPort();
|
|
26
|
+
const second = inputPort();
|
|
27
|
+
retainMidiInputPortResourceState(first, inputOperations());
|
|
28
|
+
retainMidiInputPortResourceState(second, inputOperations());
|
|
29
|
+
expect(getMidiPortResourceState(first)).not.toBe(getMidiPortResourceState(second));
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('retainMidiAccessResourceState', () => {
|
|
34
|
+
it('starts an empty live access ownership ledger', () => {
|
|
35
|
+
const access = createEntity({}) as MidiAccess;
|
|
36
|
+
retainMidiAccessResourceState(access, accessOperations());
|
|
37
|
+
expect(getMidiAccessResourceState(access)).toMatchObject({
|
|
38
|
+
disposeCompleted: false,
|
|
39
|
+
disposePending: null,
|
|
40
|
+
disposed: false,
|
|
41
|
+
});
|
|
42
|
+
expect(getMidiAccessResourceState(access)?.knownPorts.size).toBe(0);
|
|
43
|
+
expect(getMidiAccessResourceState(access)?.subscriptions.size).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('retainMidiInputPortResourceState', () => {
|
|
48
|
+
it('retains separate state and message subscription ownership', () => {
|
|
49
|
+
const port = inputPort();
|
|
50
|
+
retainMidiInputPortResourceState(port, inputOperations());
|
|
51
|
+
const state = getMidiPortResourceState(port);
|
|
52
|
+
expect(state?.kind).toBe('input');
|
|
53
|
+
if (state?.kind !== 'input') throw new TypeError('expected input state');
|
|
54
|
+
expect(state.messageSubscriptions.size).toBe(0);
|
|
55
|
+
expect(state.stateSubscriptions.size).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('retainMidiOutputPortResourceState', () => {
|
|
60
|
+
it('retains output operations without an input-message ledger', () => {
|
|
61
|
+
const port = outputPort();
|
|
62
|
+
retainMidiOutputPortResourceState(port, outputOperations());
|
|
63
|
+
const state = getMidiPortResourceState(port);
|
|
64
|
+
expect(state?.kind).toBe('output');
|
|
65
|
+
expect(state).not.toHaveProperty('messageSubscriptions');
|
|
66
|
+
expect(state?.stateSubscriptions.size).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function accessOperations() {
|
|
71
|
+
return {
|
|
72
|
+
attachStateChange: vi.fn(),
|
|
73
|
+
getInputPorts: () => [],
|
|
74
|
+
getOutputPorts: () => [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function inputOperations() {
|
|
79
|
+
return {
|
|
80
|
+
attachMessage: vi.fn(),
|
|
81
|
+
attachStateChange: vi.fn(),
|
|
82
|
+
close: vi.fn(),
|
|
83
|
+
getConnection: () => 'closed' as const,
|
|
84
|
+
getState: () => 'connected' as const,
|
|
85
|
+
open: vi.fn(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function outputOperations() {
|
|
90
|
+
return {
|
|
91
|
+
attachStateChange: vi.fn(),
|
|
92
|
+
close: vi.fn(),
|
|
93
|
+
getConnection: () => 'closed' as const,
|
|
94
|
+
getState: () => 'connected' as const,
|
|
95
|
+
open: vi.fn(),
|
|
96
|
+
send: vi.fn(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function inputPort(): MidiInputPort {
|
|
101
|
+
return createEntity({ id: 'same', manufacturer: null, name: null, type: 'input' as const, version: null });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function outputPort(): MidiOutputPort {
|
|
105
|
+
return createEntity({ id: 'same', manufacturer: null, name: null, type: 'output' as const, version: null });
|
|
106
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity/contract';
|
|
2
|
+
import { connectSignal, hasSignalSlots } from '@flighthq/signals/contract';
|
|
3
|
+
import { EntityRuntimeKey } from '@flighthq/types/contract';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import * as midi from './contract';
|
|
7
|
+
|
|
8
|
+
describe('attachMidiAccessStateSubscription', () => {
|
|
9
|
+
it('pins the exact access origin and emits stable hotplug port entities', async () => {
|
|
10
|
+
const listener: { current: ((port: unknown) => void) | null } = { current: null };
|
|
11
|
+
const release = vi.fn(async () => ({ reason: 'ok' }));
|
|
12
|
+
const access = createAccess(async (next) => {
|
|
13
|
+
listener.current = next;
|
|
14
|
+
return { attachment: createEntity({ release }), reason: 'ok' };
|
|
15
|
+
});
|
|
16
|
+
const subscription = requiredFunction('createMidiAccessStateSubscription')() as SignalEntity;
|
|
17
|
+
const seen: unknown[] = [];
|
|
18
|
+
connectSignal(subscription.onMidiAccessStateChange, (port: unknown) => seen.push(port));
|
|
19
|
+
await expect(requiredFunction('attachMidiAccessStateSubscription')(access, subscription)).resolves.toEqual({
|
|
20
|
+
reason: 'ok',
|
|
21
|
+
});
|
|
22
|
+
const port = createInputPort();
|
|
23
|
+
listener.current?.(port);
|
|
24
|
+
expect(seen).toEqual([port]);
|
|
25
|
+
await requiredFunction('detachMidiAccessStateSubscription')(subscription);
|
|
26
|
+
expect(release).toHaveBeenCalledOnce();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('attachMidiInputMessageSubscription', () => {
|
|
31
|
+
it('copies each byte payload and carries the native event time', async () => {
|
|
32
|
+
const listener: { current: ((data: Uint8Array, timestamp: number) => void) | null } = { current: null };
|
|
33
|
+
const input = createInputPort({
|
|
34
|
+
attachMessage: async (next) => {
|
|
35
|
+
listener.current = next;
|
|
36
|
+
return { attachment: successfulAttachment(), reason: 'ok' };
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const subscription = requiredFunction('createMidiInputMessageSubscription')() as MessageSignalEntity;
|
|
40
|
+
const seen: Array<{ data: Uint8Array; timestamp: number }> = [];
|
|
41
|
+
connectSignal(subscription.onMidiInputMessage, (message: Readonly<{ data: Uint8Array; timestamp: number }>) => {
|
|
42
|
+
seen.push({ data: message.data, timestamp: message.timestamp });
|
|
43
|
+
});
|
|
44
|
+
await expect(requiredFunction('attachMidiInputMessageSubscription')(input, subscription)).resolves.toEqual({
|
|
45
|
+
reason: 'ok',
|
|
46
|
+
});
|
|
47
|
+
const nativeData = new Uint8Array([0x90, 60, 127]);
|
|
48
|
+
listener.current?.(nativeData, 42.5);
|
|
49
|
+
nativeData[1] = 1;
|
|
50
|
+
expect(seen).toHaveLength(1);
|
|
51
|
+
expect([...seen[0].data]).toEqual([0x90, 60, 127]);
|
|
52
|
+
expect(seen[0].data).not.toBe(nativeData);
|
|
53
|
+
expect(seen[0].timestamp).toBe(42.5);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('attachMidiPortStateSubscription', () => {
|
|
58
|
+
it('emits the originating port while state and connection stay pull-based', async () => {
|
|
59
|
+
const listener: { current: (() => void) | null } = { current: null };
|
|
60
|
+
const port = createInputPort({
|
|
61
|
+
attachStateChange: async (next) => {
|
|
62
|
+
listener.current = next;
|
|
63
|
+
return { attachment: successfulAttachment(), reason: 'ok' };
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const subscription = requiredFunction('createMidiPortStateSubscription')() as SignalEntity;
|
|
67
|
+
const seen: unknown[] = [];
|
|
68
|
+
connectSignal(subscription.onMidiPortStateChange, (changed: unknown) => seen.push(changed));
|
|
69
|
+
await expect(requiredFunction('attachMidiPortStateSubscription')(port, subscription)).resolves.toEqual({
|
|
70
|
+
reason: 'ok',
|
|
71
|
+
});
|
|
72
|
+
listener.current?.();
|
|
73
|
+
expect(seen).toEqual([port]);
|
|
74
|
+
expect(Reflect.has(port, 'state')).toBe(false);
|
|
75
|
+
expect(Reflect.has(port, 'connection')).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('createMidiAccessStateSubscription', () => {
|
|
80
|
+
it('creates a detached subscription Entity with an empty signal', () => {
|
|
81
|
+
const subscription = requiredFunction('createMidiAccessStateSubscription')() as SignalEntity;
|
|
82
|
+
expect(EntityRuntimeKey in subscription).toBe(true);
|
|
83
|
+
expect(hasSignalSlots(subscription.onMidiAccessStateChange)).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('createMidiInputMessageSubscription', () => {
|
|
88
|
+
it('creates a detached subscription Entity with an empty signal', () => {
|
|
89
|
+
const subscription = requiredFunction('createMidiInputMessageSubscription')() as MessageSignalEntity;
|
|
90
|
+
expect(EntityRuntimeKey in subscription).toBe(true);
|
|
91
|
+
expect(hasSignalSlots(subscription.onMidiInputMessage)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('createMidiPortStateSubscription', () => {
|
|
96
|
+
it('creates a detached subscription Entity with an empty signal', () => {
|
|
97
|
+
const subscription = requiredFunction('createMidiPortStateSubscription')() as SignalEntity;
|
|
98
|
+
expect(EntityRuntimeKey in subscription).toBe(true);
|
|
99
|
+
expect(hasSignalSlots(subscription.onMidiPortStateChange)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('detachMidiAccessStateSubscription', () => {
|
|
104
|
+
it('retains a failed exact release for retry and never redirects through another access', async () => {
|
|
105
|
+
const release = vi
|
|
106
|
+
.fn()
|
|
107
|
+
.mockResolvedValueOnce({ reason: 'operation-failed' })
|
|
108
|
+
.mockResolvedValueOnce({ reason: 'ok' });
|
|
109
|
+
const access = createAccess(async () => ({ attachment: createEntity({ release }), reason: 'ok' }));
|
|
110
|
+
const subscription = requiredFunction('createMidiAccessStateSubscription')();
|
|
111
|
+
await requiredFunction('attachMidiAccessStateSubscription')(access, subscription);
|
|
112
|
+
const detach = requiredFunction('detachMidiAccessStateSubscription');
|
|
113
|
+
await expect(detach(subscription)).resolves.toEqual({ reason: 'operation-failed', releaseFailed: true });
|
|
114
|
+
await expect(detach(subscription)).resolves.toEqual({ reason: 'ok' });
|
|
115
|
+
await expect(detach(subscription)).resolves.toEqual({ reason: 'not-attached' });
|
|
116
|
+
expect(release).toHaveBeenCalledTimes(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('invalidates an in-flight attach and waits for its exact release', async () => {
|
|
120
|
+
const settle: { current: ((value: unknown) => void) | null } = { current: null };
|
|
121
|
+
const release = vi.fn(async () => ({ reason: 'ok' }));
|
|
122
|
+
const access = createAccess(
|
|
123
|
+
() =>
|
|
124
|
+
new Promise((resolve) => {
|
|
125
|
+
settle.current = resolve;
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
const subscription = requiredFunction('createMidiAccessStateSubscription')();
|
|
129
|
+
const attaching = requiredFunction('attachMidiAccessStateSubscription')(access, subscription);
|
|
130
|
+
const detaching = requiredFunction('detachMidiAccessStateSubscription')(subscription);
|
|
131
|
+
settle.current?.({ attachment: createEntity({ release }), reason: 'ok' });
|
|
132
|
+
await expect(attaching).resolves.toEqual({ reason: 'ok' });
|
|
133
|
+
await expect(detaching).resolves.toEqual({ reason: 'ok' });
|
|
134
|
+
expect(release).toHaveBeenCalledOnce();
|
|
135
|
+
await expect(requiredFunction('detachMidiAccessStateSubscription')(subscription)).resolves.toEqual({
|
|
136
|
+
reason: 'not-attached',
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('detachMidiInputMessageSubscription', () => {
|
|
142
|
+
it('releases an input listener once and reports the detached state', async () => {
|
|
143
|
+
const release = vi.fn(async () => ({ reason: 'ok' }));
|
|
144
|
+
const input = createInputPort({
|
|
145
|
+
attachMessage: async () => ({ attachment: createEntity({ release }), reason: 'ok' }),
|
|
146
|
+
});
|
|
147
|
+
const subscription = requiredFunction('createMidiInputMessageSubscription')();
|
|
148
|
+
await requiredFunction('attachMidiInputMessageSubscription')(input, subscription);
|
|
149
|
+
const detach = requiredFunction('detachMidiInputMessageSubscription');
|
|
150
|
+
await expect(detach(subscription)).resolves.toEqual({ reason: 'ok' });
|
|
151
|
+
await expect(detach(subscription)).resolves.toEqual({ reason: 'not-attached' });
|
|
152
|
+
expect(release).toHaveBeenCalledOnce();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('detachMidiPortStateSubscription', () => {
|
|
157
|
+
it('releases the exact port-state listener without disposing consumer slots', async () => {
|
|
158
|
+
const release = vi.fn(async () => ({ reason: 'ok' }));
|
|
159
|
+
const port = createInputPort({
|
|
160
|
+
attachStateChange: async () => ({ attachment: createEntity({ release }), reason: 'ok' }),
|
|
161
|
+
});
|
|
162
|
+
const subscription = requiredFunction('createMidiPortStateSubscription')() as SignalEntity;
|
|
163
|
+
connectSignal(subscription.onMidiPortStateChange, vi.fn());
|
|
164
|
+
await requiredFunction('attachMidiPortStateSubscription')(port, subscription);
|
|
165
|
+
await expect(requiredFunction('detachMidiPortStateSubscription')(subscription)).resolves.toEqual({ reason: 'ok' });
|
|
166
|
+
expect(hasSignalSlots(subscription.onMidiPortStateChange)).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('disposeMidiAccessStateSubscription', () => {
|
|
171
|
+
it('invalidates an in-flight attach, releases its eventual origin, clears slots, and stays terminal', async () => {
|
|
172
|
+
const settle: { current: ((value: unknown) => void) | null } = { current: null };
|
|
173
|
+
const release = vi.fn(async () => ({ reason: 'ok' }));
|
|
174
|
+
const access = createAccess(
|
|
175
|
+
() =>
|
|
176
|
+
new Promise((resolve) => {
|
|
177
|
+
settle.current = resolve;
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
const subscription = requiredFunction('createMidiAccessStateSubscription')() as SignalEntity;
|
|
181
|
+
connectSignal(subscription.onMidiAccessStateChange, vi.fn());
|
|
182
|
+
const attaching = requiredFunction('attachMidiAccessStateSubscription')(access, subscription);
|
|
183
|
+
const disposing = requiredFunction('disposeMidiAccessStateSubscription')(subscription);
|
|
184
|
+
settle.current?.({ attachment: createEntity({ release }), reason: 'ok' });
|
|
185
|
+
await attaching;
|
|
186
|
+
await expect(disposing).resolves.toEqual({ reason: 'ok' });
|
|
187
|
+
expect(release).toHaveBeenCalledOnce();
|
|
188
|
+
expect(hasSignalSlots(subscription.onMidiAccessStateChange)).toBe(false);
|
|
189
|
+
await expect(requiredFunction('disposeMidiAccessStateSubscription')(subscription)).resolves.toEqual({
|
|
190
|
+
reason: 'already-disposed',
|
|
191
|
+
});
|
|
192
|
+
await expect(requiredFunction('attachMidiAccessStateSubscription')(access, subscription)).resolves.toMatchObject({
|
|
193
|
+
attachFailed: true,
|
|
194
|
+
reason: 'operation-failed',
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('disposeMidiInputMessageSubscription', () => {
|
|
200
|
+
it('clears consumer slots even when native release fails and retries only that release', async () => {
|
|
201
|
+
const release = vi
|
|
202
|
+
.fn()
|
|
203
|
+
.mockResolvedValueOnce({ reason: 'operation-failed' })
|
|
204
|
+
.mockResolvedValueOnce({ reason: 'ok' });
|
|
205
|
+
const input = createInputPort({
|
|
206
|
+
attachMessage: async () => ({ attachment: createEntity({ release }), reason: 'ok' }),
|
|
207
|
+
});
|
|
208
|
+
const subscription = requiredFunction('createMidiInputMessageSubscription')() as MessageSignalEntity;
|
|
209
|
+
connectSignal(subscription.onMidiInputMessage, vi.fn());
|
|
210
|
+
await requiredFunction('attachMidiInputMessageSubscription')(input, subscription);
|
|
211
|
+
const dispose = requiredFunction('disposeMidiInputMessageSubscription');
|
|
212
|
+
await expect(dispose(subscription)).resolves.toEqual({
|
|
213
|
+
attachFailed: false,
|
|
214
|
+
reason: 'operation-failed',
|
|
215
|
+
releaseFailed: true,
|
|
216
|
+
});
|
|
217
|
+
expect(hasSignalSlots(subscription.onMidiInputMessage)).toBe(false);
|
|
218
|
+
await expect(dispose(subscription)).resolves.toEqual({ reason: 'ok' });
|
|
219
|
+
expect(release).toHaveBeenCalledTimes(2);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('disposeMidiPortStateSubscription', () => {
|
|
224
|
+
it('is idempotent after a successful terminal release', async () => {
|
|
225
|
+
const port = createInputPort({
|
|
226
|
+
attachStateChange: async () => ({ attachment: successfulAttachment(), reason: 'ok' }),
|
|
227
|
+
});
|
|
228
|
+
const subscription = requiredFunction('createMidiPortStateSubscription')();
|
|
229
|
+
await requiredFunction('attachMidiPortStateSubscription')(port, subscription);
|
|
230
|
+
const dispose = requiredFunction('disposeMidiPortStateSubscription');
|
|
231
|
+
await expect(dispose(subscription)).resolves.toEqual({ reason: 'ok' });
|
|
232
|
+
await expect(dispose(subscription)).resolves.toEqual({ reason: 'already-disposed' });
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
interface SignalEntity {
|
|
237
|
+
[EntityRuntimeKey]: unknown;
|
|
238
|
+
onMidiAccessStateChange: Parameters<typeof hasSignalSlots>[0];
|
|
239
|
+
onMidiPortStateChange: Parameters<typeof hasSignalSlots>[0];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface MessageSignalEntity {
|
|
243
|
+
[EntityRuntimeKey]: unknown;
|
|
244
|
+
onMidiInputMessage: Parameters<typeof hasSignalSlots>[0];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
interface InputOperations {
|
|
248
|
+
attachMessage?: (listener: (data: Uint8Array, timestamp: number) => void) => Promise<unknown>;
|
|
249
|
+
attachStateChange?: (listener: () => void) => Promise<unknown>;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function createAccess(attachStateChange: (listener: (port: unknown) => void) => Promise<unknown>): unknown {
|
|
253
|
+
return requiredFunction('createMidiAccessResource')({
|
|
254
|
+
attachStateChange,
|
|
255
|
+
getInputPorts: () => [],
|
|
256
|
+
getOutputPorts: () => [],
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function createInputPort(operations: Readonly<InputOperations> = {}): Record<string, unknown> {
|
|
261
|
+
return requiredFunction('createMidiInputPortResource')(
|
|
262
|
+
{ id: 'input', manufacturer: null, name: 'Input', version: null },
|
|
263
|
+
{
|
|
264
|
+
attachMessage: operations.attachMessage ?? vi.fn(),
|
|
265
|
+
attachStateChange: operations.attachStateChange ?? vi.fn(),
|
|
266
|
+
close: async () => undefined,
|
|
267
|
+
getConnection: () => 'closed',
|
|
268
|
+
getState: () => 'connected',
|
|
269
|
+
open: async () => undefined,
|
|
270
|
+
},
|
|
271
|
+
) as Record<string, unknown>;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function requiredFunction(name: string): (...args: unknown[]) => unknown {
|
|
275
|
+
const value: unknown = Reflect.get(midi, name);
|
|
276
|
+
expect(value, `${name} export`).toBeTypeOf('function');
|
|
277
|
+
if (typeof value !== 'function') throw new TypeError(`${name} is not exported`);
|
|
278
|
+
return value as (...args: unknown[]) => unknown;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function successfulAttachment(): object {
|
|
282
|
+
return createEntity({ release: async () => ({ reason: 'ok' }) });
|
|
283
|
+
}
|