@goliapkg/smix 1.0.3 → 1.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goliapkg/smix",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Playwright-shape iOS Simulator / Android emulator UI automation SDK for React Native + Expo + Detox-compatible TypeScript test runners.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0 OR MIT",
package/src/HttpRunner.ts CHANGED
@@ -46,7 +46,14 @@ import type { LabelsResolver, SelectorResolver } from './SelectorResolver.js'
46
46
  export type HttpFetch = (
47
47
  input: string,
48
48
  init: { method: 'POST'; headers: Record<string, string>; body: string },
49
- ) => Promise<{ ok: boolean; status: number; json(): Promise<unknown>; text(): Promise<string> }>
49
+ ) => Promise<{
50
+ ok: boolean
51
+ status: number
52
+ json(): Promise<unknown>
53
+ text(): Promise<string>
54
+ /** v1.0.4 §D7 — optional; used to parse `X-Sim-Health` response header. */
55
+ headers?: { get(name: string): string | null }
56
+ }>
50
57
 
51
58
  /**
52
59
  * SmixSimRuntime + SelectorResolver wrapper around the smix-runner HTTP
@@ -71,6 +78,14 @@ export class HttpSimRuntime implements SmixSimRuntime {
71
78
  */
72
79
  private sessionId: string | null = null
73
80
 
