@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 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.0-next.1944.cd6b6e7](https://github.com/flighthq/flight/tree/cd6b6e71eb472b3a4249830a7743333f019fa7b7/packages/protocol)
17
- - [License](https://github.com/flighthq/flight/blob/cd6b6e71eb472b3a4249830a7743333f019fa7b7/LICENSE.md)
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, explainProtocolBackend, getProtocolLaunchUrl, getRegisteredProtocolSchemes, isProtocolSchemeDefault, isProtocolSchemeRegistered, isValidProtocolScheme, parseProtocolUrl, registerProtocolScheme, registerProtocolSchemes, removeProtocolSchemeAsDefault, setProtocolSchemeAsDefault, unregisterProtocolScheme, unregisterProtocolSchemes, } from './contract';
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
@@ -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,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"}
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, explainProtocolBackend, getProtocolLaunchUrl, getRegisteredProtocolSchemes, isProtocolSchemeDefault, isProtocolSchemeRegistered, isValidProtocolScheme, parseProtocolUrl, registerProtocolScheme, registerProtocolSchemes, removeProtocolSchemeAsDefault, setProtocolSchemeAsDefault, unregisterProtocolScheme, unregisterProtocolSchemes, } from './contract';
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,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"}
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"}
@@ -1,25 +1,19 @@
1
- import type { BackendExplanation, ParsedProtocolUrl, ProtocolBackend, ProtocolHandler } from '@flighthq/types/contract';
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 explainProtocolBackend(): BackendExplanation;
8
- export declare function getProtocolBackend(): ProtocolBackend;
9
- export declare function getProtocolLaunchUrl(): string | null;
10
- export declare function getRegisteredProtocolSchemes(): readonly string[];
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 resetProtocolBackendForTest(): void;
21
- export declare function setProtocolBackend(backend: ProtocolBackend | null): void;
22
- export declare function setProtocolSchemeAsDefault(scheme: string): boolean;
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
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAOxH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAUpE;AAID,wBAAgB,qBAAqB,IAAI,eAAe,CAEvD;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,GAAG,MAAM,CAgBrF;AAGD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAMpE;AAID,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAErE;AAED,wBAAgB,sBAAsB,IAAI,kBAAkB,CAa3D;AAGD,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AAKD,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAEpD;AAID,wBAAgB,4BAA4B,IAAI,SAAS,MAAM,EAAE,CAEhE;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAMzE;AAID,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE/D;AAGD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAElE;AAMD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAK7D;AAED,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CAKrF;AAMD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAuDtE;AAKD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAG9D;AAID,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAO3E;AAID,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErE;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI,CAExE;AAID,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAElE;AAID,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEhE;AAGD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAO7E"}
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 { createSignal, emitSignal } from '@flighthq/signals/contract';
2
- // Begins delivering deep-link opens to `handler`'s signal by subscribing to the active backend. Wires
3
- // subscribe→onOpenUrl. Idempotent: a prior subscription is torn down first. Drains any URLs that
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 = getProtocolBackend();
10
- // Drain URLs that arrived between process start and first attach (pre-attach burst).
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(([k]) => k.length > 0);
20
+ const entries = Object.entries(query).filter(([key]) => key.length > 0);
36
21
  if (entries.length > 0) {
37
- const qs = entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
38
- url += `?${qs}`;
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
- const unsubscribe = _subscriptions.get(handler);
46
- if (unsubscribe !== undefined) {
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 explainProtocolBackend() {
57
- if (_custom !== null) {
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
- // The active protocol backend. Precedence: custom > host > sentinel.
71
- export function getProtocolBackend() {
72
- return _custom ?? _host ?? _sentinel;
41
+ export function getRegisteredProtocolSchemes(host) {
42
+ return host.protocol.registration.getRegisteredSchemes();
73
43
  }
74
- // Returns the URL the app was launched with via a deep link (cold start), or null when the app was
75
- // not launched via a link. This is a one-shot idempotent query — re-readable. Distinct from
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
- // True when `scheme` is the OS default handler for deep links. Returns false where the host cannot
94
- // report it (e.g. the web platform).
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 colonIdx = url.indexOf(':');
128
- if (colonIdx <= 0)
61
+ const colonIndex = url.indexOf(':');
62
+ if (colonIndex <= 0)
129
63
  return null;
130
- const scheme = url.slice(0, colonIdx).toLowerCase();
64
+ const scheme = url.slice(0, colonIndex).toLowerCase();
131
65
  if (!_schemePattern.test(scheme))
132
66
  return null;
133
- let rest = url.slice(colonIdx + 1);
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 slashIdx = rest.indexOf('/');
139
- const qIdx = rest.indexOf('?');
140
- let hostEnd;
141
- if (slashIdx >= 0 && (qIdx < 0 || slashIdx < qIdx)) {
142
- hostEnd = slashIdx;
143
- }
144
- else if (qIdx >= 0) {
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
- // Path and query
154
- const qIdx = rest.indexOf('?');
155
- let path;
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 eqIdx = pair.indexOf('=');
170
- if (eqIdx < 0) {
171
- const k = _safeDecode(pair);
172
- if (k.length > 0)
173
- query[k] = '';
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 k = _safeDecode(pair.slice(0, eqIdx));
177
- if (k.length > 0)
178
- query[k] = _safeDecode(pair.slice(eqIdx + 1));
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 { scheme, host, path, query };
100
+ return { host, path, query, scheme };
183
101
  }
184
- // Registers a custom URI scheme (for example 'myapp') to this app. Returns false when the scheme is
185
- // invalid (RFC 3986 grammar, no reserved schemes) or when the host denies or does not support
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
- // Registers multiple custom URI schemes in one call. Returns false if any scheme is invalid or if any
193
- // registration fails.
194
- export function registerProtocolSchemes(schemes) {
195
- const backend = getProtocolBackend();
196
- let allOk = true;
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 (!isValidProtocolScheme(scheme) || !backend.register(scheme))
199
- allOk = false;
111
+ if (!backend.register(scheme))
112
+ allSucceeded = false;
200
113
  }
201
- return allOk;
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 resetProtocolBackendForTest() {
209
- _custom = null;
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
- // Installs a custom protocol backend; pass null to clear the custom override.
215
- export function setProtocolBackend(backend) {
216
- _custom = backend;
119
+ export function setProtocolSchemeAsDefault(host, scheme) {
120
+ return isValidProtocolScheme(scheme) && host.protocol.default.setAsDefault(scheme);
217
121
  }
218
- // Makes this app the default handler for `scheme`. Returns false when the host denies or does not
219
- // support it.
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
- // Unregisters a previously registered custom URI scheme. Returns false when the host denies or does
224
- // not support unregistration.
225
- export function unregisterProtocolScheme(scheme) {
226
- return getProtocolBackend().unregister(scheme);
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
- allOk = false;
132
+ allSucceeded = false;
235
133
  }
236
- return allOk;
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 _safeDecode(s) {
139
+ function safeDecodeProtocolComponent(value) {
280
140
  try {
281
- return decodeURIComponent(s.replace(/\+/g, ' '));
141
+ return decodeURIComponent(value.replace(/\+/g, ' '));
282
142
  }
283
143
  catch {
284
- return s;
144
+ return value;
285
145
  }
286
146
  }
287
147
  //# sourceMappingURL=protocol.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
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.0-next.1944.cd6b6e7",
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/signals": "0.5.0-next.1944.cd6b6e7",
42
- "@flighthq/types": "0.5.0-next.1944.cd6b6e7"
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"
@@ -1,5 +1,7 @@
1
+ import { createEntity } from '@flighthq/entity/contract';
1
2
  import { connectSignal } from '@flighthq/signals/contract';
2
- import type { ProtocolBackend } from '@flighthq/types/contract';
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 fakeBackend(): ProtocolBackend & {
31
- registered: boolean;
32
- defaulted: boolean;
33
- launchUrl: string | null;
34
- pendingUrls: string[];
35
- fire: (url: string) => void;
36
- } {
37
- let listener: ((url: string) => void) | null = null;
38
- const registeredSchemes: string[] = [];
39
- return {
40
- registered: false,
41
- defaulted: false,
42
- launchUrl: null,
43
- pendingUrls: [],
44
- register(_scheme) {
45
- this.registered = true;
46
- if (!registeredSchemes.includes(_scheme)) registeredSchemes.push(_scheme);
47
- return true;
48
- },
49
- unregister(_scheme) {
50
- this.registered = false;
51
- const idx = registeredSchemes.indexOf(_scheme);
52
- if (idx >= 0) registeredSchemes.splice(idx, 1);
53
- return true;
54
- },
55
- isRegistered(_scheme) {
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('drains pre-attach pending URLs before subscribing', () => {
96
- const backend = fakeBackend();
97
- backend.pendingUrls = ['myapp://early/1', 'myapp://early/2'];
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
- let opened = '';
114
- connectSignal(handler.onOpenUrl, (url) => (opened = url));
115
- attachProtocolHandler(handler);
116
- backend.fire('myapp://open/123');
117
- expect(opened).toBe('myapp://open/123');
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 entity with the open-URL signal', () => {
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('builds a basic URL from all parts', () => {
142
- const url = createProtocolUrl({ scheme: 'myapp', host: 'open', path: '/item/1', query: { ref: 'home' } });
143
- expect(url).toBe('myapp://open/item/1?ref=home');
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('percent-encodes query keys and values', () => {
157
- const url = createProtocolUrl({ scheme: 'myapp', query: { 'a b': 'c d' } });
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('stops further delivery', () => {
174
- const backend = fakeBackend();
175
- setProtocolBackend(backend);
93
+ it('unsubscribes the current provider exactly once', () => {
94
+ const fixture = createFixture();
176
95
  const handler = createProtocolHandler();
177
- let opened = 0;
178
- connectSignal(handler.onOpenUrl, () => opened++);
179
- attachProtocolHandler(handler);
96
+ attachProtocolHandler(fixture.host, handler);
180
97
  detachProtocolHandler(handler);
181
- backend.fire('myapp://x');
182
- expect(opened).toBe(0);
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 the subscription', () => {
193
- const backend = fakeBackend();
194
- setProtocolBackend(backend);
104
+ it('detaches and clears signal listeners', () => {
105
+ const fixture = createFixture();
195
106
  const handler = createProtocolHandler();
196
- attachProtocolHandler(handler);
197
- expect(() => disposeProtocolHandler(handler)).not.toThrow();
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
- backend.fire('myapp://x');
209
- expect(count).toBe(0);
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 null when no launch URL', () => {
266
- const backend = fakeBackend();
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 empty when no schemes registered', () => {
290
- const backend = fakeBackend();
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('returns false by default', () => {
321
- const backend = fakeBackend();
322
- setProtocolBackend(backend);
323
- expect(isProtocolSchemeDefault('myapp')).toBe(false);
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('reflects the backend registration state', () => {
344
- const backend = fakeBackend();
345
- setProtocolBackend(backend);
346
- expect(isProtocolSchemeRegistered('myapp')).toBe(false);
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 valid custom schemes', () => {
354
- expect(isValidProtocolScheme('myapp')).toBe(true);
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('ftp')).toBe(false);
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 a full URL with host, path, and query', () => {
396
- const result = parseProtocolUrl('myapp://host/path?foo=bar&baz=qux');
397
- expect(result?.scheme).toBe('myapp');
398
- expect(result?.host).toBe('host');
399
- expect(result?.path).toBe('/path');
400
- expect(result?.query['foo']).toBe('bar');
401
- expect(result?.query['baz']).toBe('qux');
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('lowercases the scheme', () => {
435
- const result = parseProtocolUrl('MyApp://host');
436
- expect(result?.scheme).toBe('myapp');
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('registers through the backend', () => {
447
- setProtocolBackend(fakeBackend());
448
- expect(registerProtocolScheme('myapp')).toBe(true);
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('registers multiple schemes and returns true when all succeed', () => {
466
- setProtocolBackend(fakeBackend());
467
- expect(registerProtocolSchemes(['myapp', 'myapp2'])).toBe(true);
468
- });
469
-
470
- it('returns false if any registration fails', () => {
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('removes default status through the backend', () => {
493
- const backend = fakeBackend();
494
- setProtocolBackend(backend);
495
- setProtocolSchemeAsDefault('myapp');
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('marks the scheme as default through the backend', () => {
524
- const backend = fakeBackend();
525
- setProtocolBackend(backend);
526
- expect(setProtocolSchemeAsDefault('myapp')).toBe(true);
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('unregisters through the backend', () => {
533
- const backend = fakeBackend();
534
- setProtocolBackend(backend);
535
- registerProtocolScheme('myapp');
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('unregisters multiple schemes and returns true when all succeed', () => {
543
- const backend = fakeBackend();
544
- setProtocolBackend(backend);
545
- registerProtocolScheme('myapp');
546
- expect(unregisterProtocolSchemes(['myapp', 'myapp2'])).toBe(true);
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
+ });