@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 +1 -1
- package/src/HttpRunner.ts +43 -1
- package/src/Session.ts +91 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/smix",
|
|
3
|
-
"version": "1.0.
|
|
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<{
|
|
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
|
-
|
|
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
|
/**
|