@gotcos/glasses-server 6.59.0 → 6.60.0

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.
@@ -23,28 +23,116 @@
23
23
  //
24
24
  // In memory: a restart forgets it, and the next poll restores it. Until then nothing is
25
25
  // held, which is the native dialog, the safe side.
26
+ //
27
+ // 6.60.0 (QA round 1, the contract shared with COS Glasses 6.9.563): the away hold needs
28
+ // more than a poll. A poll QUALIFIES only with `hold=1` (this client shows held questions;
29
+ // 6.9.563 and later send it) AND a finite `presenceAgeMs` within [0, 24 h]: the ms since
30
+ // the latest real wearer evidence (glasses: a ring or temple input, or a device status with
31
+ // wearing true; phone: a foreground touch). A connected lens nobody wears sends no
32
+ // `presenceAgeMs`, and an older client sends no `hold=1`, so neither can make the Mac hold a
33
+ // question for 10 minutes.
34
+ //
35
+ // Kept apart, all in memory (a restart forgets them, which fails toward 6.59.0):
36
+ // - the newest counting poll of any kind (`lastQuestionsPollAt`, for /api/health);
37
+ // - the newest poll WITHOUT `hold=1` (`lastLegacyQuestionsPollAt`): the 6.59.0 liveness,
38
+ // unchanged for every older client;
39
+ // - the newest poll WITH `hold=1` (`lastHoldQuestionsPollAt`);
40
+ // - the newest qualifying presence, on BOTH clocks, each forward-only: monotonic
41
+ // (`performance.now()`, immune to the wall clock stepping) and wall. Its age is the
42
+ // larger of the two (`presenceAgeMs`): on macOS the monotonic clock does not advance
43
+ // while the Mac sleeps, so on its own it would read a pre-sleep presence as younger
44
+ // than it is; the wall clock does advance. A wall clock stepped forward reads older
45
+ // (no hold), never younger.
46
+
47
+ /** 6.60.0: the most a client may report as `presenceAgeMs`. Anything else is no presence. */
48
+ export const PRESENCE_AGE_MAX_MS = 86_400_000
26
49
 
27
50
  let lastPollAt: number | null = null
51
+ let lastLegacyPollAt: number | null = null
52
+ let lastHoldPollAt: number | null = null
53
+ let lastPresenceMono: number | null = null
54
+ let lastPresenceWall: number | null = null
55
+
56
+ /** 6.60.0: the monotonic clock. Milliseconds since this process started; paused while the Mac sleeps. */
57
+ export function monotonicNowMs(): number {
58
+ return performance.now()
59
+ }
28
60
 
29
61
  /** A stored stamp this far ahead of a new poll means the clock stepped back. */
30
62
  const CLOCK_STEP_BACK_MS = 5_000
31
63
 
64
+ /** Newest-wins for a poll stamp, following a clock that stepped back (see below). */
65
+ function newerStamp(stored: number | null, at: number): number {
66
+ return stored === null || at > stored || stored - at > CLOCK_STEP_BACK_MS ? at : stored
67
+ }
68
+
69
+ /** 6.60.0: what a counting poll said about the client, read from its query. */
70
+ export interface QuestionsPollFacts {
71
+ /** `hold=1`: this client can show held questions. */
72
+ hold?: boolean
73
+ /** ms since the latest real wearer evidence; null or absent when there is none since boot. */
74
+ presenceAgeMs?: number | null
75
+ }
76
+
77
+ /** 6.60.0: a `presenceAgeMs` the contract accepts: a finite whole number within [0, 24 h]. */
78
+ export function validPresenceAgeMs(value: unknown): value is number {
79
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= PRESENCE_AGE_MAX_MS
80
+ }
81
+
32
82
  /**
33
83
  * A counting questions poll answered at `at` (server clock). Never moves backwards for a
34
84
  * slower request answered later, but a clock that stepped back is followed: a stamp left in
35
85
  * the future would read stale to the broker, and ignoring every poll until the clock caught
36
- * up would leave no live client for that long.
86
+ * up would leave no live client for that long. 6.60.0: `poll` says whether it was a `hold=1`
87
+ * poll and carried qualifying presence; `mono` is the monotonic time it was answered.
37
88
  */
38
- export function noteQuestionsPoll(at: number): void {
89
+ export function noteQuestionsPoll(at: number, poll: QuestionsPollFacts = {}, mono: number = monotonicNowMs()): void {
39
90
  if (!Number.isFinite(at)) return
40
- if (lastPollAt === null || at > lastPollAt || lastPollAt - at > CLOCK_STEP_BACK_MS) lastPollAt = at
91
+ lastPollAt = newerStamp(lastPollAt, at)
92
+ if (poll.hold !== true) {
93
+ lastLegacyPollAt = newerStamp(lastLegacyPollAt, at)
94
+ return
95
+ }
96
+ lastHoldPollAt = newerStamp(lastHoldPollAt, at)
97
+ if (!validPresenceAgeMs(poll.presenceAgeMs) || !Number.isFinite(mono)) return
98
+ // Forward-only on each clock: a slower poll carrying older evidence never moves it back.
99
+ const presenceMono = mono - poll.presenceAgeMs
100
+ const presenceWall = at - poll.presenceAgeMs
101
+ if (lastPresenceMono === null || presenceMono > lastPresenceMono) lastPresenceMono = presenceMono
102
+ if (lastPresenceWall === null || presenceWall > lastPresenceWall) lastPresenceWall = presenceWall
103
+ }
104
+
105
+ /**
106
+ * 6.60.0: how long ago the newest qualifying wearer presence was, or null when no
107
+ * qualifying poll has arrived since this process started. The larger of the monotonic age
108
+ * and the wall-clock age, so a Mac that slept never makes it read younger.
109
+ */
110
+ export function presenceAgeMs(mono: number = monotonicNowMs(), wall: number = Date.now()): number | null {
111
+ if (lastPresenceMono === null || !Number.isFinite(mono)) return null
112
+ const monoAge = Math.max(0, mono - lastPresenceMono)
113
+ const wallAge = lastPresenceWall !== null && Number.isFinite(wall) ? wall - lastPresenceWall : 0
114
+ return Math.max(monoAge, wallAge)
41
115
  }
42
116
 
43
- /** When the newest authenticated questions poll arrived, or null when none has since boot. */
117
+ /** When the newest authenticated questions poll of any kind arrived, or null since boot (/api/health). */
44
118
  export function lastQuestionsPollAt(): number | null {
45
119
  return lastPollAt
46
120
  }
47
121
 
122
+ /** 6.60.0: the newest poll WITHOUT `hold=1`: the 6.59.0 liveness, for every older client. */
123
+ export function lastLegacyQuestionsPollAt(): number | null {
124
+ return lastLegacyPollAt
125
+ }
126
+
127
+ /** 6.60.0: the newest poll WITH `hold=1`. */
128
+ export function lastHoldQuestionsPollAt(): number | null {
129
+ return lastHoldPollAt
130
+ }
131
+
48
132
  export function __resetClientLivenessForTests(): void {
49
133
  lastPollAt = null
134
+ lastLegacyPollAt = null
135
+ lastHoldPollAt = null
136
+ lastPresenceMono = null
137
+ lastPresenceWall = null
50
138
  }