@flighthq/protocol 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e
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/README.md +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +12 -18
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +71 -211
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -3
- package/src/protocol.test.ts +114 -464
- package/src/protocolHost.test.ts +53 -0
package/README.md
CHANGED
|
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/protocol`. The `@fli
|
|
|
13
13
|
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
14
|
|
|
15
15
|
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
-
- [Source for @flighthq/protocol@0.5.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/protocol@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/protocol)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { attachProtocolHandler, createProtocolHandler, createProtocolUrl, detachProtocolHandler, disposeProtocolHandler,
|
|
1
|
+
export { attachProtocolHandler, createProtocolHandler, createProtocolUrl, detachProtocolHandler, disposeProtocolHandler, getProtocolLaunchUrl, getRegisteredProtocolSchemes, isProtocolSchemeDefault, isProtocolSchemeRegistered, isValidProtocolScheme, parseProtocolUrl, registerProtocolScheme, registerProtocolSchemes, removeProtocolSchemeAsDefault, setProtocolSchemeAsDefault, unregisterProtocolScheme, unregisterProtocolSchemes, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACtB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,EACpB,4BAA4B,EAC5B,uBAAuB,EACvB,0BAA0B,EAC1B,qBAAqB,EACrB,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,EACvB,6BAA6B,EAC7B,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { attachProtocolHandler, createProtocolHandler, createProtocolUrl, detachProtocolHandler, disposeProtocolHandler,
|
|
1
|
+
export { attachProtocolHandler, createProtocolHandler, createProtocolUrl, detachProtocolHandler, disposeProtocolHandler, getProtocolLaunchUrl, getRegisteredProtocolSchemes, isProtocolSchemeDefault, isProtocolSchemeRegistered, isValidProtocolScheme, parseProtocolUrl, registerProtocolScheme, registerProtocolSchemes, removeProtocolSchemeAsDefault, setProtocolSchemeAsDefault, unregisterProtocolScheme, unregisterProtocolSchemes, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACtB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,EACpB,4BAA4B,EAC5B,uBAAuB,EACvB,0BAA0B,EAC1B,qBAAqB,EACrB,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,EACvB,6BAA6B,EAC7B,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,YAAY,CAAC"}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function attachProtocolHandler(handler: ProtocolHandler): void;
|
|
1
|
+
import type { HasProtocolDefault, HasProtocolLaunch, HasProtocolOpen, HasProtocolRegistration, HasProtocolRegistrationQuery, HasProtocolUnregistration, ParsedProtocolUrl, ProtocolHandler } from '@flighthq/types/contract';
|
|
2
|
+
export declare function attachProtocolHandler(host: HasProtocolOpen, handler: ProtocolHandler): void;
|
|
3
3
|
export declare function createProtocolHandler(): ProtocolHandler;
|
|
4
4
|
export declare function createProtocolUrl(parts: Readonly<Partial<ParsedProtocolUrl>>): string;
|
|
5
5
|
export declare function detachProtocolHandler(handler: ProtocolHandler): void;
|
|
6
6
|
export declare function disposeProtocolHandler(handler: ProtocolHandler): void;
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function
|
|
11
|
-
export declare function installProtocolHostBackend(backend: ProtocolBackend): void;
|
|
12
|
-
export declare function isProtocolSchemeDefault(scheme: string): boolean;
|
|
13
|
-
export declare function isProtocolSchemeRegistered(scheme: string): boolean;
|
|
7
|
+
export declare function getProtocolLaunchUrl(host: HasProtocolLaunch): string | null;
|
|
8
|
+
export declare function getRegisteredProtocolSchemes(host: HasProtocolRegistration): readonly string[];
|
|
9
|
+
export declare function isProtocolSchemeDefault(host: HasProtocolDefault, scheme: string): boolean;
|
|
10
|
+
export declare function isProtocolSchemeRegistered(host: HasProtocolRegistrationQuery, scheme: string): boolean;
|
|
14
11
|
export declare function isValidProtocolScheme(scheme: string): boolean;
|
|
15
|
-
export declare function observeProtocolHostResult(operation: string, succeeded: boolean): void;
|
|
16
12
|
export declare function parseProtocolUrl(url: string): ParsedProtocolUrl | null;
|
|
17
|
-
export declare function registerProtocolScheme(scheme: string): boolean;
|
|
18
|
-
export declare function registerProtocolSchemes(schemes: readonly string[]): boolean;
|
|
19
|
-
export declare function removeProtocolSchemeAsDefault(scheme: string): boolean;
|
|
20
|
-
export declare function
|
|
21
|
-
export declare function
|
|
22
|
-
export declare function
|
|
23
|
-
export declare function unregisterProtocolScheme(scheme: string): boolean;
|
|
24
|
-
export declare function unregisterProtocolSchemes(schemes: readonly string[]): boolean;
|
|
13
|
+
export declare function registerProtocolScheme(host: HasProtocolRegistration, scheme: string): boolean;
|
|
14
|
+
export declare function registerProtocolSchemes(host: HasProtocolRegistration, schemes: readonly string[]): boolean;
|
|
15
|
+
export declare function removeProtocolSchemeAsDefault(host: HasProtocolDefault, scheme: string): boolean;
|
|
16
|
+
export declare function setProtocolSchemeAsDefault(host: HasProtocolDefault, scheme: string): boolean;
|
|
17
|
+
export declare function unregisterProtocolScheme(host: HasProtocolUnregistration, scheme: string): boolean;
|
|
18
|
+
export declare function unregisterProtocolSchemes(host: HasProtocolUnregistration, schemes: readonly string[]): boolean;
|
|
25
19
|
//# sourceMappingURL=protocol.d.ts.map
|
package/dist/protocol.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,4BAA4B,EAC5B,yBAAyB,EACzB,iBAAiB,EACjB,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAO3F;AAED,wBAAgB,qBAAqB,IAAI,eAAe,CAEvD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,GAAG,MAAM,CAkBrF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAGpE;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAGrE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAE3E;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,uBAAuB,GAAG,SAAS,MAAM,EAAE,CAE7F;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzF;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,4BAA4B,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEtG;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAK7D;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAwCtE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAE7F;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAQ1G;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAE/F;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAE5F;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,yBAAyB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEjG;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAQ9G"}
|
package/dist/protocol.js
CHANGED
|
@@ -1,28 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// arrived before this call (buffered by the backend between process start and first attach) and
|
|
5
|
-
// emits each one before activating the live subscription. Pair with
|
|
6
|
-
// detachProtocolHandler/disposeProtocolHandler.
|
|
7
|
-
export function attachProtocolHandler(handler) {
|
|
1
|
+
import { createEntity } from '@flighthq/entity/contract';
|
|
2
|
+
import { clearSignal, createSignal, emitSignal } from '@flighthq/signals/contract';
|
|
3
|
+
export function attachProtocolHandler(host, handler) {
|
|
8
4
|
detachProtocolHandler(handler);
|
|
9
|
-
const backend =
|
|
10
|
-
|
|
11
|
-
const pending = backend.drainPendingUrls();
|
|
12
|
-
for (const url of pending) {
|
|
13
|
-
emitSignal(handler.onOpenUrl, url);
|
|
14
|
-
}
|
|
15
|
-
const unsubscribe = backend.subscribe((url) => emitSignal(handler.onOpenUrl, url));
|
|
16
|
-
_subscriptions.set(handler, unsubscribe);
|
|
5
|
+
const backend = host.protocol.open;
|
|
6
|
+
_subscriptions.set(handler, backend.subscribe((url) => emitSignal(handler.onOpenUrl, url)));
|
|
17
7
|
}
|
|
18
|
-
// Allocates a ProtocolHandler event entity with an inert signal; call attachProtocolHandler to start
|
|
19
|
-
// delivery.
|
|
20
8
|
export function createProtocolHandler() {
|
|
21
|
-
return { onOpenUrl: createSignal() };
|
|
9
|
+
return createEntity({ onOpenUrl: createSignal() });
|
|
22
10
|
}
|
|
23
|
-
// Builds a protocol URL string from its parsed components. scheme, host, path, and query are all
|
|
24
|
-
// optional; the result is always a valid absolute URI string. Round-trips with parseProtocolUrl for
|
|
25
|
-
// well-formed inputs.
|
|
26
11
|
export function createProtocolUrl(parts) {
|
|
27
12
|
const scheme = parts.scheme ?? 'unknown';
|
|
28
13
|
const host = parts.host ?? '';
|
|
@@ -32,77 +17,36 @@ export function createProtocolUrl(parts) {
|
|
|
32
17
|
const normalizedPath = path && !path.startsWith('/') ? `/${path}` : path;
|
|
33
18
|
let url = `${scheme}:${authority}${normalizedPath}`;
|
|
34
19
|
if (query) {
|
|
35
|
-
const entries = Object.entries(query).filter(([
|
|
20
|
+
const entries = Object.entries(query).filter(([key]) => key.length > 0);
|
|
36
21
|
if (entries.length > 0) {
|
|
37
|
-
const
|
|
38
|
-
|
|
22
|
+
const queryString = entries
|
|
23
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
24
|
+
.join('&');
|
|
25
|
+
url += `?${queryString}`;
|
|
39
26
|
}
|
|
40
27
|
}
|
|
41
28
|
return url;
|
|
42
29
|
}
|
|
43
|
-
// Stops delivery to `handler` and forgets its subscription. Safe to call when not attached.
|
|
44
30
|
export function detachProtocolHandler(handler) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
unsubscribe();
|
|
48
|
-
_subscriptions.delete(handler);
|
|
49
|
-
}
|
|
31
|
+
_subscriptions.get(handler)?.();
|
|
32
|
+
_subscriptions.delete(handler);
|
|
50
33
|
}
|
|
51
|
-
// Releases `handler` for garbage collection by detaching its backend subscription. The signal remains
|
|
52
|
-
// plain GC-managed memory afterward.
|
|
53
34
|
export function disposeProtocolHandler(handler) {
|
|
54
35
|
detachProtocolHandler(handler);
|
|
36
|
+
clearSignal(handler.onOpenUrl);
|
|
55
37
|
}
|
|
56
|
-
export function
|
|
57
|
-
|
|
58
|
-
return { conflict: _hostConflict, layer: 'custom', operation: null, viability: 'unobserved' };
|
|
59
|
-
}
|
|
60
|
-
if (_host !== null) {
|
|
61
|
-
return {
|
|
62
|
-
conflict: _hostConflict,
|
|
63
|
-
layer: 'host',
|
|
64
|
-
operation: _hostObservation !== null ? _hostObservation.operation : null,
|
|
65
|
-
viability: _hostObservation !== null ? _hostObservation.viability : 'unobserved',
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
return { conflict: false, layer: 'host-not-enabled', operation: null, viability: 'unobserved' };
|
|
38
|
+
export function getProtocolLaunchUrl(host) {
|
|
39
|
+
return host.protocol.launch.getLaunchUrl();
|
|
69
40
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return _custom ?? _host ?? _sentinel;
|
|
41
|
+
export function getRegisteredProtocolSchemes(host) {
|
|
42
|
+
return host.protocol.registration.getRegisteredSchemes();
|
|
73
43
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// onOpenUrl, which fires only for warm (subsequent) opens.
|
|
77
|
-
export function getProtocolLaunchUrl() {
|
|
78
|
-
return getProtocolBackend().getLaunchUrl();
|
|
79
|
-
}
|
|
80
|
-
// Returns all custom URI schemes currently registered by this app. Returns [] when the host cannot
|
|
81
|
-
// enumerate registered schemes.
|
|
82
|
-
export function getRegisteredProtocolSchemes() {
|
|
83
|
-
return getProtocolBackend().getRegisteredSchemes();
|
|
84
|
-
}
|
|
85
|
-
export function installProtocolHostBackend(backend) {
|
|
86
|
-
if (_host !== null) {
|
|
87
|
-
if (_host !== backend)
|
|
88
|
-
_hostConflict = true;
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
_host = backend;
|
|
44
|
+
export function isProtocolSchemeDefault(host, scheme) {
|
|
45
|
+
return isValidProtocolScheme(scheme) && host.protocol.default.isDefault(scheme);
|
|
92
46
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
export function isProtocolSchemeDefault(scheme) {
|
|
96
|
-
return getProtocolBackend().isDefault(scheme);
|
|
47
|
+
export function isProtocolSchemeRegistered(host, scheme) {
|
|
48
|
+
return isValidProtocolScheme(scheme) && host.protocol.registrationQuery.isRegistered(scheme);
|
|
97
49
|
}
|
|
98
|
-
// True when `scheme` is currently registered to this app. Returns false where the host cannot report it.
|
|
99
|
-
export function isProtocolSchemeRegistered(scheme) {
|
|
100
|
-
return getProtocolBackend().isRegistered(scheme);
|
|
101
|
-
}
|
|
102
|
-
// Returns true when `scheme` is a valid RFC 3986 URI scheme: starts with an ASCII letter followed by
|
|
103
|
-
// zero or more letters, digits, '+', '-', or '.'. Reserved schemes ('http', 'https', 'ftp', 'ftps',
|
|
104
|
-
// 'mailto', 'file') are rejected to prevent OS conflicts. Scheme is lowercased before validation so
|
|
105
|
-
// 'MyApp' is treated identically to 'myapp'.
|
|
106
50
|
export function isValidProtocolScheme(scheme) {
|
|
107
51
|
if (typeof scheme !== 'string' || scheme.length === 0)
|
|
108
52
|
return false;
|
|
@@ -111,177 +55,93 @@ export function isValidProtocolScheme(scheme) {
|
|
|
111
55
|
return false;
|
|
112
56
|
return _schemePattern.test(lower);
|
|
113
57
|
}
|
|
114
|
-
export function observeProtocolHostResult(operation, succeeded) {
|
|
115
|
-
_hostObservation = {
|
|
116
|
-
operation,
|
|
117
|
-
viability: succeeded ? 'available' : 'runtime-api-unavailable',
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
// Parses a deep-link URL into its components. Returns null for malformed or non-custom-scheme URLs.
|
|
121
|
-
// query values are percent-decoded. Multi-value query keys: last value wins (matches URLSearchParams
|
|
122
|
-
// behavior). This function does not allocate a large object on the happy path — it reads all fields
|
|
123
|
-
// before writing to avoid aliasing issues if the caller reuses a buffer.
|
|
124
58
|
export function parseProtocolUrl(url) {
|
|
125
59
|
if (typeof url !== 'string' || url.length === 0)
|
|
126
60
|
return null;
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
61
|
+
const colonIndex = url.indexOf(':');
|
|
62
|
+
if (colonIndex <= 0)
|
|
129
63
|
return null;
|
|
130
|
-
const scheme = url.slice(0,
|
|
64
|
+
const scheme = url.slice(0, colonIndex).toLowerCase();
|
|
131
65
|
if (!_schemePattern.test(scheme))
|
|
132
66
|
return null;
|
|
133
|
-
let rest = url.slice(
|
|
134
|
-
// Authority (//host)
|
|
67
|
+
let rest = url.slice(colonIndex + 1);
|
|
135
68
|
let host = '';
|
|
136
69
|
if (rest.startsWith('//')) {
|
|
137
70
|
rest = rest.slice(2);
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
hostEnd = qIdx;
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
hostEnd = rest.length;
|
|
149
|
-
}
|
|
71
|
+
const slashIndex = rest.indexOf('/');
|
|
72
|
+
const queryIndex = rest.indexOf('?');
|
|
73
|
+
const hostEnd = slashIndex >= 0 && (queryIndex < 0 || slashIndex < queryIndex)
|
|
74
|
+
? slashIndex
|
|
75
|
+
: queryIndex >= 0
|
|
76
|
+
? queryIndex
|
|
77
|
+
: rest.length;
|
|
150
78
|
host = rest.slice(0, hostEnd);
|
|
151
79
|
rest = rest.slice(hostEnd);
|
|
152
80
|
}
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
let queryString;
|
|
157
|
-
if (qIdx >= 0) {
|
|
158
|
-
path = rest.slice(0, qIdx);
|
|
159
|
-
queryString = rest.slice(qIdx + 1);
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
path = rest;
|
|
163
|
-
queryString = '';
|
|
164
|
-
}
|
|
165
|
-
// Decode query
|
|
81
|
+
const queryIndex = rest.indexOf('?');
|
|
82
|
+
const path = queryIndex >= 0 ? rest.slice(0, queryIndex) : rest;
|
|
83
|
+
const queryString = queryIndex >= 0 ? rest.slice(queryIndex + 1) : '';
|
|
166
84
|
const query = {};
|
|
167
85
|
if (queryString.length > 0) {
|
|
168
86
|
for (const pair of queryString.split('&')) {
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
173
|
-
query[
|
|
87
|
+
const equalsIndex = pair.indexOf('=');
|
|
88
|
+
if (equalsIndex < 0) {
|
|
89
|
+
const key = safeDecodeProtocolComponent(pair);
|
|
90
|
+
if (key.length > 0)
|
|
91
|
+
query[key] = '';
|
|
174
92
|
}
|
|
175
93
|
else {
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
178
|
-
query[
|
|
94
|
+
const key = safeDecodeProtocolComponent(pair.slice(0, equalsIndex));
|
|
95
|
+
if (key.length > 0)
|
|
96
|
+
query[key] = safeDecodeProtocolComponent(pair.slice(equalsIndex + 1));
|
|
179
97
|
}
|
|
180
98
|
}
|
|
181
99
|
}
|
|
182
|
-
return {
|
|
100
|
+
return { host, path, query, scheme };
|
|
183
101
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// registration.
|
|
187
|
-
export function registerProtocolScheme(scheme) {
|
|
188
|
-
if (!isValidProtocolScheme(scheme))
|
|
189
|
-
return false;
|
|
190
|
-
return getProtocolBackend().register(scheme);
|
|
102
|
+
export function registerProtocolScheme(host, scheme) {
|
|
103
|
+
return isValidProtocolScheme(scheme) && host.protocol.registration.register(scheme);
|
|
191
104
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const backend =
|
|
196
|
-
let
|
|
105
|
+
export function registerProtocolSchemes(host, schemes) {
|
|
106
|
+
if (!schemes.every(isValidProtocolScheme))
|
|
107
|
+
return false;
|
|
108
|
+
const backend = host.protocol.registration;
|
|
109
|
+
let allSucceeded = true;
|
|
197
110
|
for (const scheme of schemes) {
|
|
198
|
-
if (!
|
|
199
|
-
|
|
111
|
+
if (!backend.register(scheme))
|
|
112
|
+
allSucceeded = false;
|
|
200
113
|
}
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
|
-
// Removes this app as the OS default handler for `scheme`. Returns false when the host denies or does
|
|
204
|
-
// not support it.
|
|
205
|
-
export function removeProtocolSchemeAsDefault(scheme) {
|
|
206
|
-
return getProtocolBackend().removeAsDefault(scheme);
|
|
114
|
+
return allSucceeded;
|
|
207
115
|
}
|
|
208
|
-
export function
|
|
209
|
-
|
|
210
|
-
_host = null;
|
|
211
|
-
_hostConflict = false;
|
|
212
|
-
_hostObservation = null;
|
|
116
|
+
export function removeProtocolSchemeAsDefault(host, scheme) {
|
|
117
|
+
return isValidProtocolScheme(scheme) && host.protocol.default.removeAsDefault(scheme);
|
|
213
118
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
_custom = backend;
|
|
119
|
+
export function setProtocolSchemeAsDefault(host, scheme) {
|
|
120
|
+
return isValidProtocolScheme(scheme) && host.protocol.default.setAsDefault(scheme);
|
|
217
121
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
export function setProtocolSchemeAsDefault(scheme) {
|
|
221
|
-
return getProtocolBackend().setAsDefault(scheme);
|
|
122
|
+
export function unregisterProtocolScheme(host, scheme) {
|
|
123
|
+
return isValidProtocolScheme(scheme) && host.protocol.unregistration.unregister(scheme);
|
|
222
124
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// Unregisters multiple custom URI schemes in one call. Returns false if any unregistration fails.
|
|
229
|
-
export function unregisterProtocolSchemes(schemes) {
|
|
230
|
-
const backend = getProtocolBackend();
|
|
231
|
-
let allOk = true;
|
|
125
|
+
export function unregisterProtocolSchemes(host, schemes) {
|
|
126
|
+
if (!schemes.every(isValidProtocolScheme))
|
|
127
|
+
return false;
|
|
128
|
+
const backend = host.protocol.unregistration;
|
|
129
|
+
let allSucceeded = true;
|
|
232
130
|
for (const scheme of schemes) {
|
|
233
131
|
if (!backend.unregister(scheme))
|
|
234
|
-
|
|
132
|
+
allSucceeded = false;
|
|
235
133
|
}
|
|
236
|
-
return
|
|
134
|
+
return allSucceeded;
|
|
237
135
|
}
|
|
238
|
-
const _sentinel = {
|
|
239
|
-
drainPendingUrls() {
|
|
240
|
-
return [];
|
|
241
|
-
},
|
|
242
|
-
getLaunchUrl() {
|
|
243
|
-
return null;
|
|
244
|
-
},
|
|
245
|
-
getRegisteredSchemes() {
|
|
246
|
-
return [];
|
|
247
|
-
},
|
|
248
|
-
isDefault() {
|
|
249
|
-
return false;
|
|
250
|
-
},
|
|
251
|
-
isRegistered() {
|
|
252
|
-
return false;
|
|
253
|
-
},
|
|
254
|
-
register() {
|
|
255
|
-
return false;
|
|
256
|
-
},
|
|
257
|
-
removeAsDefault() {
|
|
258
|
-
return false;
|
|
259
|
-
},
|
|
260
|
-
setAsDefault() {
|
|
261
|
-
return false;
|
|
262
|
-
},
|
|
263
|
-
subscribe() {
|
|
264
|
-
return () => { };
|
|
265
|
-
},
|
|
266
|
-
unregister() {
|
|
267
|
-
return false;
|
|
268
|
-
},
|
|
269
|
-
};
|
|
270
|
-
// RFC 3986 scheme grammar: letter followed by zero or more letter/digit/+/-/. (lowercased).
|
|
271
136
|
const _schemePattern = /^[a-z][a-z0-9+\-.]*$/;
|
|
272
|
-
// Schemes that should never be claimed as custom URI handlers to avoid OS conflicts.
|
|
273
137
|
const _reservedSchemes = new Set(['file', 'ftp', 'ftps', 'http', 'https', 'mailto']);
|
|
274
|
-
let _custom = null;
|
|
275
|
-
let _host = null;
|
|
276
|
-
let _hostConflict = false;
|
|
277
|
-
let _hostObservation = null;
|
|
278
138
|
const _subscriptions = new WeakMap();
|
|
279
|
-
function
|
|
139
|
+
function safeDecodeProtocolComponent(value) {
|
|
280
140
|
try {
|
|
281
|
-
return decodeURIComponent(
|
|
141
|
+
return decodeURIComponent(value.replace(/\+/g, ' '));
|
|
282
142
|
}
|
|
283
143
|
catch {
|
|
284
|
-
return
|
|
144
|
+
return value;
|
|
285
145
|
}
|
|
286
146
|
}
|
|
287
147
|
//# sourceMappingURL=protocol.js.map
|
package/dist/protocol.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/protocol",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1-edge.638.d4dbd4e",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,8 +38,9 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/
|
|
42
|
-
"@flighthq/
|
|
41
|
+
"@flighthq/entity": "0.5.1-edge.638.d4dbd4e",
|
|
42
|
+
"@flighthq/signals": "0.5.1-edge.638.d4dbd4e",
|
|
43
|
+
"@flighthq/types": "0.5.1-edge.638.d4dbd4e"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"typescript": "^5.3.0"
|
package/src/protocol.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity/contract';
|
|
1
2
|
import { connectSignal } from '@flighthq/signals/contract';
|
|
2
|
-
import
|
|
3
|
+
import { EntityRuntimeKey } from '@flighthq/types/contract';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
5
|
|
|
4
6
|
import {
|
|
5
7
|
attachProtocolHandler,
|
|
@@ -7,554 +9,202 @@ import {
|
|
|
7
9
|
createProtocolUrl,
|
|
8
10
|
detachProtocolHandler,
|
|
9
11
|
disposeProtocolHandler,
|
|
10
|
-
explainProtocolBackend,
|
|
11
|
-
getProtocolBackend,
|
|
12
12
|
getProtocolLaunchUrl,
|
|
13
13
|
getRegisteredProtocolSchemes,
|
|
14
|
-
installProtocolHostBackend,
|
|
15
14
|
isProtocolSchemeDefault,
|
|
16
15
|
isProtocolSchemeRegistered,
|
|
17
16
|
isValidProtocolScheme,
|
|
18
|
-
observeProtocolHostResult,
|
|
19
17
|
parseProtocolUrl,
|
|
20
18
|
registerProtocolScheme,
|
|
21
19
|
registerProtocolSchemes,
|
|
22
20
|
removeProtocolSchemeAsDefault,
|
|
23
|
-
resetProtocolBackendForTest,
|
|
24
|
-
setProtocolBackend,
|
|
25
21
|
setProtocolSchemeAsDefault,
|
|
26
22
|
unregisterProtocolScheme,
|
|
27
23
|
unregisterProtocolSchemes,
|
|
28
24
|
} from './protocol';
|
|
29
25
|
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return this.registered;
|
|
57
|
-
},
|
|
58
|
-
getRegisteredSchemes() {
|
|
59
|
-
return registeredSchemes.slice();
|
|
60
|
-
},
|
|
61
|
-
setAsDefault(_scheme) {
|
|
62
|
-
this.defaulted = true;
|
|
63
|
-
return true;
|
|
64
|
-
},
|
|
65
|
-
isDefault(_scheme) {
|
|
66
|
-
return this.defaulted;
|
|
67
|
-
},
|
|
68
|
-
removeAsDefault(_scheme) {
|
|
69
|
-
this.defaulted = false;
|
|
70
|
-
return true;
|
|
71
|
-
},
|
|
72
|
-
getLaunchUrl() {
|
|
73
|
-
return this.launchUrl;
|
|
74
|
-
},
|
|
75
|
-
drainPendingUrls() {
|
|
76
|
-
const drained = this.pendingUrls.slice();
|
|
77
|
-
this.pendingUrls = [];
|
|
78
|
-
return drained;
|
|
79
|
-
},
|
|
80
|
-
subscribe(l) {
|
|
81
|
-
listener = l;
|
|
82
|
-
return () => {
|
|
83
|
-
listener = null;
|
|
84
|
-
};
|
|
85
|
-
},
|
|
86
|
-
fire(url) {
|
|
87
|
-
listener?.(url);
|
|
26
|
+
function createFixture() {
|
|
27
|
+
const calls: string[] = [];
|
|
28
|
+
let open: ((url: string) => void) | null = null;
|
|
29
|
+
const unsubscribe = vi.fn();
|
|
30
|
+
const host = {
|
|
31
|
+
protocol: {
|
|
32
|
+
default: createEntity({
|
|
33
|
+
isDefault: (scheme: string) => scheme === 'flight',
|
|
34
|
+
removeAsDefault: (scheme: string) => (calls.push(`removeDefault:${scheme}`), true),
|
|
35
|
+
setAsDefault: (scheme: string) => (calls.push(`default:${scheme}`), true),
|
|
36
|
+
}),
|
|
37
|
+
launch: createEntity({ getLaunchUrl: () => 'flight://cold-start' }),
|
|
38
|
+
open: createEntity({
|
|
39
|
+
subscribe(listener: (url: string) => void) {
|
|
40
|
+
open = listener;
|
|
41
|
+
return unsubscribe;
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
registration: createEntity({
|
|
45
|
+
getRegisteredSchemes: () => ['flight'],
|
|
46
|
+
register: (scheme: string) => (calls.push(`register:${scheme}`), scheme !== 'fail'),
|
|
47
|
+
}),
|
|
48
|
+
registrationQuery: createEntity({ isRegistered: (scheme: string) => scheme === 'flight' }),
|
|
49
|
+
unregistration: createEntity({
|
|
50
|
+
unregister: (scheme: string) => (calls.push(`unregister:${scheme}`), scheme !== 'fail'),
|
|
51
|
+
}),
|
|
88
52
|
},
|
|
89
53
|
};
|
|
54
|
+
return { calls, emitOpen: (url: string) => open?.(url), host, unsubscribe };
|
|
90
55
|
}
|
|
91
56
|
|
|
92
|
-
afterEach(() => resetProtocolBackendForTest());
|
|
93
|
-
|
|
94
57
|
describe('attachProtocolHandler', () => {
|
|
95
|
-
it('
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
setProtocolBackend(backend);
|
|
99
|
-
const handler = createProtocolHandler();
|
|
100
|
-
const received: string[] = [];
|
|
101
|
-
connectSignal(handler.onOpenUrl, (url) => received.push(url));
|
|
102
|
-
attachProtocolHandler(handler);
|
|
103
|
-
expect(received).toEqual(['myapp://early/1', 'myapp://early/2']);
|
|
104
|
-
// Pending queue is cleared — a second attach does not re-deliver them.
|
|
105
|
-
attachProtocolHandler(handler);
|
|
106
|
-
expect(received).toEqual(['myapp://early/1', 'myapp://early/2']);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('emits onOpenUrl when the backend delivers a deep link', () => {
|
|
110
|
-
const backend = fakeBackend();
|
|
111
|
-
setProtocolBackend(backend);
|
|
58
|
+
it('forwards live opens and replaces a prior subscription', () => {
|
|
59
|
+
const first = createFixture();
|
|
60
|
+
const second = createFixture();
|
|
112
61
|
const handler = createProtocolHandler();
|
|
113
|
-
|
|
114
|
-
connectSignal(handler.onOpenUrl,
|
|
115
|
-
attachProtocolHandler(handler);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
it('is idempotent — reattaching does not double-fire', () => {
|
|
121
|
-
const backend = fakeBackend();
|
|
122
|
-
setProtocolBackend(backend);
|
|
123
|
-
const handler = createProtocolHandler();
|
|
124
|
-
let count = 0;
|
|
125
|
-
connectSignal(handler.onOpenUrl, () => count++);
|
|
126
|
-
attachProtocolHandler(handler);
|
|
127
|
-
attachProtocolHandler(handler);
|
|
128
|
-
backend.fire('myapp://x');
|
|
129
|
-
expect(count).toBe(1);
|
|
62
|
+
const receive = vi.fn();
|
|
63
|
+
connectSignal(handler.onOpenUrl, receive);
|
|
64
|
+
attachProtocolHandler(first.host, handler);
|
|
65
|
+
attachProtocolHandler(second.host, handler);
|
|
66
|
+
second.emitOpen('flight://warm-open');
|
|
67
|
+
expect(first.unsubscribe).toHaveBeenCalledOnce();
|
|
68
|
+
expect(receive).toHaveBeenCalledExactlyOnceWith('flight://warm-open');
|
|
130
69
|
});
|
|
131
70
|
});
|
|
132
71
|
|
|
133
72
|
describe('createProtocolHandler', () => {
|
|
134
|
-
it('creates an
|
|
73
|
+
it('creates an Entity carrying the open-url signal', () => {
|
|
135
74
|
const handler = createProtocolHandler();
|
|
75
|
+
expect(EntityRuntimeKey in handler).toBe(true);
|
|
136
76
|
expect(handler.onOpenUrl).toBeDefined();
|
|
137
77
|
});
|
|
138
78
|
});
|
|
139
79
|
|
|
140
80
|
describe('createProtocolUrl', () => {
|
|
141
|
-
it('
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
it('omits authority when host is empty', () => {
|
|
147
|
-
const url = createProtocolUrl({ scheme: 'myapp', path: '/action' });
|
|
148
|
-
expect(url).toBe('myapp:/action');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('omits query when no query params', () => {
|
|
152
|
-
const url = createProtocolUrl({ scheme: 'myapp', host: 'x' });
|
|
153
|
-
expect(url).toBe('myapp://x');
|
|
81
|
+
it('normalizes the path and encodes query components', () => {
|
|
82
|
+
expect(
|
|
83
|
+
createProtocolUrl({ host: 'open', path: 'file', query: { name: 'a b', '': 'ignored' }, scheme: 'flight' }),
|
|
84
|
+
).toBe('flight://open/file?name=a%20b');
|
|
154
85
|
});
|
|
155
86
|
|
|
156
|
-
it('
|
|
157
|
-
|
|
158
|
-
expect(url).toContain('a%20b=c%20d');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('round-trips with parseProtocolUrl', () => {
|
|
162
|
-
const parts = { scheme: 'myapp', host: 'host', path: '/path', query: { k: 'v' } };
|
|
163
|
-
const url = createProtocolUrl(parts);
|
|
164
|
-
const parsed = parseProtocolUrl(url);
|
|
165
|
-
expect(parsed?.scheme).toBe('myapp');
|
|
166
|
-
expect(parsed?.host).toBe('host');
|
|
167
|
-
expect(parsed?.path).toBe('/path');
|
|
168
|
-
expect(parsed?.query['k']).toBe('v');
|
|
87
|
+
it('supports authority-free URLs', () => {
|
|
88
|
+
expect(createProtocolUrl({ path: '/open', scheme: 'flight' })).toBe('flight:/open');
|
|
169
89
|
});
|
|
170
90
|
});
|
|
171
91
|
|
|
172
92
|
describe('detachProtocolHandler', () => {
|
|
173
|
-
it('
|
|
174
|
-
const
|
|
175
|
-
setProtocolBackend(backend);
|
|
93
|
+
it('unsubscribes the current provider exactly once', () => {
|
|
94
|
+
const fixture = createFixture();
|
|
176
95
|
const handler = createProtocolHandler();
|
|
177
|
-
|
|
178
|
-
connectSignal(handler.onOpenUrl, () => opened++);
|
|
179
|
-
attachProtocolHandler(handler);
|
|
96
|
+
attachProtocolHandler(fixture.host, handler);
|
|
180
97
|
detachProtocolHandler(handler);
|
|
181
|
-
|
|
182
|
-
expect(
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('is safe when not attached', () => {
|
|
186
|
-
const handler = createProtocolHandler();
|
|
187
|
-
expect(() => detachProtocolHandler(handler)).not.toThrow();
|
|
98
|
+
detachProtocolHandler(handler);
|
|
99
|
+
expect(fixture.unsubscribe).toHaveBeenCalledOnce();
|
|
188
100
|
});
|
|
189
101
|
});
|
|
190
102
|
|
|
191
103
|
describe('disposeProtocolHandler', () => {
|
|
192
|
-
it('detaches
|
|
193
|
-
const
|
|
194
|
-
setProtocolBackend(backend);
|
|
104
|
+
it('detaches and clears signal listeners', () => {
|
|
105
|
+
const fixture = createFixture();
|
|
195
106
|
const handler = createProtocolHandler();
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
it('stops delivery after dispose', () => {
|
|
201
|
-
const backend = fakeBackend();
|
|
202
|
-
setProtocolBackend(backend);
|
|
203
|
-
const handler = createProtocolHandler();
|
|
204
|
-
let count = 0;
|
|
205
|
-
connectSignal(handler.onOpenUrl, () => count++);
|
|
206
|
-
attachProtocolHandler(handler);
|
|
107
|
+
const receive = vi.fn();
|
|
108
|
+
connectSignal(handler.onOpenUrl, receive);
|
|
109
|
+
attachProtocolHandler(fixture.host, handler);
|
|
207
110
|
disposeProtocolHandler(handler);
|
|
208
|
-
|
|
209
|
-
expect(
|
|
210
|
-
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
describe('explainProtocolBackend', () => {
|
|
214
|
-
it('reports host-not-enabled when no backend is installed', () => {
|
|
215
|
-
const explanation = explainProtocolBackend();
|
|
216
|
-
expect(explanation.layer).toBe('host-not-enabled');
|
|
217
|
-
expect(explanation.conflict).toBe(false);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('reports custom layer when a custom backend is set', () => {
|
|
221
|
-
setProtocolBackend(fakeBackend());
|
|
222
|
-
const explanation = explainProtocolBackend();
|
|
223
|
-
expect(explanation.layer).toBe('custom');
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('reports host layer when a host backend is installed', () => {
|
|
227
|
-
installProtocolHostBackend(fakeBackend());
|
|
228
|
-
const explanation = explainProtocolBackend();
|
|
229
|
-
expect(explanation.layer).toBe('host');
|
|
230
|
-
expect(explanation.viability).toBe('unobserved');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('reports observation viability after observeProtocolHostResult', () => {
|
|
234
|
-
installProtocolHostBackend(fakeBackend());
|
|
235
|
-
observeProtocolHostResult('register', true);
|
|
236
|
-
const explanation = explainProtocolBackend();
|
|
237
|
-
expect(explanation.operation).toBe('register');
|
|
238
|
-
expect(explanation.viability).toBe('available');
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('reports conflict when two different host backends are installed', () => {
|
|
242
|
-
installProtocolHostBackend(fakeBackend());
|
|
243
|
-
installProtocolHostBackend(fakeBackend());
|
|
244
|
-
const explanation = explainProtocolBackend();
|
|
245
|
-
expect(explanation.conflict).toBe(true);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
describe('getProtocolBackend', () => {
|
|
250
|
-
it('returns the sentinel when no backend is installed', () => {
|
|
251
|
-
const backend = getProtocolBackend();
|
|
252
|
-
expect(backend).not.toBeNull();
|
|
253
|
-
expect(backend.isRegistered('myapp')).toBe(false);
|
|
254
|
-
expect(backend.drainPendingUrls()).toEqual([]);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('returns the installed custom backend', () => {
|
|
258
|
-
const backend = fakeBackend();
|
|
259
|
-
setProtocolBackend(backend);
|
|
260
|
-
expect(getProtocolBackend()).toBe(backend);
|
|
111
|
+
fixture.emitOpen('flight://ignored');
|
|
112
|
+
expect(fixture.unsubscribe).toHaveBeenCalledOnce();
|
|
113
|
+
expect(receive).not.toHaveBeenCalled();
|
|
261
114
|
});
|
|
262
115
|
});
|
|
263
116
|
|
|
264
117
|
describe('getProtocolLaunchUrl', () => {
|
|
265
|
-
it('returns
|
|
266
|
-
|
|
267
|
-
backend.launchUrl = null;
|
|
268
|
-
setProtocolBackend(backend);
|
|
269
|
-
expect(getProtocolLaunchUrl()).toBeNull();
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('returns the launch URL when present', () => {
|
|
273
|
-
const backend = fakeBackend();
|
|
274
|
-
backend.launchUrl = 'myapp://cold-start/item';
|
|
275
|
-
setProtocolBackend(backend);
|
|
276
|
-
expect(getProtocolLaunchUrl()).toBe('myapp://cold-start/item');
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('is idempotent — returns the same value on repeated calls', () => {
|
|
280
|
-
const backend = fakeBackend();
|
|
281
|
-
backend.launchUrl = 'myapp://x';
|
|
282
|
-
setProtocolBackend(backend);
|
|
283
|
-
expect(getProtocolLaunchUrl()).toBe('myapp://x');
|
|
284
|
-
expect(getProtocolLaunchUrl()).toBe('myapp://x');
|
|
118
|
+
it('returns the cold-start provider fact', () => {
|
|
119
|
+
expect(getProtocolLaunchUrl(createFixture().host)).toBe('flight://cold-start');
|
|
285
120
|
});
|
|
286
121
|
});
|
|
287
122
|
|
|
288
123
|
describe('getRegisteredProtocolSchemes', () => {
|
|
289
|
-
it('returns
|
|
290
|
-
|
|
291
|
-
setProtocolBackend(backend);
|
|
292
|
-
expect(getRegisteredProtocolSchemes()).toEqual([]);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('includes schemes that have been registered', () => {
|
|
296
|
-
const backend = fakeBackend();
|
|
297
|
-
setProtocolBackend(backend);
|
|
298
|
-
registerProtocolScheme('myapp');
|
|
299
|
-
expect(getRegisteredProtocolSchemes()).toContain('myapp');
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
describe('installProtocolHostBackend', () => {
|
|
304
|
-
it('installs the host backend', () => {
|
|
305
|
-
const backend = fakeBackend();
|
|
306
|
-
installProtocolHostBackend(backend);
|
|
307
|
-
expect(getProtocolBackend()).toBe(backend);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('first-host-wins — ignores a second different backend', () => {
|
|
311
|
-
const first = fakeBackend();
|
|
312
|
-
const second = fakeBackend();
|
|
313
|
-
installProtocolHostBackend(first);
|
|
314
|
-
installProtocolHostBackend(second);
|
|
315
|
-
expect(getProtocolBackend()).toBe(first);
|
|
124
|
+
it('returns the provider schemes', () => {
|
|
125
|
+
expect(getRegisteredProtocolSchemes(createFixture().host)).toEqual(['flight']);
|
|
316
126
|
});
|
|
317
127
|
});
|
|
318
128
|
|
|
319
129
|
describe('isProtocolSchemeDefault', () => {
|
|
320
|
-
it('
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
expect(isProtocolSchemeDefault('
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it('returns true after setProtocolSchemeAsDefault', () => {
|
|
327
|
-
const backend = fakeBackend();
|
|
328
|
-
setProtocolBackend(backend);
|
|
329
|
-
setProtocolSchemeAsDefault('myapp');
|
|
330
|
-
expect(isProtocolSchemeDefault('myapp')).toBe(true);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it('returns false after removeProtocolSchemeAsDefault', () => {
|
|
334
|
-
const backend = fakeBackend();
|
|
335
|
-
setProtocolBackend(backend);
|
|
336
|
-
setProtocolSchemeAsDefault('myapp');
|
|
337
|
-
removeProtocolSchemeAsDefault('myapp');
|
|
338
|
-
expect(isProtocolSchemeDefault('myapp')).toBe(false);
|
|
130
|
+
it('validates before querying the provider', () => {
|
|
131
|
+
const host = createFixture().host;
|
|
132
|
+
expect(isProtocolSchemeDefault(host, 'flight')).toBe(true);
|
|
133
|
+
expect(isProtocolSchemeDefault(host, 'https')).toBe(false);
|
|
339
134
|
});
|
|
340
135
|
});
|
|
341
|
-
|
|
342
136
|
describe('isProtocolSchemeRegistered', () => {
|
|
343
|
-
it('
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
expect(isProtocolSchemeRegistered('
|
|
347
|
-
registerProtocolScheme('myapp');
|
|
348
|
-
expect(isProtocolSchemeRegistered('myapp')).toBe(true);
|
|
137
|
+
it('validates before querying the provider', () => {
|
|
138
|
+
const host = createFixture().host;
|
|
139
|
+
expect(isProtocolSchemeRegistered(host, 'flight')).toBe(true);
|
|
140
|
+
expect(isProtocolSchemeRegistered(host, 'https')).toBe(false);
|
|
349
141
|
});
|
|
350
142
|
});
|
|
351
|
-
|
|
352
143
|
describe('isValidProtocolScheme', () => {
|
|
353
|
-
it('accepts
|
|
354
|
-
expect(isValidProtocolScheme('
|
|
355
|
-
expect(isValidProtocolScheme('my-app')).toBe(true);
|
|
356
|
-
expect(isValidProtocolScheme('my.app')).toBe(true);
|
|
357
|
-
expect(isValidProtocolScheme('my+app')).toBe(true);
|
|
358
|
-
expect(isValidProtocolScheme('app123')).toBe(true);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('rejects schemes not starting with a letter', () => {
|
|
362
|
-
expect(isValidProtocolScheme('1app')).toBe(false);
|
|
363
|
-
expect(isValidProtocolScheme('-app')).toBe(false);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it('rejects empty and non-string inputs', () => {
|
|
367
|
-
expect(isValidProtocolScheme('')).toBe(false);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it('rejects reserved schemes', () => {
|
|
371
|
-
expect(isValidProtocolScheme('http')).toBe(false);
|
|
144
|
+
it('accepts custom grammar and rejects reserved or malformed schemes', () => {
|
|
145
|
+
expect(isValidProtocolScheme('flight+desktop')).toBe(true);
|
|
372
146
|
expect(isValidProtocolScheme('https')).toBe(false);
|
|
373
|
-
expect(isValidProtocolScheme('
|
|
374
|
-
expect(isValidProtocolScheme('mailto')).toBe(false);
|
|
375
|
-
expect(isValidProtocolScheme('file')).toBe(false);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it('is case-insensitive — lowercases before validation', () => {
|
|
379
|
-
expect(isValidProtocolScheme('MyApp')).toBe(true);
|
|
380
|
-
expect(isValidProtocolScheme('HTTP')).toBe(false);
|
|
147
|
+
expect(isValidProtocolScheme('1flight')).toBe(false);
|
|
381
148
|
});
|
|
382
149
|
});
|
|
383
|
-
|
|
384
|
-
describe('observeProtocolHostResult', () => {
|
|
385
|
-
it('records observation in the explanation', () => {
|
|
386
|
-
installProtocolHostBackend(fakeBackend());
|
|
387
|
-
observeProtocolHostResult('register', false);
|
|
388
|
-
const explanation = explainProtocolBackend();
|
|
389
|
-
expect(explanation.operation).toBe('register');
|
|
390
|
-
expect(explanation.viability).toBe('runtime-api-unavailable');
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
|
|
394
150
|
describe('parseProtocolUrl', () => {
|
|
395
|
-
it('parses
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it('parses a URL with no host', () => {
|
|
405
|
-
const result = parseProtocolUrl('myapp:/action');
|
|
406
|
-
expect(result?.scheme).toBe('myapp');
|
|
407
|
-
expect(result?.host).toBe('');
|
|
408
|
-
expect(result?.path).toBe('/action');
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it('parses a URL with no path', () => {
|
|
412
|
-
const result = parseProtocolUrl('myapp://host');
|
|
413
|
-
expect(result?.scheme).toBe('myapp');
|
|
414
|
-
expect(result?.host).toBe('host');
|
|
415
|
-
expect(result?.path).toBe('');
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it('decodes percent-encoded query values', () => {
|
|
419
|
-
const result = parseProtocolUrl('myapp://x?key=hello%20world');
|
|
420
|
-
expect(result?.query['key']).toBe('hello world');
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it('last-value-wins for duplicate query keys', () => {
|
|
424
|
-
const result = parseProtocolUrl('myapp://x?k=first&k=second');
|
|
425
|
-
expect(result?.query['k']).toBe('second');
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it('returns null for malformed or empty input', () => {
|
|
429
|
-
expect(parseProtocolUrl('')).toBeNull();
|
|
430
|
-
expect(parseProtocolUrl('notaurl')).toBeNull();
|
|
431
|
-
expect(parseProtocolUrl(':noscheme')).toBeNull();
|
|
151
|
+
it('parses host, path, decoded query, and lowercase scheme', () => {
|
|
152
|
+
expect(parseProtocolUrl('FLIGHT://open/file?name=a%20b&empty')).toEqual({
|
|
153
|
+
host: 'open',
|
|
154
|
+
path: '/file',
|
|
155
|
+
query: { empty: '', name: 'a b' },
|
|
156
|
+
scheme: 'flight',
|
|
157
|
+
});
|
|
432
158
|
});
|
|
433
159
|
|
|
434
|
-
it('
|
|
435
|
-
|
|
436
|
-
expect(
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it('handles URL with no query string', () => {
|
|
440
|
-
const result = parseProtocolUrl('myapp://host/path');
|
|
441
|
-
expect(result?.query).toEqual({});
|
|
160
|
+
it('returns null for malformed URLs and preserves malformed escapes', () => {
|
|
161
|
+
expect(parseProtocolUrl('not-a-url')).toBeNull();
|
|
162
|
+
expect(parseProtocolUrl('flight:/open?value=%ZZ')?.query).toEqual({ value: '%ZZ' });
|
|
442
163
|
});
|
|
443
164
|
});
|
|
444
|
-
|
|
445
165
|
describe('registerProtocolScheme', () => {
|
|
446
|
-
it('
|
|
447
|
-
|
|
448
|
-
expect(registerProtocolScheme('
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
it('returns false for an invalid scheme without calling the backend', () => {
|
|
452
|
-
const backend = fakeBackend();
|
|
453
|
-
setProtocolBackend(backend);
|
|
454
|
-
expect(registerProtocolScheme('123invalid')).toBe(false);
|
|
455
|
-
expect(backend.registered).toBe(false);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it('returns false for a reserved scheme', () => {
|
|
459
|
-
setProtocolBackend(fakeBackend());
|
|
460
|
-
expect(registerProtocolScheme('https')).toBe(false);
|
|
166
|
+
it('delegates a valid scheme', () => {
|
|
167
|
+
const { calls, host } = createFixture();
|
|
168
|
+
expect(registerProtocolScheme(host, 'flight')).toBe(true);
|
|
169
|
+
expect(calls).toEqual(['register:flight']);
|
|
461
170
|
});
|
|
462
171
|
});
|
|
463
|
-
|
|
464
172
|
describe('registerProtocolSchemes', () => {
|
|
465
|
-
it('
|
|
466
|
-
|
|
467
|
-
expect(registerProtocolSchemes(['
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const backend = fakeBackend();
|
|
472
|
-
let callCount = 0;
|
|
473
|
-
const orig = backend.register.bind(backend);
|
|
474
|
-
backend.register = (scheme) => {
|
|
475
|
-
callCount++;
|
|
476
|
-
return callCount === 1 ? orig(scheme) : false;
|
|
477
|
-
};
|
|
478
|
-
setProtocolBackend(backend);
|
|
479
|
-
expect(registerProtocolSchemes(['myapp', 'myapp2'])).toBe(false);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
it('returns false and skips backend for invalid schemes in the batch', () => {
|
|
483
|
-
const backend = fakeBackend();
|
|
484
|
-
setProtocolBackend(backend);
|
|
485
|
-
expect(registerProtocolSchemes(['myapp', 'http'])).toBe(false);
|
|
486
|
-
// 'myapp' succeeded but 'http' failed — registered reflects the partial success
|
|
487
|
-
expect(backend.registered).toBe(true);
|
|
173
|
+
it('prevalidates the full batch and aggregates provider outcomes', () => {
|
|
174
|
+
const fixture = createFixture();
|
|
175
|
+
expect(registerProtocolSchemes(fixture.host, ['flight', 'https'])).toBe(false);
|
|
176
|
+
expect(fixture.calls).toEqual([]);
|
|
177
|
+
expect(registerProtocolSchemes(fixture.host, ['flight', 'fail'])).toBe(false);
|
|
178
|
+
expect(fixture.calls).toEqual(['register:flight', 'register:fail']);
|
|
488
179
|
});
|
|
489
180
|
});
|
|
490
|
-
|
|
491
181
|
describe('removeProtocolSchemeAsDefault', () => {
|
|
492
|
-
it('
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
expect(removeProtocolSchemeAsDefault('myapp')).toBe(true);
|
|
497
|
-
expect(isProtocolSchemeDefault('myapp')).toBe(false);
|
|
182
|
+
it('delegates a valid scheme', () => {
|
|
183
|
+
const { calls, host } = createFixture();
|
|
184
|
+
expect(removeProtocolSchemeAsDefault(host, 'flight')).toBe(true);
|
|
185
|
+
expect(calls).toEqual(['removeDefault:flight']);
|
|
498
186
|
});
|
|
499
187
|
});
|
|
500
|
-
|
|
501
|
-
describe('resetProtocolBackendForTest', () => {
|
|
502
|
-
it('clears all backend slots', () => {
|
|
503
|
-
setProtocolBackend(fakeBackend());
|
|
504
|
-
installProtocolHostBackend(fakeBackend());
|
|
505
|
-
resetProtocolBackendForTest();
|
|
506
|
-
const explanation = explainProtocolBackend();
|
|
507
|
-
expect(explanation.layer).toBe('host-not-enabled');
|
|
508
|
-
expect(explanation.conflict).toBe(false);
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
describe('setProtocolBackend', () => {
|
|
513
|
-
it('clears back to the sentinel when passed null', () => {
|
|
514
|
-
setProtocolBackend(fakeBackend());
|
|
515
|
-
setProtocolBackend(null);
|
|
516
|
-
const backend = getProtocolBackend();
|
|
517
|
-
expect(backend).not.toBeNull();
|
|
518
|
-
expect(backend.isRegistered('myapp')).toBe(false);
|
|
519
|
-
});
|
|
520
|
-
});
|
|
521
|
-
|
|
522
188
|
describe('setProtocolSchemeAsDefault', () => {
|
|
523
|
-
it('
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
expect(
|
|
527
|
-
expect(backend.defaulted).toBe(true);
|
|
189
|
+
it('delegates a valid scheme', () => {
|
|
190
|
+
const { calls, host } = createFixture();
|
|
191
|
+
expect(setProtocolSchemeAsDefault(host, 'flight')).toBe(true);
|
|
192
|
+
expect(calls).toEqual(['default:flight']);
|
|
528
193
|
});
|
|
529
194
|
});
|
|
530
|
-
|
|
531
195
|
describe('unregisterProtocolScheme', () => {
|
|
532
|
-
it('
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
expect(unregisterProtocolScheme('myapp')).toBe(true);
|
|
537
|
-
expect(backend.registered).toBe(false);
|
|
196
|
+
it('delegates a valid scheme', () => {
|
|
197
|
+
const { calls, host } = createFixture();
|
|
198
|
+
expect(unregisterProtocolScheme(host, 'flight')).toBe(true);
|
|
199
|
+
expect(calls).toEqual(['unregister:flight']);
|
|
538
200
|
});
|
|
539
201
|
});
|
|
540
|
-
|
|
541
202
|
describe('unregisterProtocolSchemes', () => {
|
|
542
|
-
it('
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
expect(unregisterProtocolSchemes(['
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
it('returns false if any unregistration fails', () => {
|
|
550
|
-
const backend = fakeBackend();
|
|
551
|
-
let callCount = 0;
|
|
552
|
-
const orig = backend.unregister.bind(backend);
|
|
553
|
-
backend.unregister = (scheme) => {
|
|
554
|
-
callCount++;
|
|
555
|
-
return callCount === 1 ? orig(scheme) : false;
|
|
556
|
-
};
|
|
557
|
-
setProtocolBackend(backend);
|
|
558
|
-
expect(unregisterProtocolSchemes(['myapp', 'myapp2'])).toBe(false);
|
|
203
|
+
it('prevalidates the full batch and aggregates provider outcomes', () => {
|
|
204
|
+
const fixture = createFixture();
|
|
205
|
+
expect(unregisterProtocolSchemes(fixture.host, ['flight', 'https'])).toBe(false);
|
|
206
|
+
expect(fixture.calls).toEqual([]);
|
|
207
|
+
expect(unregisterProtocolSchemes(fixture.host, ['flight', 'fail'])).toBe(false);
|
|
208
|
+
expect(fixture.calls).toEqual(['unregister:flight', 'unregister:fail']);
|
|
559
209
|
});
|
|
560
210
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity/contract';
|
|
2
|
+
import { connectSignal } from '@flighthq/signals/contract';
|
|
3
|
+
import { EntityRuntimeKey } from '@flighthq/types/contract';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import * as protocolContract from './contract';
|
|
7
|
+
import { attachProtocolHandler, createProtocolHandler, getProtocolLaunchUrl, registerProtocolScheme } from './protocol';
|
|
8
|
+
|
|
9
|
+
describe('protocol explicit Host ownership', () => {
|
|
10
|
+
it('uses a promoted top-level protocol group for independent command and query slots', () => {
|
|
11
|
+
const register = vi.fn(() => true);
|
|
12
|
+
const getLaunchUrl = vi.fn(() => 'flight://cold-start');
|
|
13
|
+
const host = {
|
|
14
|
+
protocol: {
|
|
15
|
+
launch: createEntity({ getLaunchUrl }),
|
|
16
|
+
registration: createEntity({ getRegisteredSchemes: () => ['flight'], register }),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
expect(Reflect.apply(registerProtocolScheme, undefined, [host, 'flight'])).toBe(true);
|
|
21
|
+
expect(Reflect.apply(getProtocolLaunchUrl, undefined, [host])).toBe('flight://cold-start');
|
|
22
|
+
expect(register).toHaveBeenCalledExactlyOnceWith('flight');
|
|
23
|
+
expect(getLaunchUrl).toHaveBeenCalledOnce();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('takes the live-open provider from Host and publishes an Entity', () => {
|
|
27
|
+
const listeners: { open?: (url: string) => void } = {};
|
|
28
|
+
const subscribe = vi.fn((listener: (url: string) => void) => {
|
|
29
|
+
listeners.open = listener;
|
|
30
|
+
return vi.fn();
|
|
31
|
+
});
|
|
32
|
+
const host = { protocol: { open: createEntity({ subscribe }) } };
|
|
33
|
+
const handler = createProtocolHandler();
|
|
34
|
+
let received = '';
|
|
35
|
+
connectSignal(handler.onOpenUrl, (url) => (received = url));
|
|
36
|
+
|
|
37
|
+
Reflect.apply(attachProtocolHandler, undefined, [host, handler]);
|
|
38
|
+
listeners.open?.('flight://warm-open');
|
|
39
|
+
|
|
40
|
+
expect(subscribe).toHaveBeenCalledOnce();
|
|
41
|
+
expect(received).toBe('flight://warm-open');
|
|
42
|
+
expect(EntityRuntimeKey in handler).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('deletes the ambient resolver family instead of retaining a parallel API', () => {
|
|
46
|
+
expect(protocolContract).not.toHaveProperty('explainProtocolBackend');
|
|
47
|
+
expect(protocolContract).not.toHaveProperty('getProtocolBackend');
|
|
48
|
+
expect(protocolContract).not.toHaveProperty('installProtocolHostBackend');
|
|
49
|
+
expect(protocolContract).not.toHaveProperty('observeProtocolHostResult');
|
|
50
|
+
expect(protocolContract).not.toHaveProperty('resetProtocolBackendForTest');
|
|
51
|
+
expect(protocolContract).not.toHaveProperty('setProtocolBackend');
|
|
52
|
+
});
|
|
53
|
+
});
|