@glassly/engine 0.3.3-beta.633 → 0.3.3-beta.654

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.
Files changed (60) hide show
  1. package/build/engine.d.ts +1 -0
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/facades/glasses.d.ts +7 -0
  4. package/build/facades/glasses.d.ts.map +1 -1
  5. package/build/facades/glasses.js +34 -1
  6. package/build/facades/glasses.js.map +1 -1
  7. package/build/generated/releaseMetadata.js +3 -3
  8. package/build/generated/releaseMetadata.js.map +1 -1
  9. package/build/index.d.ts +1 -1
  10. package/build/index.d.ts.map +1 -1
  11. package/build/index.js +1 -1
  12. package/build/index.js.map +1 -1
  13. package/build/services/AiTierResolution.d.ts.map +1 -1
  14. package/build/services/AiTierResolution.js +11 -3
  15. package/build/services/AiTierResolution.js.map +1 -1
  16. package/build/services/GestureDotsService.d.ts.map +1 -1
  17. package/build/services/GestureDotsService.js +15 -6
  18. package/build/services/GestureDotsService.js.map +1 -1
  19. package/build/services/MicStateCoordinator.d.ts +8 -0
  20. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  21. package/build/services/MicStateCoordinator.js +24 -3
  22. package/build/services/MicStateCoordinator.js.map +1 -1
  23. package/build/services/gestureDotGlyphs.d.ts +1 -1
  24. package/build/services/gestureDotGlyphs.d.ts.map +1 -1
  25. package/build/services/gestureDotGlyphs.js +1 -0
  26. package/build/services/gestureDotGlyphs.js.map +1 -1
  27. package/build/stores/glasses.d.ts.map +1 -1
  28. package/build/stores/glasses.js +1 -0
  29. package/build/stores/glasses.js.map +1 -1
  30. package/build/stores/settings.js +5 -5
  31. package/build/stores/settings.js.map +1 -1
  32. package/build/types/capabilities/even-realities-g2.js +1 -1
  33. package/build/types/capabilities/even-realities-g2.js.map +1 -1
  34. package/build/types/capabilities/mentra-display.js +1 -1
  35. package/build/types/capabilities/mentra-display.js.map +1 -1
  36. package/build/types/capabilities/rayneo-io.js +1 -1
  37. package/build/types/capabilities/rayneo-io.js.map +1 -1
  38. package/build/utils/timers/index.d.ts +1 -2
  39. package/build/utils/timers/index.d.ts.map +1 -1
  40. package/build/utils/timers/index.js +1 -2
  41. package/build/utils/timers/index.js.map +1 -1
  42. package/build/utils/timers/timers.d.ts +3 -5
  43. package/build/utils/timers/timers.d.ts.map +1 -1
  44. package/build/utils/timers/timers.js +78 -128
  45. package/build/utils/timers/timers.js.map +1 -1
  46. package/package.json +8 -8
  47. package/src/facades/glasses.ts +34 -1
  48. package/src/generated/releaseMetadata.ts +3 -3
  49. package/src/index.ts +1 -1
  50. package/src/services/AiTierResolution.ts +12 -3
  51. package/src/services/GestureDotsService.ts +16 -6
  52. package/src/services/MicStateCoordinator.ts +25 -3
  53. package/src/services/gestureDotGlyphs.ts +2 -1
  54. package/src/stores/glasses.ts +1 -0
  55. package/src/stores/settings.ts +5 -5
  56. package/src/types/capabilities/even-realities-g2.ts +1 -1
  57. package/src/types/capabilities/mentra-display.ts +1 -1
  58. package/src/types/capabilities/rayneo-io.ts +1 -1
  59. package/src/utils/timers/index.ts +1 -2
  60. package/src/utils/timers/timers.ts +82 -157
@@ -1,3 +1,2 @@
1
- import { BgTimer, throttle, debounce } from "./timers";
2
- export { BgTimer, throttle, debounce };
1
+ export { BgTimer } from "./timers";
3
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/timers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAC,MAAM,UAAU,CAAA;AACpD,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAC,CAAA","sourcesContent":["import {BgTimer, throttle, debounce} from \"./timers\"\nexport {BgTimer, throttle, debounce}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/timers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,EAAC,MAAM,UAAU,CAAA","sourcesContent":["export {BgTimer} from \"./timers\"\n"]}
@@ -1,9 +1,7 @@
1
1
  export declare class BgTimer {
2
- static setInterval(callback: () => void, delay: number): number;
3
- static clearInterval(intervalId: number): void;
4
2
  static setTimeout(callback: () => void, delay: number): number;
5
- static clearTimeout(timeoutId: number): void;
3
+ static clearTimeout(id: number): void;
4
+ static setInterval(callback: () => void, delay: number): number;
5
+ static clearInterval(id: number): void;
6
6
  }
7
- export declare function throttle<T extends (...args: any[]) => any>(fn: T, ms: number): (...args: Parameters<T>) => void;
8
- export declare function debounce<T extends (...args: any[]) => any>(fn: T, ms: number): (...args: Parameters<T>) => void;
9
7
  //# sourceMappingURL=timers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"timers.d.ts","sourceRoot":"","sources":["../../../src/utils/timers/timers.ts"],"names":[],"mappings":"AAoJA,qBAAa,OAAO;IAClB,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAO/D,MAAM,CAAC,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAO9C,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAO9D,MAAM,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;CAM7C;AAED,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAU/G;AAED,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAY/G"}
