@mentra/crust 3.2.1 → 3.3.0-beta.1000
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 +27 -0
- package/android/build.gradle +24 -9
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/mentra/crust/CrustModule.kt +44 -15
- package/android/src/main/java/com/mentra/crust/http/NativeHttpFailure.kt +31 -0
- package/android/src/main/java/com/mentra/crust/http/NativeHttpRequests.kt +86 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +69 -54
- package/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt +65 -8
- package/android/src/main/java/com/mentra/crust/jsc/JSDeliveryDiagnostics.kt +44 -0
- package/android/src/main/java/com/mentra/crust/navigation/NavigationManager.kt +4 -0
- package/android/src/main/java/com/mentra/crust/preview/PixelCopyPreview.kt +76 -0
- package/android/src/test/java/com/mentra/crust/http/NativeHttpFailureTest.kt +44 -0
- package/android/src/test/java/com/mentra/crust/http/NativeHttpRequestsTest.kt +133 -0
- package/android/src/test/java/com/mentra/crust/jsc/FetchBridgeTest.kt +199 -0
- package/android/src/test/java/com/mentra/crust/jsc/JSDeliveryDiagnosticsTest.kt +54 -0
- package/android/src/test/java/com/mentra/crust/jsc/QuickJsLifecycleTest.kt +87 -0
- package/build/Crust.types.d.ts +25 -11
- package/build/Crust.types.d.ts.map +1 -1
- package/build/Crust.types.js.map +1 -1
- package/build/CrustModule.d.ts +9 -2
- package/build/CrustModule.d.ts.map +1 -1
- package/build/CrustModule.js.map +1 -1
- package/build/CrustModule.web.d.ts +3 -1
- package/build/CrustModule.web.d.ts.map +1 -1
- package/build/CrustModule.web.js +20 -8
- package/build/CrustModule.web.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/nativeHttpRequest.d.ts +13 -0
- package/build/nativeHttpRequest.d.ts.map +1 -0
- package/build/nativeHttpRequest.js +51 -0
- package/build/nativeHttpRequest.js.map +1 -0
- package/build/nativeHttpRequest.test.d.ts +2 -0
- package/build/nativeHttpRequest.test.d.ts.map +1 -0
- package/build/nativeHttpRequest.test.js +88 -0
- package/build/nativeHttpRequest.test.js.map +1 -0
- package/ios/Crust.podspec +1 -1
- package/ios/CrustModule.swift +18 -7
- package/ios/PermissionStatusReceipt.swift +105 -0
- package/ios/Resources/startup.js +154 -68
- package/ios/Source/JSCPolyfillBridge.swift +31 -14
- package/package.json +2 -2
- package/plugin/build/index.d.ts +2 -0
- package/plugin/build/index.js +2 -0
- package/plugin/build/withAndroid.js +39 -0
- package/src/Crust.types.ts +26 -12
- package/src/CrustModule.ts +9 -1
- package/src/CrustModule.web.ts +18 -7
- package/src/index.ts +1 -0
- package/src/nativeHttpRequest.test.ts +94 -0
- package/src/nativeHttpRequest.ts +74 -0
- package/android/src/internal/AndroidManifest.xml +0 -9
- package/android/src/internal/java/com/mentra/crust/receivers/CaptionsTesterIncidentReceiver.kt +0 -46
package/build/index.d.ts
CHANGED
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAA;AACrC,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,aAAa,CAAA;AAChD,cAAc,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAA;AACrC,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,aAAa,CAAA;AAChD,cAAc,eAAe,CAAA;AAC7B,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAA"}
|
package/build/index.js
CHANGED
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,4CAA4C;AAC5C,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAA;AACrC,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,aAAa,CAAA;AAChD,cAAc,eAAe,CAAA","sourcesContent":["// Reexport the native module. On web, it will be resolved to CrustModule.web.ts\n// and on native platforms to CrustModule.ts\nexport {default} from \"./CrustModule\"\nexport {default as CrustView} from \"./CrustView\"\nexport * from \"./Crust.types\"\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,4CAA4C;AAC5C,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAA;AACrC,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,aAAa,CAAA;AAChD,cAAc,eAAe,CAAA;AAC7B,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAA","sourcesContent":["// Reexport the native module. On web, it will be resolved to CrustModule.web.ts\n// and on native platforms to CrustModule.ts\nexport {default} from \"./CrustModule\"\nexport {default as CrustView} from \"./CrustView\"\nexport * from \"./Crust.types\"\nexport {nativeHttpRequest} from \"./nativeHttpRequest\"\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface NativeHttpResult {
|
|
2
|
+
status: number;
|
|
3
|
+
statusText: string;
|
|
4
|
+
headers: Record<string, string>;
|
|
5
|
+
body: string;
|
|
6
|
+
}
|
|
7
|
+
export interface NativeHttpTransport {
|
|
8
|
+
nativeHttpRequest(requestId: string, method: string, url: string, headers: Record<string, string>, body: string | null): Promise<NativeHttpResult>;
|
|
9
|
+
cancelNativeHttpRequest(requestId: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/** Keep Android's background-safe HTTP path while cancelling its actual native request. */
|
|
12
|
+
export declare function nativeHttpRequest(native: NativeHttpTransport, method: string, url: string, headers: Record<string, string>, body: string | null, signal?: AbortSignal): Promise<NativeHttpResult>;
|
|
13
|
+
//# sourceMappingURL=nativeHttpRequest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nativeHttpRequest.d.ts","sourceRoot":"","sources":["../src/nativeHttpRequest.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,mBAAmB;IAClC,iBAAiB,CACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC5B,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1D;AAWD,2FAA2F;AAC3F,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAsC3B"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
let requestSequence = 0;
|
|
2
|
+
function abortReason(signal) {
|
|
3
|
+
if (signal.reason !== undefined)
|
|
4
|
+
return signal.reason;
|
|
5
|
+
const error = new Error("Native HTTP request aborted");
|
|
6
|
+
error.name = "AbortError";
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
/** Keep Android's background-safe HTTP path while cancelling its actual native request. */
|
|
10
|
+
export function nativeHttpRequest(native, method, url, headers, body, signal) {
|
|
11
|
+
if (signal?.aborted)
|
|
12
|
+
return Promise.reject(abortReason(signal));
|
|
13
|
+
const requestId = `http-${Date.now().toString(36)}-${++requestSequence}`;
|
|
14
|
+
// Start first, then subscribe: Expo delivers start and cancel on the same serial queue.
|
|
15
|
+
const request = native.nativeHttpRequest(requestId, method, url, headers, body);
|
|
16
|
+
if (!signal)
|
|
17
|
+
return request;
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
let settled = false;
|
|
20
|
+
let cancelling = false;
|
|
21
|
+
const finish = (action) => {
|
|
22
|
+
if (settled)
|
|
23
|
+
return;
|
|
24
|
+
settled = true;
|
|
25
|
+
signal.removeEventListener("abort", onAbort);
|
|
26
|
+
action();
|
|
27
|
+
};
|
|
28
|
+
const onAbort = () => {
|
|
29
|
+
if (settled || cancelling)
|
|
30
|
+
return;
|
|
31
|
+
cancelling = true;
|
|
32
|
+
void native.cancelNativeHttpRequest(requestId).then(() => finish(() => reject(abortReason(signal))), (error) => finish(() => reject(error)));
|
|
33
|
+
};
|
|
34
|
+
request.then((result) => {
|
|
35
|
+
if (signal.aborted)
|
|
36
|
+
onAbort();
|
|
37
|
+
else
|
|
38
|
+
finish(() => resolve(result));
|
|
39
|
+
}, (error) => {
|
|
40
|
+
if (signal.aborted)
|
|
41
|
+
onAbort();
|
|
42
|
+
else
|
|
43
|
+
finish(() => reject(error));
|
|
44
|
+
});
|
|
45
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
46
|
+
// Handles an abort fired synchronously during native registration.
|
|
47
|
+
if (signal.aborted)
|
|
48
|
+
onAbort();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=nativeHttpRequest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nativeHttpRequest.js","sourceRoot":"","sources":["../src/nativeHttpRequest.ts"],"names":[],"mappings":"AAkBA,IAAI,eAAe,GAAG,CAAC,CAAA;AAEvB,SAAS,WAAW,CAAC,MAAmB;IACtC,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,MAAM,CAAA;IACrD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAA;IACtD,KAAK,CAAC,IAAI,GAAG,YAAY,CAAA;IACzB,OAAO,KAAK,CAAA;AACd,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,iBAAiB,CAC/B,MAA2B,EAC3B,MAAc,EACd,GAAW,EACX,OAA+B,EAC/B,IAAmB,EACnB,MAAoB;IAEpB,IAAI,MAAM,EAAE,OAAO;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAA;IAC/D,MAAM,SAAS,GAAG,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,CAAA;IACxE,wFAAwF;IACxF,MAAM,OAAO,GAAG,MAAM,CAAC,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IAC/E,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAA;IAE3B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,IAAI,UAAU,GAAG,KAAK,CAAA;QACtB,MAAM,MAAM,GAAG,CAAC,MAAkB,EAAE,EAAE;YACpC,IAAI,OAAO;gBAAE,OAAM;YACnB,OAAO,GAAG,IAAI,CAAA;YACd,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YAC5C,MAAM,EAAE,CAAA;QACV,CAAC,CAAA;QACD,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,IAAI,OAAO,IAAI,UAAU;gBAAE,OAAM;YACjC,UAAU,GAAG,IAAI,CAAA;YACjB,KAAK,MAAM,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,IAAI,CACjD,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAC/C,CAAC,KAAc,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAChD,CAAA;QACH,CAAC,CAAA;QACD,OAAO,CAAC,IAAI,CACV,CAAC,MAAM,EAAE,EAAE;YACT,IAAI,MAAM,CAAC,OAAO;gBAAE,OAAO,EAAE,CAAA;;gBACxB,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;QACpC,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;YACjB,IAAI,MAAM,CAAC,OAAO;gBAAE,OAAO,EAAE,CAAA;;gBACxB,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;QAClC,CAAC,CACF,CAAA;QACD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAC,IAAI,EAAE,IAAI,EAAC,CAAC,CAAA;QACvD,mEAAmE;QACnE,IAAI,MAAM,CAAC,OAAO;YAAE,OAAO,EAAE,CAAA;IAC/B,CAAC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["export interface NativeHttpResult {\n status: number\n statusText: string\n headers: Record<string, string>\n body: string\n}\n\nexport interface NativeHttpTransport {\n nativeHttpRequest(\n requestId: string,\n method: string,\n url: string,\n headers: Record<string, string>,\n body: string | null,\n ): Promise<NativeHttpResult>\n cancelNativeHttpRequest(requestId: string): Promise<void>\n}\n\nlet requestSequence = 0\n\nfunction abortReason(signal: AbortSignal): unknown {\n if (signal.reason !== undefined) return signal.reason\n const error = new Error(\"Native HTTP request aborted\")\n error.name = \"AbortError\"\n return error\n}\n\n/** Keep Android's background-safe HTTP path while cancelling its actual native request. */\nexport function nativeHttpRequest(\n native: NativeHttpTransport,\n method: string,\n url: string,\n headers: Record<string, string>,\n body: string | null,\n signal?: AbortSignal,\n): Promise<NativeHttpResult> {\n if (signal?.aborted) return Promise.reject(abortReason(signal))\n const requestId = `http-${Date.now().toString(36)}-${++requestSequence}`\n // Start first, then subscribe: Expo delivers start and cancel on the same serial queue.\n const request = native.nativeHttpRequest(requestId, method, url, headers, body)\n if (!signal) return request\n\n return new Promise((resolve, reject) => {\n let settled = false\n let cancelling = false\n const finish = (action: () => void) => {\n if (settled) return\n settled = true\n signal.removeEventListener(\"abort\", onAbort)\n action()\n }\n const onAbort = () => {\n if (settled || cancelling) return\n cancelling = true\n void native.cancelNativeHttpRequest(requestId).then(\n () => finish(() => reject(abortReason(signal))),\n (error: unknown) => finish(() => reject(error)),\n )\n }\n request.then(\n (result) => {\n if (signal.aborted) onAbort()\n else finish(() => resolve(result))\n },\n (error: unknown) => {\n if (signal.aborted) onAbort()\n else finish(() => reject(error))\n },\n )\n signal.addEventListener(\"abort\", onAbort, {once: true})\n // Handles an abort fired synchronously during native registration.\n if (signal.aborted) onAbort()\n })\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nativeHttpRequest.test.d.ts","sourceRoot":"","sources":["../src/nativeHttpRequest.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { nativeHttpRequest } from "./nativeHttpRequest";
|
|
2
|
+
const response = { status: 200, statusText: "OK", headers: {}, body: "{}" };
|
|
3
|
+
function deferred() {
|
|
4
|
+
let resolve;
|
|
5
|
+
let reject;
|
|
6
|
+
const promise = new Promise((yes, no) => {
|
|
7
|
+
resolve = yes;
|
|
8
|
+
reject = no;
|
|
9
|
+
});
|
|
10
|
+
return { promise, resolve, reject };
|
|
11
|
+
}
|
|
12
|
+
function transport(request = deferred()) {
|
|
13
|
+
const native = {
|
|
14
|
+
nativeHttpRequest: jest.fn(() => request.promise),
|
|
15
|
+
cancelNativeHttpRequest: jest.fn(async () => { }),
|
|
16
|
+
};
|
|
17
|
+
return { native, request };
|
|
18
|
+
}
|
|
19
|
+
describe("nativeHttpRequest", () => {
|
|
20
|
+
test("pre-aborted requests do not start or cancel native work", async () => {
|
|
21
|
+
const { native } = transport();
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const reason = new Error("deadline");
|
|
24
|
+
controller.abort(reason);
|
|
25
|
+
await expect(nativeHttpRequest(native, "GET", "https://example.com", {}, null, controller.signal)).rejects.toBe(reason);
|
|
26
|
+
expect(native.nativeHttpRequest).not.toHaveBeenCalled();
|
|
27
|
+
expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled();
|
|
28
|
+
});
|
|
29
|
+
test("abort cancels only this request and waits for native cancellation acceptance", async () => {
|
|
30
|
+
const { native, request } = transport();
|
|
31
|
+
const cancellation = deferred();
|
|
32
|
+
native.cancelNativeHttpRequest = jest.fn(() => cancellation.promise);
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const remove = jest.spyOn(controller.signal, "removeEventListener");
|
|
35
|
+
const promise = nativeHttpRequest(native, "POST", "https://example.com", { test: "header" }, "body", controller.signal);
|
|
36
|
+
const rejected = expect(promise).rejects.toMatchObject({ name: "AbortError" });
|
|
37
|
+
controller.abort();
|
|
38
|
+
const id = native.nativeHttpRequest.mock.calls[0][0];
|
|
39
|
+
expect(native.cancelNativeHttpRequest).toHaveBeenCalledWith(id);
|
|
40
|
+
expect(remove).not.toHaveBeenCalled();
|
|
41
|
+
request.reject(new Error("cancelled socket"));
|
|
42
|
+
cancellation.resolve();
|
|
43
|
+
await rejected;
|
|
44
|
+
expect(native.cancelNativeHttpRequest).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(remove).toHaveBeenCalledWith("abort", expect.any(Function));
|
|
46
|
+
});
|
|
47
|
+
test("an abort during native start is cancelled after start and removes its listener", async () => {
|
|
48
|
+
const { native } = transport();
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const order = [];
|
|
51
|
+
native.nativeHttpRequest = jest.fn(() => {
|
|
52
|
+
order.push("start");
|
|
53
|
+
controller.abort();
|
|
54
|
+
return new Promise(() => { });
|
|
55
|
+
});
|
|
56
|
+
native.cancelNativeHttpRequest = jest.fn(async () => { order.push("cancel"); });
|
|
57
|
+
await expect(nativeHttpRequest(native, "GET", "https://example.com", {}, null, controller.signal)).rejects.toMatchObject({ name: "AbortError" });
|
|
58
|
+
expect(order).toEqual(["start", "cancel"]);
|
|
59
|
+
});
|
|
60
|
+
test.each(["success", "failure"])("%s removes its abort listener and a later abort does not cancel", async (outcome) => {
|
|
61
|
+
const { native, request } = transport();
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const remove = jest.spyOn(controller.signal, "removeEventListener");
|
|
64
|
+
const promise = nativeHttpRequest(native, "GET", "https://example.com", {}, null, controller.signal);
|
|
65
|
+
if (outcome === "success") {
|
|
66
|
+
request.resolve(response);
|
|
67
|
+
await expect(promise).resolves.toBe(response);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
request.reject(new Error("offline"));
|
|
71
|
+
await expect(promise).rejects.toThrow("offline");
|
|
72
|
+
}
|
|
73
|
+
controller.abort();
|
|
74
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
test("independent calls have separate IDs and unsignalled calls keep the native path", async () => {
|
|
78
|
+
const { native, request } = transport();
|
|
79
|
+
const first = nativeHttpRequest(native, "GET", "https://example.com/first", {}, null);
|
|
80
|
+
const second = nativeHttpRequest(native, "GET", "https://example.com/second", {}, null);
|
|
81
|
+
const calls = native.nativeHttpRequest.mock.calls;
|
|
82
|
+
expect(calls[0][0]).not.toBe(calls[1][0]);
|
|
83
|
+
request.resolve(response);
|
|
84
|
+
await Promise.all([first, second]);
|
|
85
|
+
expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
//# sourceMappingURL=nativeHttpRequest.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nativeHttpRequest.test.js","sourceRoot":"","sources":["../src/nativeHttpRequest.test.ts"],"names":[],"mappings":"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","sourcesContent":["import {nativeHttpRequest, type NativeHttpResult, type NativeHttpTransport} from \"./nativeHttpRequest\"\n\nconst response: NativeHttpResult = {status: 200, statusText: \"OK\", headers: {}, body: \"{}\"}\n\nfunction deferred<T>() {\n let resolve!: (value: T) => void\n let reject!: (error: unknown) => void\n const promise = new Promise<T>((yes, no) => {\n resolve = yes\n reject = no\n })\n return {promise, resolve, reject}\n}\n\nfunction transport(request = deferred<NativeHttpResult>()) {\n const native: NativeHttpTransport = {\n nativeHttpRequest: jest.fn(() => request.promise),\n cancelNativeHttpRequest: jest.fn(async () => {}),\n }\n return {native, request}\n}\n\ndescribe(\"nativeHttpRequest\", () => {\n test(\"pre-aborted requests do not start or cancel native work\", async () => {\n const {native} = transport()\n const controller = new AbortController()\n const reason = new Error(\"deadline\")\n controller.abort(reason)\n await expect(nativeHttpRequest(native, \"GET\", \"https://example.com\", {}, null, controller.signal)).rejects.toBe(reason)\n expect(native.nativeHttpRequest).not.toHaveBeenCalled()\n expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled()\n })\n\n test(\"abort cancels only this request and waits for native cancellation acceptance\", async () => {\n const {native, request} = transport()\n const cancellation = deferred<void>()\n native.cancelNativeHttpRequest = jest.fn(() => cancellation.promise)\n const controller = new AbortController()\n const remove = jest.spyOn(controller.signal, \"removeEventListener\")\n const promise = nativeHttpRequest(native, \"POST\", \"https://example.com\", {test: \"header\"}, \"body\", controller.signal)\n const rejected = expect(promise).rejects.toMatchObject({name: \"AbortError\"})\n controller.abort()\n const id = (native.nativeHttpRequest as jest.Mock).mock.calls[0][0]\n expect(native.cancelNativeHttpRequest).toHaveBeenCalledWith(id)\n expect(remove).not.toHaveBeenCalled()\n request.reject(new Error(\"cancelled socket\"))\n cancellation.resolve()\n await rejected\n expect(native.cancelNativeHttpRequest).toHaveBeenCalledTimes(1)\n expect(remove).toHaveBeenCalledWith(\"abort\", expect.any(Function))\n })\n\n test(\"an abort during native start is cancelled after start and removes its listener\", async () => {\n const {native} = transport()\n const controller = new AbortController()\n const order: string[] = []\n native.nativeHttpRequest = jest.fn(() => {\n order.push(\"start\")\n controller.abort()\n return new Promise<NativeHttpResult>(() => {})\n })\n native.cancelNativeHttpRequest = jest.fn(async () => {order.push(\"cancel\")})\n await expect(nativeHttpRequest(native, \"GET\", \"https://example.com\", {}, null, controller.signal)).rejects.toMatchObject({name: \"AbortError\"})\n expect(order).toEqual([\"start\", \"cancel\"])\n })\n\n test.each([\"success\", \"failure\"])(\"%s removes its abort listener and a later abort does not cancel\", async (outcome) => {\n const {native, request} = transport()\n const controller = new AbortController()\n const remove = jest.spyOn(controller.signal, \"removeEventListener\")\n const promise = nativeHttpRequest(native, \"GET\", \"https://example.com\", {}, null, controller.signal)\n if (outcome === \"success\") {\n request.resolve(response)\n await expect(promise).resolves.toBe(response)\n } else {\n request.reject(new Error(\"offline\"))\n await expect(promise).rejects.toThrow(\"offline\")\n }\n controller.abort()\n expect(remove).toHaveBeenCalledTimes(1)\n expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled()\n })\n\n test(\"independent calls have separate IDs and unsignalled calls keep the native path\", async () => {\n const {native, request} = transport()\n const first = nativeHttpRequest(native, \"GET\", \"https://example.com/first\", {}, null)\n const second = nativeHttpRequest(native, \"GET\", \"https://example.com/second\", {}, null)\n const calls = (native.nativeHttpRequest as jest.Mock).mock.calls\n expect(calls[0][0]).not.toBe(calls[1][0])\n request.resolve(response)\n await Promise.all([first, second])\n expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled()\n })\n})\n"]}
|
package/ios/Crust.podspec
CHANGED
|
@@ -29,7 +29,7 @@ Pod::Spec.new do |s|
|
|
|
29
29
|
# is intentionally NO `s.dependency 'MapboxNavigationCore'` here.
|
|
30
30
|
|
|
31
31
|
# iOS frameworks required for media processing + navigation + heading
|
|
32
|
-
s.frameworks = 'AVFoundation', 'Photos', 'CoreImage', 'CoreGraphics', 'UIKit', 'CoreLocation', 'CoreMotion'
|
|
32
|
+
s.frameworks = 'AVFoundation', 'Photos', 'CoreImage', 'CoreGraphics', 'UIKit', 'CoreLocation', 'CoreMotion', 'CoreBluetooth'
|
|
33
33
|
|
|
34
34
|
# Swift/Objective-C compatibility + Mapbox SPM module visibility.
|
|
35
35
|
#
|
package/ios/CrustModule.swift
CHANGED
|
@@ -61,6 +61,13 @@ private struct PhotoLibrarySaveResult {
|
|
|
61
61
|
let timedOut: Bool
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/// MentraJS calls get their own serial queue. Expo runs every module's default-queue
|
|
65
|
+
/// AsyncFunction on one shared serial queue, so a blocking call in another module held every
|
|
66
|
+
/// host→miniapp message behind it: the ~25s offline STT model extraction after login kept
|
|
67
|
+
/// miniapps from spawning and starved running ones of PINGs until the host respawned them.
|
|
68
|
+
/// Serial, so spawn, dispatch and kill still run in the order JS issued them.
|
|
69
|
+
private let mentraJsQueue = DispatchQueue(label: "com.mentra.mentrajs-bridge", qos: .userInitiated)
|
|
70
|
+
|
|
64
71
|
public class CrustModule: Module {
|
|
65
72
|
public func definition() -> ModuleDefinition {
|
|
66
73
|
Name("Crust")
|
|
@@ -69,6 +76,11 @@ public class CrustModule: Module {
|
|
|
69
76
|
ProcessInfo.processInfo.isiOSAppOnMac
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
AsyncFunction("getPermissionStatus") { (requestId: String?) -> [String: Any] in
|
|
80
|
+
if let requestId { return try await PermissionStatusReceipt.publish(requestId: requestId) }
|
|
81
|
+
return await PermissionStatusReceipt.current()
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
Constant("PI") {
|
|
73
85
|
Double.pi
|
|
74
86
|
}
|
|
@@ -77,7 +89,6 @@ public class CrustModule: Module {
|
|
|
77
89
|
"onChange",
|
|
78
90
|
"phone_notification",
|
|
79
91
|
"phone_notification_dismissed",
|
|
80
|
-
"captions_tester_incident",
|
|
81
92
|
"onNavManeuver",
|
|
82
93
|
"onNavRerouting",
|
|
83
94
|
"onNavArrived",
|
|
@@ -283,7 +294,7 @@ public class CrustModule: Module {
|
|
|
283
294
|
polyfillBundle: polyfillBundle,
|
|
284
295
|
miniappJs: miniappJs
|
|
285
296
|
)
|
|
286
|
-
}
|
|
297
|
+
}.runOnQueue(mentraJsQueue)
|
|
287
298
|
|
|
288
299
|
/// Evaluate an arbitrary script inside the named context. Returns
|
|
289
300
|
/// the JS return value bridged to a JSON-friendly Swift type, or
|
|
@@ -291,19 +302,19 @@ public class CrustModule: Module {
|
|
|
291
302
|
/// + tests; production code paths use mentraJsDispatchToJs.
|
|
292
303
|
AsyncFunction("mentraJsEvaluate") { (packageName: String, source: String) -> Any? in
|
|
293
304
|
return JSCRuntime.shared.evaluate(packageName: packageName, source: source)
|
|
294
|
-
}
|
|
305
|
+
}.runOnQueue(mentraJsQueue)
|
|
295
306
|
|
|
296
307
|
/// Tear down a JS context. Cancels timers, drops refs, forces GC.
|
|
297
308
|
AsyncFunction("mentraJsKill") { (packageName: String) -> Void in
|
|
298
309
|
JSCRuntime.shared.kill(packageName: packageName)
|
|
299
|
-
}
|
|
310
|
+
}.runOnQueue(mentraJsQueue)
|
|
300
311
|
|
|
301
312
|
/// Push an event / response envelope into the named context's
|
|
302
313
|
/// globalThis.__deliver. Used by MentraJSRouter for
|
|
303
314
|
/// glasses-status broadcasts and request/response correlation.
|
|
304
315
|
AsyncFunction("mentraJsDispatchToJs") { (packageName: String, envelope: [String: Any]) -> Void in
|
|
305
316
|
JSCRuntime.shared.dispatchToJs(packageName: packageName, envelope: envelope)
|
|
306
|
-
}
|
|
317
|
+
}.runOnQueue(mentraJsQueue)
|
|
307
318
|
|
|
308
319
|
/// Set the installed manifest for a miniapp so the dispatcher's
|
|
309
320
|
/// permission gate can authorize sensitive `__dispatch` calls.
|
|
@@ -312,7 +323,7 @@ public class CrustModule: Module {
|
|
|
312
323
|
packageName: packageName,
|
|
313
324
|
manifest: InstalledMiniappManifest(permissions: Set(permissions))
|
|
314
325
|
)
|
|
315
|
-
}
|
|
326
|
+
}.runOnQueue(mentraJsQueue)
|
|
316
327
|
|
|
317
328
|
/// Diagnostic: list all live packageNames.
|
|
318
329
|
Function("mentraJsAlivePackages") { () -> [String] in
|
|
@@ -324,7 +335,7 @@ public class CrustModule: Module {
|
|
|
324
335
|
/// the context is dead.
|
|
325
336
|
AsyncFunction("mentraJsDebugForceGC") { (packageName: String) -> Bool in
|
|
326
337
|
return JSCRuntime.shared.debugForceGC(packageName: packageName)
|
|
327
|
-
}
|
|
338
|
+
}.runOnQueue(mentraJsQueue)
|
|
328
339
|
|
|
329
340
|
/// Read the bundled MentraJS polyfill (startup.js) from the iOS
|
|
330
341
|
/// pod's resource bundle. The host calls this once on app boot,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import CoreBluetooth
|
|
3
|
+
import CoreLocation
|
|
4
|
+
import Foundation
|
|
5
|
+
|
|
6
|
+
/// Passive app-owned observations. None of these getters requests consent,
|
|
7
|
+
/// starts capture, creates a Bluetooth central, or joins a network.
|
|
8
|
+
enum PermissionStatusReceipt {
|
|
9
|
+
static let relativePath = "MentraDiagnostics/permission-status.json"
|
|
10
|
+
|
|
11
|
+
static func state(_ rawStatus: String, _ status: String) -> [String: String] {
|
|
12
|
+
["rawStatus": rawStatus, "status": status]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static func location(_ value: CLAuthorizationStatus) -> [String: String] {
|
|
16
|
+
switch value {
|
|
17
|
+
case .notDetermined: return state("notDetermined", "denied")
|
|
18
|
+
case .denied: return state("denied", "blocked")
|
|
19
|
+
case .restricted: return state("restricted", "unavailable")
|
|
20
|
+
case .authorizedAlways: return state("authorizedAlways", "granted")
|
|
21
|
+
case .authorizedWhenInUse: return state("authorizedWhenInUse", "granted")
|
|
22
|
+
@unknown default: return state("unknown", "unknown")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static func capture(_ value: AVAuthorizationStatus) -> [String: String] {
|
|
27
|
+
switch value {
|
|
28
|
+
case .notDetermined: return state("notDetermined", "denied")
|
|
29
|
+
case .denied: return state("denied", "blocked")
|
|
30
|
+
case .restricted: return state("restricted", "unavailable")
|
|
31
|
+
case .authorized: return state("authorized", "granted")
|
|
32
|
+
@unknown default: return state("unknown", "unknown")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static func bluetooth(_ value: CBManagerAuthorization) -> [String: String] {
|
|
37
|
+
switch value {
|
|
38
|
+
case .notDetermined: return state("notDetermined", "denied")
|
|
39
|
+
case .denied: return state("denied", "blocked")
|
|
40
|
+
case .restricted: return state("restricted", "unavailable")
|
|
41
|
+
case .allowedAlways: return state("allowedAlways", "granted")
|
|
42
|
+
@unknown default: return state("unknown", "unknown")
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static func current() async -> [String: Any] {
|
|
47
|
+
// The global service getter may block; only the manager's authorization
|
|
48
|
+
// observation belongs to the main actor, which owns Core Location objects.
|
|
49
|
+
let servicesEnabled = CLLocationManager.locationServicesEnabled()
|
|
50
|
+
return await MainActor.run {
|
|
51
|
+
let bundle = Bundle.main
|
|
52
|
+
let formatter = ISO8601DateFormatter()
|
|
53
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
54
|
+
return [
|
|
55
|
+
"schemaVersion": 1,
|
|
56
|
+
"observedAt": formatter.string(from: Date()),
|
|
57
|
+
"identity": [
|
|
58
|
+
"pid": ProcessInfo.processInfo.processIdentifier,
|
|
59
|
+
"uid": getuid(),
|
|
60
|
+
"bundleId": bundle.bundleIdentifier ?? "",
|
|
61
|
+
"bundlePath": bundle.bundleURL.path,
|
|
62
|
+
"executablePath": bundle.executableURL?.path ?? "",
|
|
63
|
+
"version": bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "",
|
|
64
|
+
"build": bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "",
|
|
65
|
+
] as [String: Any],
|
|
66
|
+
"locationServicesEnabled": servicesEnabled,
|
|
67
|
+
"permissions": [
|
|
68
|
+
"location": location(CLLocationManager().authorizationStatus),
|
|
69
|
+
"camera": capture(AVCaptureDevice.authorizationStatus(for: .video)),
|
|
70
|
+
"microphone": capture(AVCaptureDevice.authorizationStatus(for: .audio)),
|
|
71
|
+
"bluetooth": bluetooth(CBManager.authorization),
|
|
72
|
+
// iOS has no public passive Local Network authorization getter.
|
|
73
|
+
"localNetwork": ["rawStatus": NSNull(), "status": "unknown"],
|
|
74
|
+
] as [String: Any],
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static func correlate(_ observation: [String: Any], requestId: String) throws -> [String: Any] {
|
|
80
|
+
guard requestId.range(of: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}\\z", options: .regularExpression) != nil else {
|
|
81
|
+
throw NSError(domain: "MentraPermissionStatus", code: 1,
|
|
82
|
+
userInfo: [NSLocalizedDescriptionKey: "One bounded permission request ID is required"])
|
|
83
|
+
}
|
|
84
|
+
return observation.merging(["requestId": requestId]) { _, fresh in fresh }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static func write(_ receipt: [String: Any], to path: URL) throws {
|
|
88
|
+
let files = FileManager.default
|
|
89
|
+
try files.createDirectory(at: path.deletingLastPathComponent(), withIntermediateDirectories: true,
|
|
90
|
+
attributes: [.posixPermissions: 0o700])
|
|
91
|
+
try JSONSerialization.data(withJSONObject: receipt, options: [.sortedKeys]).write(to: path, options: .atomic)
|
|
92
|
+
try files.setAttributes([.posixPermissions: 0o600], ofItemAtPath: path.path)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static func publish(requestId: String) async throws -> [String: Any] {
|
|
96
|
+
// Validate before observing; the request never controls a filesystem path.
|
|
97
|
+
_ = try correlate([:], requestId: requestId)
|
|
98
|
+
let observation = await current()
|
|
99
|
+
let receipt = try correlate(observation, requestId: requestId)
|
|
100
|
+
let cache = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask,
|
|
101
|
+
appropriateFor: nil, create: true)
|
|
102
|
+
try write(receipt, to: cache.appendingPathComponent(relativePath))
|
|
103
|
+
return receipt
|
|
104
|
+
}
|
|
105
|
+
}
|