@goliapkg/smix 1.0.1 → 1.0.3
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 +34 -1
- package/src/Session.ts +134 -0
- package/src/index.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/smix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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
|
@@ -62,6 +62,24 @@ export class HttpSimRuntime implements SmixSimRuntime {
|
|
|
62
62
|
*/
|
|
63
63
|
readonly labelsResolver: LabelsResolver
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* v1.0.3 — session id sent as `Session-Id` header on every request
|
|
67
|
+
* when non-null. Set via {@link setSessionId}; when set, the runner
|
|
68
|
+
* short-circuits per-request `.activate()` and reuses the cached
|
|
69
|
+
* XCUIApplication binding. Typically managed by the {@link Session}
|
|
70
|
+
* class — direct callers rarely need to touch this.
|
|
71
|
+
*/
|
|
72
|
+
private sessionId: string | null = null
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* v1.0.3 — public alias for the wrapped fetch implementation.
|
|
76
|
+
* Consumed by the {@link Session} class which drives `/session/*`
|
|
77
|
+
* routes through the same transport.
|
|
78
|
+
*/
|
|
79
|
+
get fetch(): HttpFetch {
|
|
80
|
+
return this.fetchImpl
|
|
81
|
+
}
|
|
82
|
+
|
|
65
83
|
constructor(
|
|
66
84
|
public readonly baseUrl: string,
|
|
67
85
|
public readonly fetchImpl: HttpFetch = globalThis.fetch as unknown as HttpFetch,
|
|
@@ -76,6 +94,15 @@ export class HttpSimRuntime implements SmixSimRuntime {
|
|
|
76
94
|
}
|
|
77
95
|
}
|
|
78
96
|
|
|
97
|
+
/**
|
|
98
|
+
* v1.0.3 — attach / clear the `Session-Id` header on every subsequent
|
|
99
|
+
* request. Called by {@link Session.open} / {@link Session.close};
|
|
100
|
+
* consumers who manage sessions manually can call this directly.
|
|
101
|
+
*/
|
|
102
|
+
setSessionId(id: string | null): void {
|
|
103
|
+
this.sessionId = id
|
|
104
|
+
}
|
|
105
|
+
|
|
79
106
|
/**
|
|
80
107
|
* v7.7 c1 — direct match-count via dedicated FFI fn (avoid allocating
|
|
81
108
|
* the full ids list when callers only need count). Mirrors Rust
|
|
@@ -155,9 +182,15 @@ export class HttpSimRuntime implements SmixSimRuntime {
|
|
|
155
182
|
|
|
156
183
|
private async post(path: string, body: unknown): Promise<unknown> {
|
|
157
184
|
const url = `${this.baseUrl}${path}`
|
|
185
|
+
const headers: Record<string, string> = {
|
|
186
|
+
'content-type': 'application/json',
|
|
187
|
+
}
|
|
188
|
+
if (this.sessionId !== null) {
|
|
189
|
+
headers['session-id'] = this.sessionId
|
|
190
|
+
}
|
|
158
191
|
const resp = await this.fetchImpl(url, {
|
|
159
192
|
method: 'POST',
|
|
160
|
-
headers
|
|
193
|
+
headers,
|
|
161
194
|
body: JSON.stringify(body),
|
|
162
195
|
})
|
|
163
196
|
if (!resp.ok) {
|
package/src/Session.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// v1.0.3 — Session lifecycle wrapper.
|
|
2
|
+
//
|
|
3
|
+
// Session lifecycle addresses the runner-side "activation storm": pre-
|
|
4
|
+
// v1.0.2 runners called `.activate()` on every request whose
|
|
5
|
+
// `App-Activate: true` header was set, exhausting XCTest process
|
|
6
|
+
// arbitration on long-running gates. Sessions replace that with a
|
|
7
|
+
// one-shot lifecycle — open once, every subsequent request carries
|
|
8
|
+
// the `Session-Id` header, runner reuses its cached XCUIApplication
|
|
9
|
+
// binding, no per-request activation.
|
|
10
|
+
//
|
|
11
|
+
// Consumer flow:
|
|
12
|
+
//
|
|
13
|
+
// ```ts
|
|
14
|
+
// import { Smix, HttpRunnerClient, HttpSimRuntime, HttpSelectorResolver } from '@goliapkg/smix'
|
|
15
|
+
//
|
|
16
|
+
// const runner = new HttpRunnerClient('http://127.0.0.1:22087')
|
|
17
|
+
// const runtime = new HttpSimRuntime(runner)
|
|
18
|
+
// const resolver = new HttpSelectorResolver(runner)
|
|
19
|
+
// const session = await Session.open(runner, 'com.example.app', { activate: true })
|
|
20
|
+
// try {
|
|
21
|
+
// const app = await Smix.launchApp(bundleId('com.example.app'), runtime, resolver)
|
|
22
|
+
// // ... use `app` normally; every request carries Session-Id ...
|
|
23
|
+
// } finally {
|
|
24
|
+
// await session.close()
|
|
25
|
+
// }
|
|
26
|
+
// ```
|
|
27
|
+
|
|
28
|
+
import type { HttpFetch } from './HttpRunner.js'
|
|
29
|
+
|
|
30
|
+
interface HttpRunnerLike {
|
|
31
|
+
readonly baseUrl: string
|
|
32
|
+
readonly fetch: HttpFetch
|
|
33
|
+
setSessionId(id: string | null): void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SessionOpenOptions {
|
|
37
|
+
/** If true, runner calls `.activate()` on the target once at open time. */
|
|
38
|
+
activate?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Runner-side session guard. Use `Session.open()` to acquire, always
|
|
43
|
+
* pair with `session.close()` in a `finally` block.
|
|
44
|
+
*
|
|
45
|
+
* While the Session is open, every runner request from the same
|
|
46
|
+
* `HttpRunnerClient` carries a `Session-Id` header. The runner
|
|
47
|
+
* short-circuits per-request `.activate()` and reuses the session's
|
|
48
|
+
* cached binding.
|
|
49
|
+
*/
|
|
50
|
+
export class Session {
|
|
51
|
+
private constructor(
|
|
52
|
+
private readonly runner: HttpRunnerLike,
|
|
53
|
+
public readonly sessionId: string,
|
|
54
|
+
public readonly activatedOnce: boolean,
|
|
55
|
+
public readonly serverTimeMs: number,
|
|
56
|
+
) {}
|
|
57
|
+
|
|
58
|
+
static async open(
|
|
59
|
+
runner: HttpRunnerLike,
|
|
60
|
+
bundleId: string,
|
|
61
|
+
options: SessionOpenOptions = {},
|
|
62
|
+
): Promise<Session> {
|
|
63
|
+
const activate = options.activate ?? false
|
|
64
|
+
const res = await runner.fetch(`${runner.baseUrl}/session/open`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({ bundleId, activate }),
|
|
68
|
+
})
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const body = await res.text()
|
|
71
|
+
throw new Error(`Session.open ${bundleId}: HTTP ${res.status}: ${body}`)
|
|
72
|
+
}
|
|
73
|
+
const json = (await res.json()) as {
|
|
74
|
+
sessionId?: string
|
|
75
|
+
activatedOnce?: boolean
|
|
76
|
+
serverTimeMs?: number
|
|
77
|
+
}
|
|
78
|
+
if (typeof json.sessionId !== 'string' || json.sessionId.length === 0) {
|
|
79
|
+
throw new Error(`Session.open ${bundleId}: response missing sessionId`)
|
|
80
|
+
}
|
|
81
|
+
runner.setSessionId(json.sessionId)
|
|
82
|
+
return new Session(
|
|
83
|
+
runner,
|
|
84
|
+
json.sessionId,
|
|
85
|
+
json.activatedOnce ?? false,
|
|
86
|
+
json.serverTimeMs ?? 0,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Ask the runner to re-issue `.activate()` on the session's cached
|
|
92
|
+
* binding. Subject to a runner-side 2 s rate limit per session; when
|
|
93
|
+
* rate-limited, returns `false` (no-op, session still healthy).
|
|
94
|
+
*
|
|
95
|
+
* Use when the client detects target-app drift (e.g. tree returned
|
|
96
|
+
* empty payload, foreground steal by system UI).
|
|
97
|
+
*/
|
|
98
|
+
async renewActivation(): Promise<boolean> {
|
|
99
|
+
const res = await this.runner.fetch(
|
|
100
|
+
`${this.runner.baseUrl}/session/renew-activation`,
|
|
101
|
+
{
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: JSON.stringify({ sessionId: this.sessionId }),
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
const body = await res.text()
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Session.renewActivation ${this.sessionId}: HTTP ${res.status}: ${body}`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
const json = (await res.json()) as { ok?: boolean; activated?: boolean }
|
|
114
|
+
return json.activated ?? false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Release the session. Sends `POST /session/close` (idempotent),
|
|
119
|
+
* clears the `Session-Id` header from the client. Subsequent client
|
|
120
|
+
* requests go via the legacy per-request rebind path (rate-limited).
|
|
121
|
+
*/
|
|
122
|
+
async close(): Promise<void> {
|
|
123
|
+
try {
|
|
124
|
+
await this.runner.fetch(`${this.runner.baseUrl}/session/close`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({ sessionId: this.sessionId }),
|
|
128
|
+
})
|
|
129
|
+
} finally {
|
|
130
|
+
// Clear the client header regardless of runner-side outcome.
|
|
131
|
+
this.runner.setSessionId(null)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|