1
+ {"version":3,"file":"timers.d.ts","sourceRoot":"","sources":["../../../src/utils/timers/timers.ts"],"names":[],"mappings":"AA+GA,qBAAa,OAAO;IAClB,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAI9D,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAIrC,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAI/D,MAAM,CAAC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAGvC"}
@@ -1,164 +1,114 @@
1
- // BgTimer — background-safe timers for the engine.
1
+ // BgTimer: every engine timer (miniapp liveness watchdog, display expiries,
2
+ // boot windows, token refresh) goes through here. Android pauses plain JS
3
+ // timers while the app is backgrounded, so on a device these run on the
4
+ // native react-native-nitro-bg-timer instead. iOS needs that package at
5
+ // >= 0.2.0: earlier versions cleared each timer right after scheduling it
6
+ // (https://github.com/tconns/react-native-nitro-bg-timer/issues/2).
2
7
  //
3
- // Everything timing-critical in the engine runs through this class: the
4
- // miniapp liveness watchdog, display durationMs expiries, boot windows,
5
- // auth-token refresh. On Android, React Native PAUSES plain JS timers while
6
- // the app is backgrounded, so those features only keep working in the
7
- // background when the native implementation (react-native-nitro-bg-timer)
8
- // is active.
8
+ // Dev and release behave the same; there is no env switch. A missing native
9
+ // module falls back to JS timers so the foreground keeps working, and says so
10
+ // once, loudly: a quiet fallback already cost a day of phantom "zombie
11
+ // miniapp" bugs (OS-1790). Never downgrade that report to a warning.
9
12
  //
10
- // Who actually gets native timers:
11
- // - Android, dev AND release: always attempted — dev must match production
12
- // background behavior. There is no opt-in/opt-out env var. If the native
13
- // module is unavailable (a dev client whose binary predates it, or a
14
- // broken release binary), the failure is LOUD: a console.error with the
15
- // remediation, plus a blocking dev alert. The JS-timer fallback still
16
- // engages so foreground behavior keeps working, but backgrounded
17
- // behavior is broken until the native rebuild — the error says exactly
18
- // that. (A silent dev fallback previously cost a full debugging day of
19
- // phantom "zombie miniapp" bugs; see OS-1790.)
20
- // - iOS: never, dev or prod — the package is disabled pending
21
- // https://github.com/tconns/react-native-nitro-bg-timer/issues/2. iOS
22
- // suspends the whole process in background anyway, so background timing
23
- // there is handled by native per-device queues, not JS timers.
24
- //
25
- // react-native is loaded lazily, not imported. Every engine service runs its
26
- // timers through this module, and the engine's bun test suites load those
27
- // services in a plain JS runtime that cannot parse react-native's Flow
28
- // source. A static import would force every such suite to mock this module;
29
- // with the lazy load, a runtime without react-native simply gets the JS
30
- // timers, which is the correct answer there anyway.
13
+ // react-native and the nitro packages are require()d lazily. The engine's bun
14
+ // suites run services in a plain JS runtime that cannot parse react-native;
15
+ // there the native lookup finds no platform and JS timers are the right answer.
16
+ const TIMER_METHODS = ["setTimeout", "clearTimeout", "setInterval", "clearInterval"];
17
+ const jsTimers = {
18
+ setTimeout: (callback, delay) => setTimeout(callback, delay),
19
+ clearTimeout: (id) => clearTimeout(id),
20
+ setInterval: (callback, delay) => setInterval(callback, delay),
21
+ clearInterval: (id) => clearInterval(id),
22
+ };
31
23
  let reactNative;
