@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.
@@ -0,0 +1,188 @@
1
+ import { createEntity } from '@flighthq/entity/contract';
2
+ import { clearSignal, createSignal, emitSignal } from '@flighthq/signals/contract';
3
+ import { getMidiAccessResourceState, getMidiPortResourceState } from './midiResource';
4
+ export function attachMidiAccessStateSubscription(access, subscription) {
5
+ const state = getMidiAccessResourceState(access);
6
+ if (state === undefined || state.disposed)
7
+ return Promise.resolve(attachFailure());
8
+ return attachMidiSubscription(subscription, state.subscriptions, (listener) => state.operations.attachStateChange(listener), (port) => {
9
+ state.knownPorts.add(port);
10
+ emitSignal(subscription.onMidiAccessStateChange, port);
11
+ });
12
+ }
13
+ export function attachMidiInputMessageSubscription(input, subscription) {
14
+ const state = getMidiPortResourceState(input);
15
+ if (state === undefined || state.kind !== 'input' || state.disposed)
16
+ return Promise.resolve(attachFailure());
17
+ return attachMidiSubscription(subscription, state.messageSubscriptions, (listener) => state.operations.attachMessage(listener), (data, timestamp) => emitSignal(subscription.onMidiInputMessage, { data: new Uint8Array(data), timestamp }));
18
+ }
19
+ export function attachMidiPortStateSubscription(port, subscription) {
20
+ const state = getMidiPortResourceState(port);
21
+ if (state === undefined || state.disposed)
22
+ return Promise.resolve(attachFailure());
23
+ return attachMidiSubscription(subscription, state.stateSubscriptions, (listener) => state.operations.attachStateChange(listener), () => emitSignal(subscription.onMidiPortStateChange, port));
24
+ }
25
+ export function createMidiAccessStateSubscription() {
26
+ return createMidiSubscription({ onMidiAccessStateChange: createSignal() });
27
+ }
28
+ export function createMidiInputMessageSubscription() {
29
+ return createMidiSubscription({ onMidiInputMessage: createSignal() });
30
+ }
31
+ export function createMidiPortStateSubscription() {
32
+ return createMidiSubscription({ onMidiPortStateChange: createSignal() });
33
+ }
34
+ export function detachMidiAccessStateSubscription(subscription) {
35
+ return detachMidiSubscription(subscription);
36
+ }
37
+ export function detachMidiInputMessageSubscription(subscription) {
38
+ return detachMidiSubscription(subscription);
39
+ }
40
+ export function detachMidiPortStateSubscription(subscription) {
41
+ return detachMidiSubscription(subscription);
42
+ }
43
+ export function disposeMidiAccessStateSubscription(subscription) {
44
+ return disposeMidiSubscription(subscription, subscription.onMidiAccessStateChange);
45
+ }
46
+ export function disposeMidiInputMessageSubscription(subscription) {
47
+ return disposeMidiSubscription(subscription, subscription.onMidiInputMessage);
48
+ }
49
+ export function disposeMidiPortStateSubscription(subscription) {
50
+ return disposeMidiSubscription(subscription, subscription.onMidiPortStateChange);
51
+ }
52
+ const subscriptionStates = new WeakMap();
53
+ function createMidiSubscription(fields) {
54
+ const subscription = createEntity(fields);
55
+ subscriptionStates.set(subscription, {
56
+ attachment: null,
57
+ attaching: null,
58
+ disposeCompleted: false,
59
+ disposed: false,
60
+ generation: 0,
61
+ ownerSubscriptions: null,
62
+ });
63
+ return subscription;
64
+ }
65
+ async function attachMidiSubscription(subscription, ownerSubscriptions, attach, listener) {
66
+ const runtime = subscriptionStates.get(subscription);
67
+ if (runtime === undefined || runtime.disposed)
68
+ return attachFailure();
69
+ if (runtime.attaching !== null) {
70
+ runtime.generation++;
71
+ await runtime.attaching;
72
+ if (runtime.disposed)
73
+ return attachFailure();
74
+ }
75
+ if (runtime.attachment !== null) {
76
+ const detached = await detachMidiSubscription(subscription);
77
+ if (detached.reason === 'operation-failed') {
78
+ return { attachFailed: false, reason: 'operation-failed', releaseFailed: true };
79
+ }
80
+ if (runtime.disposed)
81
+ return attachFailure();
82
+ }
83
+ const generation = ++runtime.generation;
84
+ runtime.ownerSubscriptions = ownerSubscriptions;
85
+ ownerSubscriptions.add(subscription);
86
+ const attaching = performMidiAttach(subscription, runtime, generation, attach, listener);
87
+ runtime.attaching = attaching;
88
+ const outcome = await attaching;
89
+ if (runtime.attaching === attaching)
90
+ runtime.attaching = null;
91
+ return outcome;
92
+ }
93
+ async function performMidiAttach(subscription, runtime, generation, attach, listener) {
94
+ let pending;
95
+ try {
96
+ pending = attach(listener);
97
+ }
98
+ catch {
99
+ untrackMidiSubscription(subscription, runtime);
100
+ return attachFailure();
101
+ }
102
+ const outcome = await settleMidiAttach(pending);
103
+ if (outcome.reason === 'operation-failed') {
104
+ untrackMidiSubscription(subscription, runtime);
105
+ return { attachFailed: true, reason: 'operation-failed', releaseFailed: outcome.releaseFailed };
106
+ }
107
+ runtime.attachment = outcome.attachment;
108
+ if (runtime.disposed || runtime.generation !== generation) {
109
+ const released = await releaseTrackedMidiAttachment(subscription, runtime);
110
+ return released ? { reason: 'ok' } : { attachFailed: false, reason: 'operation-failed', releaseFailed: true };
111
+ }
112
+ return { reason: 'ok' };
113
+ }
114
+ async function detachMidiSubscription(subscription) {
115
+ const runtime = subscriptionStates.get(subscription);
116
+ if (runtime === undefined)
117
+ return { reason: 'not-attached' };
118
+ runtime.generation++;
119
+ let invalidatedAttach = false;
120
+ if (runtime.attaching !== null) {
121
+ const outcome = await runtime.attaching;
122
+ if (outcome.reason === 'operation-failed' && outcome.releaseFailed) {
123
+ return { reason: 'operation-failed', releaseFailed: true };
124
+ }
125
+ invalidatedAttach = outcome.reason === 'ok';
126
+ }
127
+ if (runtime.attachment === null)
128
+ return { reason: invalidatedAttach ? 'ok' : 'not-attached' };
129
+ if (!(await releaseTrackedMidiAttachment(subscription, runtime))) {
130
+ return { reason: 'operation-failed', releaseFailed: true };
131
+ }
132
+ return { reason: 'ok' };
133
+ }
134
+ async function disposeMidiSubscription(subscription, signal) {
135
+ const runtime = subscriptionStates.get(subscription);
136
+ if (runtime === undefined || runtime.disposeCompleted)
137
+ return { reason: 'already-disposed' };
138
+ runtime.disposed = true;
139
+ runtime.generation++;
140
+ let attachFailed = false;
141
+ let releaseFailed = false;
142
+ if (runtime.attaching !== null) {
143
+ const outcome = await runtime.attaching;
144
+ if (outcome.reason === 'operation-failed')
145
+ ({ attachFailed, releaseFailed } = outcome);
146
+ }
147
+ if (!releaseFailed) {
148
+ const detached = await detachMidiSubscription(subscription);
149
+ if (detached.reason === 'operation-failed')
150
+ releaseFailed = true;
151
+ }
152
+ clearSignal(signal);
153
+ if (attachFailed || releaseFailed)
154
+ return { attachFailed, reason: 'operation-failed', releaseFailed };
155
+ runtime.disposeCompleted = true;
156
+ return { reason: 'ok' };
157
+ }
158
+ function attachFailure() {
159
+ return { attachFailed: true, reason: 'operation-failed', releaseFailed: false };
160
+ }
161
+ async function releaseMidiAttachment(attachment) {
162
+ try {
163
+ return (await attachment.release()).reason === 'ok';
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ }
169
+ async function releaseTrackedMidiAttachment(subscription, runtime) {
170
+ if (runtime.attachment === null || !(await releaseMidiAttachment(runtime.attachment)))
171
+ return false;
172
+ runtime.attachment = null;
173
+ untrackMidiSubscription(subscription, runtime);
174
+ return true;
175
+ }
176
+ function untrackMidiSubscription(subscription, runtime) {
177
+ runtime.ownerSubscriptions?.delete(subscription);
178
+ runtime.ownerSubscriptions = null;
179
+ }
180
+ async function settleMidiAttach(pending) {
181
+ try {
182
+ return await pending;
183
+ }
184
+ catch {
185
+ return { reason: 'operation-failed', releaseFailed: false };
186
+ }
187
+ }
188
+ //# sourceMappingURL=midiSubscription.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"midiSubscription.js","sourceRoot":"","sources":["../src/midiSubscription.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@flighthq/midi",
3
+ "version": "0.4.1-next.515.b0834fe",
4
+ "author": "Joshua Granick and other contributors",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/flighthq/flight.git",
9
+ "directory": "packages/midi"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ },
19
+ "./contract": {
20
+ "types": "./dist/contract.d.ts",
21
+ "default": "./dist/contract.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src/**/*.test.ts",
27
+ "!dist/**/*.test.js",
28
+ "!dist/**/*.test.d.ts",
29
+ "!dist/**/*.test.js.map",
30
+ "!dist/**/*.test.d.ts.map"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsc -b",
34
+ "clean": "tsc -b --clean",
35
+ "test": "vitest run --config vitest.config.ts",
36
+ "test:watch": "vitest --watch --config vitest.config.ts",
37
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
38
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
+ },
40
+ "dependencies": {
41
+ "@flighthq/entity": "0.4.1-next.515.b0834fe",
42
+ "@flighthq/signals": "0.4.1-next.515.b0834fe",
43
+ "@flighthq/types": "0.4.1-next.515.b0834fe"
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "^5.3.0"
47
+ },
48
+ "description": "Explicit MIDI access, port resources, and lifecycle subscriptions",
49
+ "sideEffects": false
50
+ }
@@ -0,0 +1,130 @@
1
+ import { createEntity } from '@flighthq/entity/contract';
2
+ import { EntityRuntimeKey } from '@flighthq/types/contract';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import * as midi from './contract';
6
+
7
+ describe('createMidiAccessResource', () => {
8
+ it('creates an Entity whose provider operations stay outside its public fields', () => {
9
+ const createMidiAccessResource = requiredFunction('createMidiAccessResource');
10
+ const access = createMidiAccessResource({
11
+ attachStateChange: vi.fn(),
12
+ getInputPorts: () => [],
13
+ getOutputPorts: () => [],
14
+ }) as object;
15
+ expect(EntityRuntimeKey in access).toBe(true);
16
+ expect(Object.keys(access)).toEqual([]);
17
+ });
18
+ });
19
+
20
+ describe('disposeMidiAccess', () => {
21
+ it('is terminal and idempotent when no owned resources need release', async () => {
22
+ const access = createEmptyAccess();
23
+ const disposeMidiAccess = requiredFunction('disposeMidiAccess');
24
+ await expect(disposeMidiAccess(access)).resolves.toEqual({ reason: 'ok' });
25
+ await expect(disposeMidiAccess(access)).resolves.toEqual({ reason: 'already-disposed' });
26
+ });
27
+
28
+ it('attempts subscriptions and hotplug-only ports, then retries only failed releases', async () => {
29
+ const accessListener: { current: ((port: unknown) => void) | null } = { current: null };
30
+ const release = vi
31
+ .fn()
32
+ .mockResolvedValueOnce({ reason: 'operation-failed' })
33
+ .mockResolvedValueOnce({ reason: 'ok' });
34
+ const access = requiredFunction('createMidiAccessResource')({
35
+ attachStateChange: async (listener: (port: unknown) => void) => {
36
+ accessListener.current = listener;
37
+ return { attachment: createEntity({ release }), reason: 'ok' };
38
+ },
39
+ getInputPorts: () => [],
40
+ getOutputPorts: () => [],
41
+ });
42
+ const connection = { value: 'closed' };
43
+ const close = vi.fn(async () => {
44
+ connection.value = 'closed';
45
+ });
46
+ const port = requiredFunction('createMidiOutputPortResource')(
47
+ { id: 'hotplug', manufacturer: null, name: 'Hotplug', version: null },
48
+ {
49
+ attachStateChange: vi.fn(),
50
+ close,
51
+ getConnection: () => connection.value,
52
+ getState: () => 'connected',
53
+ open: async () => {
54
+ connection.value = 'open';
55
+ },
56
+ send: vi.fn(),
57
+ },
58
+ );
59
+ const subscription = requiredFunction('createMidiAccessStateSubscription')();
60
+ await requiredFunction('attachMidiAccessStateSubscription')(access, subscription);
61
+ accessListener.current?.(port);
62
+ await requiredFunction('openMidiPort')(port);
63
+
64
+ const disposeMidiAccess = requiredFunction('disposeMidiAccess');
65
+ await expect(disposeMidiAccess(access)).resolves.toEqual({
66
+ failures: [{ operation: 'state-subscription-release' }],
67
+ reason: 'operation-failed',
68
+ });
69
+ expect(close).toHaveBeenCalledOnce();
70
+ await expect(disposeMidiAccess(access)).resolves.toEqual({ reason: 'ok' });
71
+ expect(close).toHaveBeenCalledOnce();
72
+ expect(release).toHaveBeenCalledTimes(2);
73
+ });
74
+ });
75
+
76
+ describe('getMidiAccessInputPorts', () => {
77
+ it('returns the provider current stable input identities without routing through ids', () => {
78
+ const input = createEntity({ id: 'duplicate' });
79
+ const access = createAccess([input], []);
80
+ const getMidiAccessInputPorts = requiredFunction('getMidiAccessInputPorts');
81
+ expect(getMidiAccessInputPorts(access)).toEqual({ ports: [input], reason: 'ok' });
82
+ expect(getMidiAccessInputPorts(access)).toEqual({ ports: [input], reason: 'ok' });
83
+ });
84
+ });
85
+
86
+ describe('getMidiAccessOutputPorts', () => {
87
+ it('accepts an access with zero devices and preserves the provider output identities', () => {
88
+ const output = createEntity({ id: 'duplicate' });
89
+ const getMidiAccessOutputPorts = requiredFunction('getMidiAccessOutputPorts');
90
+ expect(getMidiAccessOutputPorts(createAccess([], []))).toEqual({ ports: [], reason: 'ok' });
91
+ expect(getMidiAccessOutputPorts(createAccess([], [output]))).toEqual({ ports: [output], reason: 'ok' });
92
+ });
93
+ });
94
+
95
+ describe('requestMidiAccess', () => {
96
+ it('retains accepted access and keeps denial, security restriction, and provider failure distinct', async () => {
97
+ const requestMidiAccess = requiredFunction('requestMidiAccess');
98
+ const access = createEmptyAccess();
99
+ const requestAccess = vi
100
+ .fn()
101
+ .mockResolvedValueOnce({ access, reason: 'accepted' })
102
+ .mockResolvedValueOnce({ reason: 'permission-denied' })
103
+ .mockResolvedValueOnce({ reason: 'security-restricted' })
104
+ .mockRejectedValueOnce(new Error('provider fault'));
105
+ const host = { midi: { access: createEntity({ requestAccess }) } };
106
+ await expect(requestMidiAccess(host)).resolves.toEqual({ access, reason: 'accepted' });
107
+ await expect(requestMidiAccess(host)).resolves.toEqual({ reason: 'permission-denied' });
108
+ await expect(requestMidiAccess(host)).resolves.toEqual({ reason: 'security-restricted' });
109
+ await expect(requestMidiAccess(host)).resolves.toEqual({ reason: 'operation-failed' });
110
+ });
111
+ });
112
+
113
+ function createAccess(inputs: readonly object[], outputs: readonly object[]): unknown {
114
+ return requiredFunction('createMidiAccessResource')({
115
+ attachStateChange: vi.fn(),
116
+ getInputPorts: () => inputs,
117
+ getOutputPorts: () => outputs,
118
+ });
119
+ }
120
+
121
+ function createEmptyAccess(): unknown {
122
+ return createAccess([], []);
123
+ }
124
+
125
+ function requiredFunction(name: string): (...args: unknown[]) => unknown {
126
+ const value: unknown = Reflect.get(midi, name);
127
+ expect(value, `${name} export`).toBeTypeOf('function');
128
+ if (typeof value !== 'function') throw new TypeError(`${name} is not exported`);
129
+ return value as (...args: unknown[]) => unknown;
130
+ }
@@ -0,0 +1,27 @@
1
+ import { createEntity } from '@flighthq/entity/contract';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import * as midi from './contract';
5
+
6
+ describe('getMidiPermission', () => {
7
+ it('queries only the explicit MIDI permission owner and preserves shared query outcomes', async () => {
8
+ const getPermission = vi
9
+ .fn()
10
+ .mockResolvedValueOnce({ reason: 'ok', state: 'prompt' })
11
+ .mockResolvedValueOnce({ reason: 'unsupported' })
12
+ .mockRejectedValueOnce(new Error('provider fault'));
13
+ const host = { midi: { permission: createEntity({ getPermission }) } };
14
+ const getMidiPermission = requiredFunction('getMidiPermission');
15
+ await expect(getMidiPermission(host)).resolves.toEqual({ reason: 'ok', state: 'prompt' });
16
+ await expect(getMidiPermission(host)).resolves.toEqual({ reason: 'unsupported' });
17
+ await expect(getMidiPermission(host)).resolves.toEqual({ reason: 'operation-failed' });
18
+ expect(getPermission).toHaveBeenCalledTimes(3);
19
+ });
20
+ });
21
+
22
+ function requiredFunction(name: string): (...args: unknown[]) => unknown {
23
+ const value: unknown = Reflect.get(midi, name);
24
+ expect(value, `${name} export`).toBeTypeOf('function');
25
+ if (typeof value !== 'function') throw new TypeError(`${name} is not exported`);
26
+ return value as (...args: unknown[]) => unknown;
27
+ }
@@ -0,0 +1,259 @@
1
+ import { createEntity } from '@flighthq/entity/contract';
2
+ import { EntityRuntimeKey } from '@flighthq/types/contract';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import * as midi from './contract';
6
+
7
+ describe('closeMidiPort', () => {
8
+ it('distinguishes a successful close, an already-closed port, disposal, and provider failure', async () => {
9
+ const connection = { value: 'open' };
10
+ const close = vi.fn(async () => {
11
+ connection.value = 'closed';
12
+ });
13
+ const port = createOutputPort({ close, connection });
14
+ const closeMidiPort = requiredFunction('closeMidiPort');
15
+ await expect(closeMidiPort(port)).resolves.toEqual({ reason: 'closed' });
16
+ await expect(closeMidiPort(port)).resolves.toEqual({ reason: 'already-closed' });
17
+ expect(close).toHaveBeenCalledOnce();
18
+ await requiredFunction('disposeMidiPort')(port);
19
+ await expect(closeMidiPort(port)).resolves.toEqual({ reason: 'disposed' });
20
+
21
+ const failed = createOutputPort({
22
+ close: vi.fn(async () => Promise.reject(new Error('close'))),
23
+ connection: { value: 'open' },
24
+ });
25
+ await expect(closeMidiPort(failed)).resolves.toEqual({ reason: 'operation-failed' });
26
+ });
27
+ });
28
+
29
+ describe('createMidiInputPortResource', () => {
30
+ it('creates a typed Entity with immutable diagnostic metadata only', () => {
31
+ const port = createInputPort();
32
+ expect(EntityRuntimeKey in port).toBe(true);
33
+ expect(port).toMatchObject({
34
+ id: 'shared-id',
35
+ manufacturer: 'Flight',
36
+ name: 'Input',
37
+ type: 'input',
38
+ version: '1',
39
+ });
40
+ expect(Object.keys(port).sort()).toEqual(['id', 'manufacturer', 'name', 'type', 'version']);
41
+ });
42
+ });
43
+
44
+ describe('createMidiOutputPortResource', () => {
45
+ it('creates a distinct output Entity even when a native id matches an input', () => {
46
+ const input = createInputPort();
47
+ const output = createOutputPort();
48
+ expect(EntityRuntimeKey in output).toBe(true);
49
+ expect(output).not.toBe(input);
50
+ expect(output).toMatchObject({ id: input.id, type: 'output' });
51
+ });
52
+ });
53
+
54
+ describe('disposeMidiPort', () => {
55
+ it('closes only a port opened through Flight, attempts failure again, and is terminal after success', async () => {
56
+ const disposeMidiPort = requiredFunction('disposeMidiPort');
57
+ const openMidiPort = requiredFunction('openMidiPort');
58
+ const ownedConnection = { value: 'closed' };
59
+ const ownedClose = vi
60
+ .fn()
61
+ .mockRejectedValueOnce(new Error('first close failed'))
62
+ .mockImplementationOnce(async () => {
63
+ ownedConnection.value = 'closed';
64
+ });
65
+ const owned = createOutputPort({ close: ownedClose, connection: ownedConnection });
66
+ await expect(openMidiPort(owned)).resolves.toEqual({ reason: 'opened' });
67
+ await expect(disposeMidiPort(owned)).resolves.toEqual({
68
+ failures: [{ operation: 'close' }],
69
+ reason: 'operation-failed',
70
+ });
71
+ await expect(disposeMidiPort(owned)).resolves.toEqual({ reason: 'ok' });
72
+ await expect(disposeMidiPort(owned)).resolves.toEqual({ reason: 'already-disposed' });
73
+ expect(ownedClose).toHaveBeenCalledTimes(2);
74
+
75
+ const borrowedClose = vi.fn(async () => undefined);
76
+ const borrowed = createOutputPort({ close: borrowedClose, connection: { value: 'open' } });
77
+ await expect(openMidiPort(borrowed)).resolves.toEqual({ reason: 'already-open' });
78
+ await expect(disposeMidiPort(borrowed)).resolves.toEqual({ reason: 'ok' });
79
+ expect(borrowedClose).not.toHaveBeenCalled();
80
+ });
81
+
82
+ it('attempts every attached subscription and owned close, then retries only failed releases', async () => {
83
+ const stateRelease = vi
84
+ .fn()
85
+ .mockResolvedValueOnce({ reason: 'operation-failed' })
86
+ .mockResolvedValueOnce({ reason: 'ok' });
87
+ const messageRelease = vi
88
+ .fn()
89
+ .mockResolvedValueOnce({ reason: 'operation-failed' })
90
+ .mockResolvedValueOnce({ reason: 'ok' });
91
+ const connection = { value: 'closed' };
92
+ const close = vi.fn(async () => {
93
+ connection.value = 'closed';
94
+ });
95
+ const input = createInputPort({
96
+ attachMessage: async () => ({ attachment: createEntity({ release: messageRelease }), reason: 'ok' }),
97
+ attachStateChange: async () => ({ attachment: createEntity({ release: stateRelease }), reason: 'ok' }),
98
+ close,
99
+ connection,
100
+ });
101
+ const messageSubscription = requiredFunction('createMidiInputMessageSubscription')();
102
+ const stateSubscription = requiredFunction('createMidiPortStateSubscription')();
103
+ await requiredFunction('attachMidiInputMessageSubscription')(input, messageSubscription);
104
+ await requiredFunction('attachMidiPortStateSubscription')(input, stateSubscription);
105
+ await requiredFunction('openMidiPort')(input);
106
+
107
+ const disposeMidiPort = requiredFunction('disposeMidiPort');
108
+ await expect(disposeMidiPort(input)).resolves.toEqual({
109
+ failures: [{ operation: 'state-subscription-release' }, { operation: 'message-subscription-release' }],
110
+ reason: 'operation-failed',
111
+ });
112
+ expect(close).toHaveBeenCalledOnce();
113
+ await expect(disposeMidiPort(input)).resolves.toEqual({ reason: 'ok' });
114
+ expect(close).toHaveBeenCalledOnce();
115
+ expect(stateRelease).toHaveBeenCalledTimes(2);
116
+ expect(messageRelease).toHaveBeenCalledTimes(2);
117
+ });
118
+ });
119
+
120
+ describe('getMidiPortConnection', () => {
121
+ it('pulls current connection from the exact origin and reports disposed resources', async () => {
122
+ const first = createInputPort({ connection: { value: 'open' } });
123
+ const second = createInputPort({ connection: { value: 'closed' } });
124
+ const getMidiPortConnection = requiredFunction('getMidiPortConnection');
125
+ expect(getMidiPortConnection(first)).toEqual({ connection: 'open', reason: 'ok' });
126
+ expect(getMidiPortConnection(second)).toEqual({ connection: 'closed', reason: 'ok' });
127
+ await requiredFunction('disposeMidiPort')(second);
128
+ expect(getMidiPortConnection(second)).toEqual({ reason: 'disposed' });
129
+ });
130
+ });
131
+
132
+ describe('getMidiPortState', () => {
133
+ it('pulls dynamic state instead of mirroring it into public metadata', () => {
134
+ const state = { value: 'connected' };
135
+ const port = createInputPort({ state });
136
+ const getMidiPortState = requiredFunction('getMidiPortState');
137
+ expect(getMidiPortState(port)).toEqual({ reason: 'ok', state: 'connected' });
138
+ state.value = 'disconnected';
139
+ expect(getMidiPortState(port)).toEqual({ reason: 'ok', state: 'disconnected' });
140
+ expect(Reflect.has(port, 'state')).toBe(false);
141
+ expect(Reflect.has(port, 'connection')).toBe(false);
142
+ });
143
+ });
144
+
145
+ describe('openMidiPort', () => {
146
+ it('distinguishes open, already-open, disconnected, disposed, and provider failure', async () => {
147
+ const connection = { value: 'closed' };
148
+ const open = vi.fn(async () => {
149
+ connection.value = 'open';
150
+ });
151
+ const port = createInputPort({ connection, open });
152
+ const openMidiPort = requiredFunction('openMidiPort');
153
+ await expect(openMidiPort(port)).resolves.toEqual({ reason: 'opened' });
154
+ await expect(openMidiPort(port)).resolves.toEqual({ reason: 'already-open' });
155
+ expect(open).toHaveBeenCalledOnce();
156
+
157
+ const disconnected = createInputPort({ state: { value: 'disconnected' } });
158
+ await expect(openMidiPort(disconnected)).resolves.toEqual({ reason: 'disconnected' });
159
+ await requiredFunction('disposeMidiPort')(disconnected);
160
+ await expect(openMidiPort(disconnected)).resolves.toEqual({ reason: 'disposed' });
161
+
162
+ const failed = createInputPort({ open: vi.fn(async () => Promise.reject(new Error('open'))) });
163
+ await expect(openMidiPort(failed)).resolves.toEqual({ reason: 'operation-failed' });
164
+ });
165
+ });
166
+
167
+ describe('sendMidiMessage', () => {
168
+ it('validates one basic message before the provider and never admits system exclusive data', async () => {
169
+ const send = vi.fn();
170
+ const connection = { value: 'closed' };
171
+ const output = createOutputPort({ connection, send });
172
+ const sendMidiMessage = requiredFunction('sendMidiMessage');
173
+ expect(sendMidiMessage(output, new Uint8Array())).toEqual({ reason: 'invalid-message' });
174
+ expect(sendMidiMessage(output, new Uint8Array([0x40]))).toEqual({ reason: 'invalid-message' });
175
+ expect(sendMidiMessage(output, new Uint8Array([0x90, 0x40]))).toEqual({ reason: 'invalid-message' });
176
+ expect(sendMidiMessage(output, new Uint8Array([0x90, 0x90, 0x7f]))).toEqual({ reason: 'invalid-message' });
177
+ expect(sendMidiMessage(output, new Uint8Array([0xf0, 0x01, 0xf7]))).toEqual({
178
+ reason: 'system-exclusive-not-enabled',
179
+ });
180
+ expect(sendMidiMessage(output, new Uint8Array([0x90, 0x40, 0x7f]))).toEqual({ reason: 'not-open' });
181
+ expect(send).not.toHaveBeenCalled();
182
+
183
+ connection.value = 'open';
184
+ expect(sendMidiMessage(output, new Uint8Array([0x90, 0x40, 0x7f]), 12.5)).toEqual({ reason: 'sent' });
185
+ expect(send).toHaveBeenCalledWith([0x90, 0x40, 0x7f], 12.5);
186
+
187
+ const disconnected = createOutputPort({ connection: { value: 'open' }, state: { value: 'disconnected' } });
188
+ expect(sendMidiMessage(disconnected, new Uint8Array([0x80, 0x40, 0]))).toEqual({ reason: 'disconnected' });
189
+ });
190
+ });
191
+
192
+ interface MutableValue {
193
+ value: string;
194
+ }
195
+
196
+ interface PortOverrides {
197
+ attachMessage?: (listener: (data: Uint8Array, timestamp: number) => void) => Promise<unknown>;
198
+ attachStateChange?: (listener: () => void) => Promise<unknown>;
199
+ close?: () => Promise<void>;
200
+ connection?: MutableValue;
201
+ open?: () => Promise<void>;
202
+ send?: (data: readonly number[], timestamp?: number) => void;
203
+ state?: MutableValue;
204
+ }
205
+
206
+ function createInputPort(overrides: Readonly<PortOverrides> = {}): Record<string, unknown> {
207
+ const connection = overrides.connection ?? { value: 'closed' };
208
+ const state = overrides.state ?? { value: 'connected' };
209
+ return requiredFunction('createMidiInputPortResource')(
210
+ { id: 'shared-id', manufacturer: 'Flight', name: 'Input', version: '1' },
211
+ {
212
+ attachMessage: overrides.attachMessage ?? vi.fn(),
213
+ attachStateChange: overrides.attachStateChange ?? vi.fn(),
214
+ close:
215
+ overrides.close ??
216
+ (async () => {
217
+ connection.value = 'closed';
218
+ }),
219
+ getConnection: () => connection.value,
220
+ getState: () => state.value,
221
+ open:
222
+ overrides.open ??
223
+ (async () => {
224
+ connection.value = 'open';
225
+ }),
226
+ },
227
+ ) as Record<string, unknown>;
228
+ }
229
+
230
+ function createOutputPort(overrides: Readonly<PortOverrides> = {}): Record<string, unknown> {
231
+ const connection = overrides.connection ?? { value: 'closed' };
232
+ const state = overrides.state ?? { value: 'connected' };
233
+ return requiredFunction('createMidiOutputPortResource')(
234
+ { id: 'shared-id', manufacturer: 'Flight', name: 'Output', version: '1' },
235
+ {
236
+ attachStateChange: vi.fn(),
237
+ close:
238
+ overrides.close ??
239
+ (async () => {
240
+ connection.value = 'closed';
241
+ }),
242
+ getConnection: () => connection.value,
243
+ getState: () => state.value,
244
+ open:
245
+ overrides.open ??
246
+ (async () => {
247
+ connection.value = 'open';
248
+ }),
249
+ send: overrides.send ?? vi.fn(),
250
+ },
251
+ ) as Record<string, unknown>;
252
+ }
253
+
254
+ function requiredFunction(name: string): (...args: unknown[]) => unknown {
255
+ const value: unknown = Reflect.get(midi, name);
256
+ expect(value, `${name} export`).toBeTypeOf('function');
257
+ if (typeof value !== 'function') throw new TypeError(`${name} is not exported`);
258
+ return value as (...args: unknown[]) => unknown;
259
+ }