81
+ /**
82
+ * v1.0.4 §D7 — setter installed by {@link Session.open} so this
83
+ * client can push `X-Sim-Health` header transitions back to the
84
+ * session's state machine. `null` when no session is open (legacy
85
+ * per-request path).
86
+ */
87
+ private sessionStateSetter: ((state: SessionState) => void) | null = null
88
+
74
89
  /**
75
90
  * v1.0.3 — public alias for the wrapped fetch implementation.
76
91
  * Consumed by the {@link Session} class which drives `/session/*`
@@ -103,6 +118,15 @@ export class HttpSimRuntime implements SmixSimRuntime {
103
118
  this.sessionId = id
104
119
  }
105
120
 
121
+ /**
122
+ * v1.0.4 §D7 — register a callback the client invokes on every
123
+ * response that carries `X-Sim-Health`. Session.open() wires this
124
+ * so consumers can subscribe via `session.on('state', ...)`.
125
+ */
126
+ attachSessionState(setter: (state: SessionState) => void): void {
127
+ this.sessionStateSetter = setter
128
+ }
129
+
106
130
  /**
107
131
  * v7.7 c1 — direct match-count via dedicated FFI fn (avoid allocating
108
132
  * the full ids list when callers only need count). Mirrors Rust
@@ -193,6 +217,21 @@ export class HttpSimRuntime implements SmixSimRuntime {
193
217
  headers,
194
218
  body: JSON.stringify(body),
195
219
  })
220
+ // v1.0.4 §D7 — parse X-Sim-Health header and forward to the
221
+ // attached session state setter (if any). Runs BEFORE the
222
+ // ok-check so state transitions are visible on error paths too.
223
+ const simHealthHeader = resp.headers?.get?.('x-sim-health')
224
+ if (simHealthHeader && this.sessionStateSetter) {
225
+ const normalized = simHealthHeader.trim().toLowerCase()
226
+ if (
227
+ normalized === 'healthy' ||
228
+ normalized === 'degraded' ||
229
+ normalized === 'cycling' ||
230
+ normalized === 'dead'
231
+ ) {
232
+ this.sessionStateSetter(normalized as SessionState)
233
+ }
234
+ }
196
235
  if (!resp.ok) {
197
236
  const text = await resp.text()
198
237
  throw new Error(`smix-runner ${path} → HTTP ${resp.status}: ${text}`)
@@ -206,6 +245,9 @@ export class HttpSimRuntime implements SmixSimRuntime {
206
245
  }
207
246
  }
208
247
 
248
+ /** v1.0.4 §D7 — re-export from Session.ts so this file's use compiles. */
249
+ export type SessionState = 'healthy' | 'degraded' | 'cycling' | 'dead'
250
+
209
251
  function base64ToBytes(b64: string): Uint8Array {
210
252
  if (typeof globalThis.atob === 'function') {
211
253
  const bin = globalThis.atob(b64)
package/src/Session.ts CHANGED
@@ -31,6 +31,10 @@ interface HttpRunnerLike {
31
31
  readonly baseUrl: string
32
32
  readonly fetch: HttpFetch
33
33
  setSessionId(id: string | null): void
34
+ /** v1.0.4 §D7 — optional hook so the client can push state
35
+ * transitions parsed from `X-Sim-Health` headers back to the
36
+ * Session. Legacy runners omit the header; state stays `healthy`. */
37
+ attachSessionState?(setter: (state: SessionState) => void): void
34
38
  }
35
39
 
36
40
  export interface SessionOpenOptions {
@@ -38,6 +42,15 @@ export interface SessionOpenOptions {
38
42
  activate?: boolean
39
43
  }
40
44
 
45
+ /**
46
+ * v1.0.4 §D7 — session-scoped sim health classification observed via
47
+ * the runner's `X-Sim-Health` response header. Consumers subscribe
48
+ * via {@link Session.on}.
49
+ */
50
+ export type SessionState = 'healthy' | 'degraded' | 'cycling' | 'dead'
51
+
52
+ type StateListener = (state: SessionState) => void
53
+
41
54
  /**
42
55
  * Runner-side session guard. Use `Session.open()` to acquire, always
43
56
  * pair with `session.close()` in a `finally` block.
@@ -48,6 +61,11 @@ export interface SessionOpenOptions {
48
61
  * cached binding.
49
62
  */
50
63
  export class Session {
64
+ /** v1.0.4 §D7 — current classification; updated on every response
65
+ * that carries `X-Sim-Health`. */
66
+ private _state: SessionState = 'healthy'
67
+ private readonly listeners = new Set<StateListener>()
68
+
51
69
  private constructor(
52
70
  private readonly runner: HttpRunnerLike,
53
71
  public readonly sessionId: string,
@@ -79,12 +97,84 @@ export class Session {
79
97
  throw new Error(`Session.open ${bundleId}: response missing sessionId`)
80
98
  }
81
99
  runner.setSessionId(json.sessionId)
82
- return new Session(
100
+ const session = new Session(
83
101
  runner,
84
102
  json.sessionId,
85
103
  json.activatedOnce ?? false,
86
104
  json.serverTimeMs ?? 0,
87
105
  )
106
+ // v1.0.4 §D7 — wire the runner's X-Sim-Health parse into this
107
+ // session's state machine. Legacy runner-clients without the
108
+ // attachSessionState hook keep working; the session's state stays
109
+ // 'healthy'.
110
+ runner.attachSessionState?.((next) => session.updateState(next))
111
+ return session
112
+ }
113
+
114
+ /** v1.0.4 §D7 — current sim-health classification. */
115
+ get state(): SessionState {
116
+ return this._state
117
+ }
118
+
119
+ /** v1.0.4 §D7 — subscribe to state transitions. Returns an
120
+ * unsubscribe function. */
121
+ on(_event: 'state', listener: StateListener): () => void {
122
+ this.listeners.add(listener)
123
+ return () => this.listeners.delete(listener)
124
+ }
125
+
126
+ /** v1.0.5 §D1 — probe `/session/list` and return `true` iff this
127
+ * session's id is still known to the runner. Consumers wire this
128
+ * after a state transition to `cycling`/`dead` to decide whether
129
+ * to keep the session or reopen. */
130
+ async stillValid(): Promise<boolean> {
131
+ const res = await this.runner.fetch(
132
+ `${this.runner.baseUrl}/session/list`,
133
+ {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/json' },
136
+ body: '{}',
137
+ },
138
+ )
139
+ if (!res.ok) return false
140
+ const json = (await res.json()) as {
141
+ sessions?: Array<{ sessionId?: string }>
142
+ }
143
+ return (json.sessions ?? []).some((s) => s.sessionId === this.sessionId)
144
+ }
145
+
146
+ /** v1.0.4 §D14 — instruct the runner to `terminate()` + `launch()`
147
+ * the session's cached `XCUIApplication` in place. Preserves the
148
+ * session id and XCUITest binding. Returns wall-clock ms. */
149
+ async relaunchApp(): Promise<number> {
150
+ const res = await this.runner.fetch(
151
+ `${this.runner.baseUrl}/session/relaunch-app`,
152
+ {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json' },
155
+ body: JSON.stringify({ sessionId: this.sessionId }),
156
+ },
157
+ )
158
+ if (!res.ok) {
159
+ const body = await res.text()
160
+ throw new Error(
161
+ `Session.relaunchApp ${this.sessionId}: HTTP ${res.status}: ${body}`,
162
+ )
163
+ }
164
+ const json = (await res.json()) as { ok?: boolean; wallMs?: number }
165
+ return json.wallMs ?? 0
166
+ }
167
+
168
+ private updateState(next: SessionState) {
169
+ if (next === this._state) return
170
+ this._state = next
171
+ for (const listener of this.listeners) {
172
+ try {
173
+ listener(next)
174
+ } catch {
175
+ // Consumer callbacks must not break the runner-response path.
176
+ }
177
+ }
88
178
  }
89
179
 
90
180
  /**