32
- function getReactNative() {
33
- if (reactNative !== undefined) {
34
- return reactNative;
35
- }
36
- try {
37
- reactNative = require("react-native");
38
- }
39
- catch {
40
- reactNative = null;
24
+ // undefined = not looked up yet, null = unavailable (JS timers from here on).
25
+ let nativeTimers;
26
+ let reported = false;
27
+ function loadReactNative() {
28
+ if (reactNative === undefined) {
29
+ try {
30
+ reactNative = require("react-native");
31
+ }
32
+ catch {
33
+ reactNative = null;
34
+ }
41
35
  }
42
36
  return reactNative;
43
37
  }
44
- function isDev() {
45
- return typeof __DEV__ !== "undefined" && __DEV__;
46
- }
47
- let nitroTimer;
48
- let nitroDisabled = false;
49
- let failedLoud = false;
50
- /**
51
- * The native background timer is unavailable — say so ONCE, loudly, with the
52
- * remediation. The JS-timer fallback keeps foreground behavior working, but
53
- * everything timing-driven freezes while the app is backgrounded, which
54
- * presents as phantom miniapp bugs (frozen captions, missed wake words,
55
- * watchdog kills on resume). A quiet warning here has already cost a full
56
- * debugging day; never downgrade this to console.warn.
57
- */
58
- function failUnavailable(reason) {
59
- if (failedLoud) {
38
+ function reportUnavailable(reason) {
39
+ if (reported)
60
40
  return;
61
- }
62
- failedLoud = true;
41
+ reported = true;
42
+ const rn = loadReactNative();
43
+ const platform = rn?.Platform.OS === "ios" ? "ios" : "android";
63
44
  const message = `BgTimer: native background timer unavailable (${reason}). ` +
64
- "Backgrounded behavior IS BROKEN until you rebuild the Android app: run `bun android`. " +
45
+ `Backgrounded behavior IS BROKEN until you rebuild the app: run \`bun ${platform}\`. ` +
65
46
  "Engine timers and local miniapps (captions, wake words) freeze whenever the app is not foregrounded.";
66
47
  console.error(message);
67
- if (isDev()) {
68
- // A stale dev client may ALSO show the library's own require-time red box
69
- // (bridgeless LogBox surfaces it even though we catch below) — this alert
70
- // is the actionable one: it names the fix.
48
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
49
+ // A stale dev client may also red-box inside the library itself; this
50
+ // alert is the one that names the fix.
71
51
  try {
72
- getReactNative()?.Alert?.alert("Background timers unavailable", message);
52
+ rn?.Alert?.alert("Background timers unavailable", message);
73
53
  }
74
54
  catch {
75
- /* Alert not available (tests / headless) — the console.error stands. */
55
+ // Headless runtime: the console.error stands.
76
56
  }
77
57
  }
78
58
  }
79
- function getNitroTimer() {
80
- if (nitroDisabled || getReactNative()?.Platform.OS !== "android") {
59
+ function loadNativeTimers() {
60
+ if (nativeTimers !== undefined)
61
+ return nativeTimers;
62
+ const platform = loadReactNative()?.Platform.OS;
63
+ if (platform !== "android" && platform !== "ios") {
64
+ nativeTimers = null;
81
65
  return null;
82
66
  }
83
- if (nitroTimer !== undefined) {
84
- return nitroTimer;
85
- }
86
67
  try {
87
68
  const { isRuntimeAlive } = require("react-native-nitro-modules");
88
- if (typeof isRuntimeAlive === "function" && !isRuntimeAlive()) {
89
- nitroTimer = null;
90
- failUnavailable("nitro runtime is not alive");
91
- return nitroTimer;
92
- }
69
+ if (typeof isRuntimeAlive === "function" && !isRuntimeAlive())
70
+ throw new Error("nitro runtime is not alive");
93
71
  const { BackgroundTimer } = require("react-native-nitro-bg-timer");
94
- nitroTimer =
95
- BackgroundTimer &&
96
- typeof BackgroundTimer.setTimeout === "function" &&
97
- typeof BackgroundTimer.setInterval === "function" &&
98
- typeof BackgroundTimer.clearTimeout === "function" &&
99
- typeof BackgroundTimer.clearInterval === "function"
100
- ? BackgroundTimer
101
- : null;
102
- }
103
- catch {
104
- nitroTimer = null;
105
- nitroDisabled = true;
72
+ for (const method of TIMER_METHODS) {
73
+ if (typeof BackgroundTimer?.[method] !== "function")
74
+ throw new Error(`BackgroundTimer.${method} missing`);
75
+ }
76
+ nativeTimers = BackgroundTimer;
106
77
  }
107
- if (!nitroTimer) {
108
- failUnavailable("react-native-nitro-bg-timer failed to load");
78
+ catch (error) {
79
+ nativeTimers = null;
80
+ reportUnavailable(error instanceof Error ? error.message : String(error));
109
81
  }
110
- return nitroTimer;
82
+ return nativeTimers;
111
83
  }
112
- function withNitro(run, fallback) {
113
- const api = getNitroTimer();
114
- if (!api) {
115
- return fallback();
116
- }
84
+ // A native call that throws mid-session is new information: drop to JS timers
85
+ // and announce again.
86
+ function call(run) {
87
+ const native = loadNativeTimers();
88
+ if (!native)
89
+ return run(jsTimers);
117
90
  try {
118
- return run(api);
91
+ return run(native);
119
92
  }
120
93
  catch (error) {
121
- nitroDisabled = true;
122
- nitroTimer = null;
123
- failedLoud = false; // a mid-session failure is new information — re-announce
124
- failUnavailable(`nitro timer call threw: ${String(error)}`);
125
- return fallback();
94
+ nativeTimers = null;
95
+ reported = false;
96
+ reportUnavailable(`native timer call threw: ${String(error)}`);
97
+ return run(jsTimers);
126
98
  }
127
99
  }
128
100
  export class BgTimer {
129
- static setInterval(callback, delay) {
130
- return withNitro((api) => api.setInterval(callback, delay), () => setInterval(callback, delay));
101
+ static setTimeout(callback, delay) {
102
+ return call((timers) => timers.setTimeout(callback, delay));
131
103
  }
132
- static clearInterval(intervalId) {
133
- withNitro((api) => api.clearInterval(intervalId), () => clearInterval(intervalId));
104
+ static clearTimeout(id) {
105
+ call((timers) => timers.clearTimeout(id));
134
106
  }
135
- static setTimeout(callback, delay) {
136
- return withNitro((api) => api.setTimeout(callback, delay), () => setTimeout(callback, delay));
107
+ static setInterval(callback, delay) {
108
+ return call((timers) => timers.setInterval(callback, delay));
137
109
  }
138
- static clearTimeout(timeoutId) {
139
- withNitro((api) => api.clearTimeout(timeoutId), () => clearTimeout(timeoutId));
110
+ static clearInterval(id) {
111
+ call((timers) => timers.clearInterval(id));
140
112
  }
141
113
  }
142
- export function throttle(fn, ms) {
143
- let lastCalled = 0;
144
- return (...args) => {
145
- const now = Date.now();
146
- if (now - lastCalled >= ms) {
147
- lastCalled = now;
148
- fn(...args);
149
- }
150
- };
151
- }
152
- export function debounce(fn, ms) {
153
- let timeoutId = null;
154
- return (...args) => {
155
- if (timeoutId) {
156
- BgTimer.clearTimeout(timeoutId);
157
- }
158
- timeoutId = BgTimer.setTimeout(() => {
159
- fn(...args);
160
- timeoutId = null;
161
- }, ms);
162
- };
163
- }
164
114
  //# sourceMappingURL=timers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"timers.js","sourceRoot":"","sources":["../../../src/utils/timers/timers.ts"],"names":[],"mappings":"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","sourcesContent":["// BgTimer — background-safe timers for the engine.\n//\n// Everything timing-critical in the engine runs through this class: the\n// miniapp liveness watchdog, display durationMs expiries, boot windows,\n// auth-token refresh. On Android, React Native PAUSES plain JS timers while\n// the app is backgrounded, so those features only keep working in the\n// background when the native implementation (react-native-nitro-bg-timer)\n// is active.\n//\n// Who actually gets native timers:\n// - Android, dev AND release: always attempted — dev must match production\n// background behavior. There is no opt-in/opt-out env var. If the native\n// module is unavailable (a dev client whose binary predates it, or a\n// broken release binary), the failure is LOUD: a console.error with the\n// remediation, plus a blocking dev alert. The JS-timer fallback still\n// engages so foreground behavior keeps working, but backgrounded\n// behavior is broken until the native rebuild — the error says exactly\n// that. (A silent dev fallback previously cost a full debugging day of\n// phantom \"zombie miniapp\" bugs; see OS-1790.)\n// - iOS: never, dev or prod — the package is disabled pending\n// https://github.com/tconns/react-native-nitro-bg-timer/issues/2. iOS\n// suspends the whole process in background anyway, so background timing\n// there is handled by native per-device queues, not JS timers.\n//\n// react-native is loaded lazily, not imported. Every engine service runs its\n// timers through this module, and the engine's bun test suites load those\n// services in a plain JS runtime that cannot parse react-native's Flow\n// source. A static import would force every such suite to mock this module;\n// with the lazy load, a runtime without react-native simply gets the JS\n// timers, which is the correct answer there anyway.\n\ntype ReactNativeSubset = {\n Platform: {OS: string}\n Alert?: {alert: (title: string, message?: string) => void}\n}\n\nlet reactNative: ReactNativeSubset | null | undefined\n\nfunction getReactNative(): ReactNativeSubset | null {\n if (reactNative !== undefined) {\n return reactNative\n }\n try {\n reactNative = require(\"react-native\") as ReactNativeSubset\n } catch {\n reactNative = null\n }\n return reactNative\n}\n\nfunction isDev(): boolean {\n return typeof __DEV__ !== \"undefined\" && __DEV__\n}\n\ntype NitroTimerApi = {\n setInterval: (callback: () => void, delay: number) => number\n clearInterval: (intervalId: number) => void\n setTimeout: (callback: () => void, delay: number) => number\n clearTimeout: (timeoutId: number) => void\n}\n\nlet nitroTimer: NitroTimerApi | null | undefined\nlet nitroDisabled = false\nlet failedLoud = false\n\n/**\n * The native background timer is unavailable — say so ONCE, loudly, with the\n * remediation. The JS-timer fallback keeps foreground behavior working, but\n * everything timing-driven freezes while the app is backgrounded, which\n * presents as phantom miniapp bugs (frozen captions, missed wake words,\n * watchdog kills on resume). A quiet warning here has already cost a full\n * debugging day; never downgrade this to console.warn.\n */\nfunction failUnavailable(reason: string) {\n if (failedLoud) {\n return\n }\n failedLoud = true\n const message =\n `BgTimer: native background timer unavailable (${reason}). ` +\n \"Backgrounded behavior IS BROKEN until you rebuild the Android app: run `bun android`. \" +\n \"Engine timers and local miniapps (captions, wake words) freeze whenever the app is not foregrounded.\"\n console.error(message)\n if (isDev()) {\n // A stale dev client may ALSO show the library's own require-time red box\n // (bridgeless LogBox surfaces it even though we catch below) — this alert\n // is the actionable one: it names the fix.\n try {\n getReactNative()?.Alert?.alert(\"Background timers unavailable\", message)\n } catch {\n /* Alert not available (tests / headless) — the console.error stands. */\n }\n }\n}\n\nfunction getNitroTimer(): NitroTimerApi | null {\n if (nitroDisabled || getReactNative()?.Platform.OS !== \"android\") {\n return null\n }\n\n if (nitroTimer !== undefined) {\n return nitroTimer\n }\n\n try {\n const {isRuntimeAlive} = require(\"react-native-nitro-modules\") as {isRuntimeAlive?: () => boolean}\n if (typeof isRuntimeAlive === \"function\" && !isRuntimeAlive()) {\n nitroTimer = null\n failUnavailable(\"nitro runtime is not alive\")\n return nitroTimer\n }\n\n const {BackgroundTimer} = require(\"react-native-nitro-bg-timer\") as {BackgroundTimer?: NitroTimerApi}\n nitroTimer =\n BackgroundTimer &&\n typeof BackgroundTimer.setTimeout === \"function\" &&\n typeof BackgroundTimer.setInterval === \"function\" &&\n typeof BackgroundTimer.clearTimeout === \"function\" &&\n typeof BackgroundTimer.clearInterval === \"function\"\n ? BackgroundTimer\n : null\n } catch {\n nitroTimer = null\n nitroDisabled = true\n }\n\n if (!nitroTimer) {\n failUnavailable(\"react-native-nitro-bg-timer failed to load\")\n }\n return nitroTimer\n}\n\nfunction withNitro<T>(run: (api: NitroTimerApi) => T, fallback: () => T): T {\n const api = getNitroTimer()\n if (!api) {\n return fallback()\n }\n try {\n return run(api)\n } catch (error) {\n nitroDisabled = true\n nitroTimer = null\n failedLoud = false // a mid-session failure is new information — re-announce\n failUnavailable(`nitro timer call threw: ${String(error)}`)\n return fallback()\n }\n}\n\nexport class BgTimer {\n static setInterval(callback: () => void, delay: number): number {\n return withNitro(\n (api) => api.setInterval(callback, delay),\n () => setInterval(callback, delay) as unknown as number,\n )\n }\n\n static clearInterval(intervalId: number): void {\n withNitro(\n (api) => api.clearInterval(intervalId),\n () => clearInterval(intervalId),\n )\n }\n\n static setTimeout(callback: () => void, delay: number): number {\n return withNitro(\n (api) => api.setTimeout(callback, delay),\n () => setTimeout(callback, delay) as unknown as number,\n )\n }\n\n static clearTimeout(timeoutId: number): void {\n withNitro(\n (api) => api.clearTimeout(timeoutId),\n () => clearTimeout(timeoutId),\n )\n }\n}\n\nexport function throttle<T extends (...args: any[]) => any>(fn: T, ms: number): (...args: Parameters<T>) => void {\n let lastCalled = 0\n\n return (...args: Parameters<T>) => {\n const now = Date.now()\n if (now - lastCalled >= ms) {\n lastCalled = now\n fn(...args)\n }\n }\n}\n\nexport function debounce<T extends (...args: any[]) => any>(fn: T, ms: number): (...args: Parameters<T>) => void {\n let timeoutId: number | null = null\n\n return (...args: Parameters<T>) => {\n if (timeoutId) {\n BgTimer.clearTimeout(timeoutId)\n }\n timeoutId = BgTimer.setTimeout(() => {\n fn(...args)\n timeoutId = null\n }, ms)\n }\n}\n"]}
1
+ {"version":3,"file":"timers.js","sourceRoot":"","sources":["../../../src/utils/timers/timers.ts"],"names":[],"mappings":"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","sourcesContent":["// BgTimer: every engine timer (miniapp liveness watchdog, display expiries,\n// boot windows, token refresh) goes through here. Android pauses plain JS\n// timers while the app is backgrounded, so on a device these run on the\n// native react-native-nitro-bg-timer instead. iOS needs that package at\n// >= 0.2.0: earlier versions cleared each timer right after scheduling it\n// (https://github.com/tconns/react-native-nitro-bg-timer/issues/2).\n//\n// Dev and release behave the same; there is no env switch. A missing native\n// module falls back to JS timers so the foreground keeps working, and says so\n// once, loudly: a quiet fallback already cost a day of phantom \"zombie\n// miniapp\" bugs (OS-1790). Never downgrade that report to a warning.\n//\n// react-native and the nitro packages are require()d lazily. The engine's bun\n// suites run services in a plain JS runtime that cannot parse react-native;\n// there the native lookup finds no platform and JS timers are the right answer.\n\ntype TimerApi = {\n setTimeout(callback: () => void, delay: number): number\n clearTimeout(id: number): void\n setInterval(callback: () => void, delay: number): number\n clearInterval(id: number): void\n}\n\ntype ReactNativeSubset = {\n Platform: {OS: string}\n Alert?: {alert(title: string, message?: string): void}\n}\n\nconst TIMER_METHODS = [\"setTimeout\", \"clearTimeout\", \"setInterval\", \"clearInterval\"] as const\n\nconst jsTimers: TimerApi = {\n setTimeout: (callback, delay) => setTimeout(callback, delay) as unknown as number,\n clearTimeout: (id) => clearTimeout(id),\n setInterval: (callback, delay) => setInterval(callback, delay) as unknown as number,\n clearInterval: (id) => clearInterval(id),\n}\n\nlet reactNative: ReactNativeSubset | null | undefined\n// undefined = not looked up yet, null = unavailable (JS timers from here on).\nlet nativeTimers: TimerApi | null | undefined\nlet reported = false\n\nfunction loadReactNative(): ReactNativeSubset | null {\n if (reactNative === undefined) {\n try {\n reactNative = require(\"react-native\") as ReactNativeSubset\n } catch {\n reactNative = null\n }\n }\n return reactNative\n}\n\nfunction reportUnavailable(reason: string): void {\n if (reported) return\n reported = true\n const rn = loadReactNative()\n const platform = rn?.Platform.OS === \"ios\" ? \"ios\" : \"android\"\n const message =\n `BgTimer: native background timer unavailable (${reason}). ` +\n `Backgrounded behavior IS BROKEN until you rebuild the app: run \\`bun ${platform}\\`. ` +\n \"Engine timers and local miniapps (captions, wake words) freeze whenever the app is not foregrounded.\"\n console.error(message)\n if (typeof __DEV__ !== \"undefined\" && __DEV__) {\n // A stale dev client may also red-box inside the library itself; this\n // alert is the one that names the fix.\n try {\n rn?.Alert?.alert(\"Background timers unavailable\", message)\n } catch {\n // Headless runtime: the console.error stands.\n }\n }\n}\n\nfunction loadNativeTimers(): TimerApi | null {\n if (nativeTimers !== undefined) return nativeTimers\n const platform = loadReactNative()?.Platform.OS\n if (platform !== \"android\" && platform !== \"ios\") {\n nativeTimers = null\n return null\n }\n try {\n const {isRuntimeAlive} = require(\"react-native-nitro-modules\") as {isRuntimeAlive?: () => boolean}\n if (typeof isRuntimeAlive === \"function\" && !isRuntimeAlive()) throw new Error(\"nitro runtime is not alive\")\n const {BackgroundTimer} = require(\"react-native-nitro-bg-timer\") as {BackgroundTimer?: Partial<TimerApi>}\n for (const method of TIMER_METHODS) {\n if (typeof BackgroundTimer?.[method] !== \"function\") throw new Error(`BackgroundTimer.${method} missing`)\n }\n nativeTimers = BackgroundTimer as TimerApi\n } catch (error) {\n nativeTimers = null\n reportUnavailable(error instanceof Error ? error.message : String(error))\n }\n return nativeTimers\n}\n\n// A native call that throws mid-session is new information: drop to JS timers\n// and announce again.\nfunction call<T>(run: (timers: TimerApi) => T): T {\n const native = loadNativeTimers()\n if (!native) return run(jsTimers)\n try {\n return run(native)\n } catch (error) {\n nativeTimers = null\n reported = false\n reportUnavailable(`native timer call threw: ${String(error)}`)\n return run(jsTimers)\n }\n}\n\nexport class BgTimer {\n static setTimeout(callback: () => void, delay: number): number {\n return call((timers) => timers.setTimeout(callback, delay))\n }\n\n static clearTimeout(id: number): void {\n call((timers) => timers.clearTimeout(id))\n }\n\n static setInterval(callback: () => void, delay: number): number {\n return call((timers) => timers.setInterval(callback, delay))\n }\n\n static clearInterval(id: number): void {\n call((timers) => timers.clearInterval(id))\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/engine",
3
- "version": "0.3.3-beta.633",
3
+ "version": "0.3.3-beta.654",
4
4
  "description": "Glassly Engine — miniapp registry, WebView bridge, and miniapp store",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -97,24 +97,24 @@
97
97
  "registry": "https://registry.npmjs.org/"
98
98
  },
99
99
  "dependencies": {
100
- "@glassly/bluetooth-sdk": "0.3.3-beta.633",
101
- "@glassly/cloud-client": "0.3.3-beta.633",
102
- "@glassly/cloud-protocol": "0.3.3-beta.633",
103
- "@glassly/crust": "0.3.3-beta.633",
104
- "@glassly/miniapp": "0.3.3-beta.633",
100
+ "@glassly/bluetooth-sdk": "0.3.3-beta.654",
101
+ "@glassly/cloud-client": "0.3.3-beta.654",
102
+ "@glassly/cloud-protocol": "0.3.3-beta.654",
103
+ "@glassly/crust": "0.3.3-beta.654",
104
+ "@glassly/miniapp": "0.3.3-beta.654",
105
105
  "buffer": "^6.0.3",
106
106
  "events": "^3.3.0",
107
107
  "react-native-marked": "8.1.1",
108
108
  "semver": "^7.7.4",
109
109
  "typesafe-ts": "^1.7.1",
110
- "@glassly/llm-providers": "0.3.3-beta.633"
110
+ "@glassly/llm-providers": "0.3.3-beta.654"
111
111
  },
112
112
  "devDependencies": {
113
113
  "@glassly/llm-providers": "workspace:*",
114
114
  "@types/semver": "^7.7.1",
115
115
  "expo-module-scripts": "^56.0.3",
116
116
  "expo-modules-core": "^57.0.7",
117
- "react-native-nitro-modules": "^0.31.10",
117
+ "react-native-nitro-modules": "^0.35.10",
118
118
  "typescript": "~5.9.2"
119
119
  },
120
120
  "peerDependencies": {
@@ -22,6 +22,7 @@ import type {
22
22
  import {useGlassesStore} from "../stores/glasses"
23
23
  import {useSettingsStore, SETTINGS} from "../stores/settings"
24
24
  import {hasDefaultDevice} from "../services/DeviceStoreHydration"
25
+ import micStateCoordinator from "../services/MicStateCoordinator"
25
26
  import {projectPairingIdentity} from "../services/PairingIdentity"
26
27
  import {isGlassesConnected, isGlassesReady} from "../services/GlassesReadiness"
27
28
  import {pushAllBluetoothSettings} from "../services/GlassesSettingsSync"
@@ -34,6 +35,21 @@ import {getModelCapabilities, type DeviceTypes} from "../types"
34
35
  import {glassesWifi} from "./glassesWifi"
35
36
  import {glassesSettings} from "./glassesSettings"
36
37
 
38
+ /** Floor for the mic level readout; digital silence would otherwise be -Infinity. */
39
+ const MIC_LEVEL_FLOOR_DBFS = -120
40
+
41
+ /** RMS level of one s16le chunk in dBFS, on the volume gate's 32767 full scale. */
42
+ function pcmLevelDbfs(pcm: ArrayBuffer): number {
43
+ const samples = new Int16Array(pcm, 0, pcm.byteLength >> 1)
44
+ if (samples.length === 0) return MIC_LEVEL_FLOOR_DBFS
45
+ let sumSquares = 0
46
+ for (let i = 0; i < samples.length; i++) sumSquares += samples[i] * samples[i]
47
+ const rms = Math.sqrt(sumSquares / samples.length)
48
+ return Math.max(MIC_LEVEL_FLOOR_DBFS, 20 * Math.log10(rms / 32767))
49
+ }
50
+
51
+ let micLevelSubscriptions = 0
52
+
37
53
  function projectStatus() {
38
54
  const s = useGlassesStore.getState()
39
55
  return {
@@ -96,6 +112,7 @@ function projectInfo() {
96
112
  sceneCanvasWidth: s.sceneCanvasWidth,
97
113
  sceneCanvasHeight: s.sceneCanvasHeight,
98
114
  volumeGate: s.volumeGate === true,
115
+ firmwareUpdateAvailable: s.firmwareUpdateAvailable === true,
99
116
  // Copy: the snapshot must not hand callers a mutable reference into the store.
100
117
  wifi: {...s.wifi},
101
118
  }
@@ -295,7 +312,7 @@ export const glasses = {
295
312
  setHand: (hand: "left" | "right"): Promise<void> => BluetoothSdk.neuralBandSetHand(hand),
296
313
  },
297
314
 
298
- // --- audio (glasses media-volume + own-app playback hint) ---
315
+ // --- audio (glasses media-volume, own-app playback hint, live mic level) ---
299
316
  audio: {
300
317
  /** Read the glasses' current media volume. */
301
318
  getMediaVolume: () => BluetoothSdk.getGlassesMediaVolume(),
@@ -303,6 +320,22 @@ export const glasses = {
303
320
  setMediaVolume: (level: number) => BluetoothSdk.setGlassesMediaVolume(level),
304
321
  /** Tell native whether THIS app is currently playing audio (for ducking). */
305
322
  setOwnAppPlaying: (playing: boolean): Promise<void> => BluetoothSdk.setOwnAppAudioPlaying(playing),
323
+ /**
324
+ * Stream the active mic's level in dBFS, one reading per PCM chunk. Holds
325
+ * raw PCM open while subscribed, so the mic runs with no miniapp listening
326
+ * (and the G2 volume gate stays open).
327
+ */
328
+ onMicLevel: (cb: (dbfs: number) => void): (() => void) => {
329
+ const owner = `mic_level_${++micLevelSubscriptions}`
330
+ micStateCoordinator.acquireHostPcm(owner)
331
+ const sub = BluetoothSdk.addListener("mic_pcm", (event: {pcm: ArrayBuffer}) => {
332
+ if (event?.pcm) cb(pcmLevelDbfs(event.pcm))
333
+ })
334
+ return () => {
335
+ sub.remove()
336
+ micStateCoordinator.releaseHostPcm(owner)
337
+ }
338
+ },
306
339
  },
307
340
 
308
341
  // --- sub-facades ---
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "0.3.3",
15
- "releaseIdentity": "0.3.3-beta.633",
16
- "releaseSetId": "glassly-0.3.3-beta.633",
17
- "sourceCommit": "34d49f6f3585d9f0e8e2d28802ae2b70666507a1",
15
+ "releaseIdentity": "0.3.3-beta.654",
16
+ "releaseSetId": "glassly-0.3.3-beta.654",
17
+ "sourceCommit": "9c554ec6e0d33cd235914ccee98e7d39e4fa29e2",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
package/src/index.ts CHANGED
@@ -201,7 +201,7 @@ export {
201
201
  type DevManifest,
202
202
  } from "./utils/devMiniappLaunch"
203
203
  export {HardwareCompatibility, type CompatibilityResult} from "./utils/hardware"
204
- export {BgTimer, throttle, debounce} from "./utils/timers"
204
+ export {BgTimer} from "./utils/timers"
205
205
  // Meta Neural Band: the account sign-in a claim needs (the WebView lives in the host).
206
206
  export {
207
207
  MetaAuthError,
@@ -42,7 +42,7 @@ export const TIER_MODEL_OPTIONS: Record<AiTier, TierModelOption[]> = {
42
42
  ],
43
43
  frontier: [
44
44
  {provider: "anthropic", model: "claude-sonnet-5", label: "Claude Sonnet 5"},
45
- {provider: "gemini", model: "gemini-3.5-pro", label: "Gemini 3.5 Pro"},
45
+ {provider: "gemini", model: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro"},
46
46
  {provider: "openai", model: "gpt-5.1", label: "GPT-5.1"},
47
47
  ],
48
48
  }
@@ -60,6 +60,14 @@ export interface ResolvedTierModel {
60
60
  model?: string
61
61
  }
62
62
 
63
+ /**
64
+ * Stored dropdown picks for model ids the provider never served, mapped to the
65
+ * curated replacement. gemini-3.5-pro 404s on generateContent.
66
+ */
67
+ const RETIRED_MODELS: Record<string, string> = {
68
+ "gemini-3.5-pro": "gemini-3.1-pro-preview",
69
+ }
70
+
63
71
  /** Coerce an untrusted wire value into a tier; anything else is "balanced". */
64
72
  export function normalizeAiTier(value: unknown): AiTier {
65
73
  return value === "quick" || value === "frontier" ? value : "balanced"
@@ -71,8 +79,9 @@ export function parseTierSetting(value: unknown): {provider: AiTierProvider; mod
71
79
  const sep = value.indexOf(":")
72
80
  if (sep <= 0) return null
73
81
  const provider = value.slice(0, sep)
74
- const model = value.slice(sep + 1).trim()
75
- if (!model) return null
82
+ const stored = value.slice(sep + 1).trim()
83
+ if (!stored) return null
84
+ const model = RETIRED_MODELS[stored] ?? stored
76
85
  if (provider !== "anthropic" && provider !== "gemini" && provider !== "openai") return null
77
86
  return {provider, model}
78
87
  }
@@ -5,7 +5,8 @@
5
5
  * (cleared after a short pulse), a bullseye for a recognized long press.
6
6
  * Controllers that report press edges (touch_down … tap_end) show the press
7
7
  * itself instead: a dot while pressed, the bullseye once held, cleared at
8
- * release. The Neural Band's middle finger (index 1) draws the same shapes hollow. Extracted from
8
+ * release. The Neural Band's middle finger (index 1) draws the same shapes
9
+ * hollow: a ring for its tap, the bullseye without its center for its hold. Extracted from
9
10
  * the CFW dashboard service; it renders through
10
11
  * the ordinary scene pipeline, so it works on any positioning device (stock
11
12
  * G2 included), and is gated by the gesture_dots debug setting.
@@ -41,7 +42,16 @@ const FLASH_GLYPHS: Record<string, GestureDotGlyph> = {
41
42
  swipe_right: "right",
42
43
  }
43
44
  /** The band's middle finger draws the filled shapes hollow. */
44
- const MIDDLE_FINGER_GLYPHS: Partial<Record<GestureDotGlyph, GestureDotGlyph>> = {dot: "ring", double: "doubleRing"}
45
+ const MIDDLE_FINGER_GLYPHS: Partial<Record<GestureDotGlyph, GestureDotGlyph>> = {
46
+ dot: "ring",
47
+ double: "doubleRing",
48
+ hold: "holdRing",
49
+ }
50
+
51
+ /** `glyph` as `finger` draws it: the middle finger's shapes are hollow. */
52
+ function fingerGlyph(glyph: GestureDotGlyph, finger: number): GestureDotGlyph {
53
+ return finger === 1 ? (MIDDLE_FINGER_GLYPHS[glyph] ?? glyph) : glyph
54
+ }
45
55
 
46
56
  class GestureDotsService {
47
57
  private started = false
@@ -135,12 +145,12 @@ class GestureDotsService {
135
145
  if (press) {
136
146
  // Both reports describe one press: after touch_down, tap_start means it is held.
137
147
  if (gestureName === "touch_down") press.raw = true
138
- else if (press.raw) press.glyph = "hold"
148
+ else if (press.raw) press.glyph = fingerGlyph("hold", finger)
139
149
  this.render()
140
150
  return
141
151
  }
142
152
  this.clearFlash()
143
- this.presses.set(finger, {raw: gestureName === "touch_down", glyph: finger === 1 ? "ring" : "dot"})
153
+ this.presses.set(finger, {raw: gestureName === "touch_down", glyph: fingerGlyph("dot", finger)})
144
154
  this.render()
145
155
  return
146
156
  }
@@ -152,14 +162,14 @@ class GestureDotsService {
152
162
  return
153
163
  }
154
164
  const shape = FLASH_GLYPHS[gestureName]
155
- const glyph = shape && finger === 1 ? MIDDLE_FINGER_GLYPHS[shape] ?? shape : shape
165
+ const glyph = shape ? fingerGlyph(shape, finger) : shape
156
166
  // The press already showed its own tap and hold; the device's report of
157
167
  // them follows after its double-tap window, often after release.
158
168
  const shownByPress = gestureName === "single_tap" || gestureName === "long_press"
159
169
  if (press?.raw && (!glyph || shownByPress)) {
160
170
  // Pinch-and-turn reports do not release a finger. A recognized hold
161
171
  // stays visible until tap_end instead of expiring halfway through it.
162
- if (gestureName === "long_press") press.glyph = "hold"
172
+ if (gestureName === "long_press") press.glyph = fingerGlyph("hold", finger)
163
173
  this.render()
164
174
  return
165
175
  }
@@ -48,6 +48,8 @@ class MicStateCoordinator {
48
48
  // Local miniapp requirements (set when miniapps subscribe to audio streams)
49
49
  private localWantsPcm = false
50
50
  private localWantsLc3 = false
51
+ // Host screens that read raw PCM themselves (e.g. the Microphone settings level meter)
52
+ private readonly hostPcmOwners = new Set<string>()
51
53
  private configuredVad: boolean | undefined
52
54
  private configuredLoudnessGate: boolean | undefined
53
55
  private readonly miniappVadOverrides = new Map<string, GateOverride>()
@@ -75,6 +77,21 @@ class MicStateCoordinator {
75
77
  this.applyUnion()
76
78
  }
77
79
 
80
+ /**
81
+ * Hold raw PCM open for a host-side consumer, independent of miniapp
82
+ * subscriptions. Release with `releaseHostPcm` using the same owner.
83
+ */
84
+ public acquireHostPcm(owner: string): void {
85
+ if (this.hostPcmOwners.has(owner)) return
86
+ this.hostPcmOwners.add(owner)
87
+ this.applyUnion()
88
+ }
89
+
90
+ public releaseHostPcm(owner: string): void {
91
+ if (!this.hostPcmOwners.delete(owner)) return
92
+ this.applyUnion()
93
+ }
94
+
78
95
  /**
79
96
  * Apply a miniapp-owned gate override without changing the OS preference.
80
97
  * Overrides are lifecycle-scoped and last-live-owner-wins independently for
@@ -159,7 +176,7 @@ class MicStateCoordinator {
159
176
  const vadOverride = this.latestOverride(this.miniappVadOverrides)
160
177
  const loudnessOverride = this.latestOverride(this.miniappLoudnessGateOverrides)
161
178
 
162
- if (this.localWantsPcm) {
179
+ if (this.wantsPcm()) {
163
180
  runtimeSettings.voice_activity_detection_enabled = false
164
181
  } else if (vadOverride) {
165
182
  runtimeSettings.voice_activity_detection_enabled = vadOverride.enabled
@@ -176,7 +193,7 @@ class MicStateCoordinator {
176
193
  * on-device PCM consumers; cloud audio uses LC3 through AudioCloudUplink.
177
194
  */
178
195
  private applyUnion(): void {
179
- const shouldSendPcm = this.localWantsPcm
196
+ const shouldSendPcm = this.wantsPcm()
180
197
  const shouldSendLc3 = this.localWantsLc3
181
198
 
182
199
  // console.log(
@@ -195,6 +212,10 @@ class MicStateCoordinator {
195
212
  flushMicRequirementsPatch(patch)
196
213
  }
197
214
 
215
+ private wantsPcm(): boolean {
216
+ return this.localWantsPcm || this.hostPcmOwners.size > 0
217
+ }
218
+
198
219
  private rememberConfiguredGates(configured: ConfiguredMicGates): void {
199
220
  // null means the connected device intentionally omits that setting.
200
221
  // undefined means the caller is not updating the remembered preference.
@@ -225,7 +246,7 @@ class MicStateCoordinator {
225
246
 
226
247
  // Hardware VAD suppresses silence. Raw-audio consumers need a continuous
227
248
  // timeline, so their requirement wins over both OS and miniapp VAD values.
228
- if (this.localWantsPcm) patch.voice_activity_detection_enabled = false
249
+ if (this.wantsPcm()) patch.voice_activity_detection_enabled = false
229
250
  else if (vadOverride) patch.voice_activity_detection_enabled = vadOverride.enabled
230
251
  else if (this.configuredVad !== undefined) patch.voice_activity_detection_enabled = this.configuredVad
231
252
 
@@ -250,6 +271,7 @@ class MicStateCoordinator {
250
271
  console.log(`${LOG_TAG}: cleanup()`)
251
272
  this.miniappVadOverrides.clear()
252
273
  this.miniappLoudnessGateOverrides.clear()
274
+ this.hostPcmOwners.clear()
253
275
  this.reset()
254
276
  MicStateCoordinator.instance = null
255
277
  }