snapback4-expo 0.2.5 → 0.2.7
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/README.md +13 -1
- package/ios/Snapback4Device.xcframework/Info.plist +4 -4
- package/ios/Snapback4Device.xcframework/ios-arm64/libsnapback4_device.a +0 -0
- package/ios/Snapback4Device.xcframework/ios-arm64-simulator/libsnapback4_device.a +0 -0
- package/ios/Snapback4Expo.podspec +8 -1
- package/ios/Snapback4Module.swift +4 -0
- package/ios/WitnessContainer.swift +46 -0
- package/package.json +6 -1
- package/src/witness-core.ts +66 -0
- package/src/witness.ts +10 -0
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ linked and about a millisecond per call; those measurements predate the
|
|
|
32
32
|
native-query host.
|
|
33
33
|
|
|
34
34
|
Earlier iOS simulator evidence from the repository's witness in a development
|
|
35
|
-
build (
|
|
35
|
+
build (before the published driver, recorded in
|
|
36
36
|
`metrics/witness.jsonl` as `dev-client-native`): a cold sync of 303 rows
|
|
37
37
|
in 31 ms, a reopen in 6 ms, a cold start with no network answering the
|
|
38
38
|
thread from the store in 2 ms, and writes composed offline surviving a
|
|
@@ -46,3 +46,15 @@ file storage beside each device store in Documents. Offline `client.asset` and
|
|
|
46
46
|
on home-row revocation, deletion, clearing or expiry and on sign-out; `once`
|
|
47
47
|
bytes never enter file storage. Rebuild the native app to include this storage
|
|
48
48
|
capability. `keep: "none"` (default) retains no bytes.
|
|
49
|
+
|
|
50
|
+
## Simulator witness
|
|
51
|
+
|
|
52
|
+
Import `witness` from `snapback4-expo/witness` in an installed iOS Release
|
|
53
|
+
app. `witness()` is null on ordinary launches; under `snapback4 witness ios`
|
|
54
|
+
it returns `{ segment, step(name, fn), receipt(object), waitFor(label) }`.
|
|
55
|
+
The `Witness` type is exported from the same subpath. Rebuild the native app
|
|
56
|
+
to include the launch-environment and Documents-file bridge. The full chat
|
|
57
|
+
runner, commands and artifact contract are in `snapback4 guide witness`
|
|
58
|
+
(aliases: `simulator`, `offline proof`) and the client package README's
|
|
59
|
+
“Proving it on an iOS simulator”. No Expo Go or Metro process supplies a
|
|
60
|
+
Release witness.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<key>HeadersPath</key>
|
|
11
11
|
<string>Headers</string>
|
|
12
12
|
<key>LibraryIdentifier</key>
|
|
13
|
-
<string>ios-arm64
|
|
13
|
+
<string>ios-arm64</string>
|
|
14
14
|
<key>LibraryPath</key>
|
|
15
15
|
<string>libsnapback4_device.a</string>
|
|
16
16
|
<key>SupportedArchitectures</key>
|
|
@@ -19,8 +19,6 @@
|
|
|
19
19
|
</array>
|
|
20
20
|
<key>SupportedPlatform</key>
|
|
21
21
|
<string>ios</string>
|
|
22
|
-
<key>SupportedPlatformVariant</key>
|
|
23
|
-
<string>simulator</string>
|
|
24
22
|
</dict>
|
|
25
23
|
<dict>
|
|
26
24
|
<key>BinaryPath</key>
|
|
@@ -28,7 +26,7 @@
|
|
|
28
26
|
<key>HeadersPath</key>
|
|
29
27
|
<string>Headers</string>
|
|
30
28
|
<key>LibraryIdentifier</key>
|
|
31
|
-
<string>ios-arm64</string>
|
|
29
|
+
<string>ios-arm64-simulator</string>
|
|
32
30
|
<key>LibraryPath</key>
|
|
33
31
|
<string>libsnapback4_device.a</string>
|
|
34
32
|
<key>SupportedArchitectures</key>
|
|
@@ -37,6 +35,8 @@
|
|
|
37
35
|
</array>
|
|
38
36
|
<key>SupportedPlatform</key>
|
|
39
37
|
<string>ios</string>
|
|
38
|
+
<key>SupportedPlatformVariant</key>
|
|
39
|
+
<string>simulator</string>
|
|
40
40
|
</dict>
|
|
41
41
|
</array>
|
|
42
42
|
<key>CFBundlePackageType</key>
|
|
Binary file
|
|
Binary file
|
|
@@ -19,8 +19,15 @@ Pod::Spec.new do |s|
|
|
|
19
19
|
# built by scripts/build-xcframework.sh.
|
|
20
20
|
s.vendored_frameworks = 'Snapback4Device.xcframework'
|
|
21
21
|
s.source_files = '*.swift'
|
|
22
|
+
# The xcframework carries arm64 device and arm64 simulator slices only, so a
|
|
23
|
+
# Release simulator build (which otherwise builds x86_64 too) must not ask
|
|
24
|
+
# for an Intel slice: exclude it for simulator SDKs in the pod and the app.
|
|
22
25
|
s.pod_target_xcconfig = {
|
|
23
26
|
'DEFINES_MODULE' => 'YES',
|
|
24
|
-
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
27
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule',
|
|
28
|
+
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64'
|
|
29
|
+
}
|
|
30
|
+
s.user_target_xcconfig = {
|
|
31
|
+
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64'
|
|
25
32
|
}
|
|
26
33
|
end
|
|
@@ -13,6 +13,10 @@ public final class Snapback4Module: Module {
|
|
|
13
13
|
public func definition() -> ModuleDefinition {
|
|
14
14
|
Name("Snapback4")
|
|
15
15
|
|
|
16
|
+
Constant("witnessLaunch") { WitnessContainer.launch }
|
|
17
|
+
Function("witnessAppend") { (line: String) in try WitnessContainer.append(line) }
|
|
18
|
+
Function("witnessSignaled") { (label: String) -> Bool in try WitnessContainer.signaled(label) }
|
|
19
|
+
|
|
16
20
|
// Where an app keeps its stores: the documents directory.
|
|
17
21
|
Constant("documentDirectory") {
|
|
18
22
|
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.path ?? NSTemporaryDirectory()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// Runtime environment is not exposed to Hermes by Expo. This small bridge
|
|
4
|
+
// supplies it and durable file append; the scenario and protocol live in JS.
|
|
5
|
+
enum WitnessContainer {
|
|
6
|
+
static var launch: String? {
|
|
7
|
+
#if targetEnvironment(simulator)
|
|
8
|
+
return ProcessInfo.processInfo.environment["SNAPBACK4_WITNESS"]
|
|
9
|
+
#else
|
|
10
|
+
return nil
|
|
11
|
+
#endif
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private static func directory() throws -> URL {
|
|
15
|
+
guard let launch, let bytes = launch.data(using: .utf8),
|
|
16
|
+
let config = try JSONSerialization.jsonObject(with: bytes) as? [String: Any],
|
|
17
|
+
let run = config["run"] as? String, !run.isEmpty,
|
|
18
|
+
run.utf8.allSatisfy({ ($0 >= 48 && $0 <= 57) || ($0 >= 97 && $0 <= 122) || $0 == 45 })
|
|
19
|
+
else { throw Snapback4OpenException("E_WITNESS_INPUT: no valid simulator witness launch") }
|
|
20
|
+
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
21
|
+
return documents.appendingPathComponent(".snapback4-witness/\(run)", isDirectory: true)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static func append(_ line: String) throws {
|
|
25
|
+
let directory = try directory()
|
|
26
|
+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
27
|
+
let file = directory.appendingPathComponent("receipts.jsonl")
|
|
28
|
+
if !FileManager.default.fileExists(atPath: file.path) {
|
|
29
|
+
guard FileManager.default.createFile(atPath: file.path, contents: nil) else {
|
|
30
|
+
throw Snapback4OpenException("E_WITNESS_RECEIPT: cannot create receipts file")
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
let handle = try FileHandle(forWritingTo: file)
|
|
34
|
+
defer { try? handle.close() }
|
|
35
|
+
try handle.seekToEnd()
|
|
36
|
+
try handle.write(contentsOf: Data(line.utf8))
|
|
37
|
+
try handle.synchronize()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static func signaled(_ label: String) throws -> Bool {
|
|
41
|
+
guard !label.isEmpty, label.utf8.allSatisfy({
|
|
42
|
+
($0 >= 48 && $0 <= 57) || ($0 >= 65 && $0 <= 90) || ($0 >= 97 && $0 <= 122) || $0 == 45 || $0 == 95
|
|
43
|
+
}) else { throw Snapback4OpenException("E_WITNESS_INPUT: invalid signal label") }
|
|
44
|
+
return FileManager.default.fileExists(atPath: try directory().appendingPathComponent(label).path)
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snapback4-expo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Snapback 4's native module for Expo: the device in Rust (the partition in its own SQLite, the stream applied, the outbox kept, queries and predictions answered) behind one JSON call, and the opener `snapback4/native` takes.",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": { "types": "./src/index.ts", "default": "./src/index.ts" },
|
|
10
|
+
"./witness": { "types": "./src/witness.ts", "default": "./src/witness.ts" },
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
8
13
|
"files": [
|
|
9
14
|
"src",
|
|
10
15
|
"ios",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** The app owns receipt fields; only `witness` is reserved for this protocol. */
|
|
2
|
+
export interface Witness {
|
|
3
|
+
readonly segment: string;
|
|
4
|
+
step<T>(name: string, fn: () => T | Promise<T>): Promise<T>;
|
|
5
|
+
receipt(object: Record<string, unknown>): Promise<void>;
|
|
6
|
+
waitFor(label: string): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WitnessContainer {
|
|
10
|
+
readonly witnessLaunch: string | null;
|
|
11
|
+
witnessAppend(line: string): void;
|
|
12
|
+
witnessSignaled(label: string): boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// @ref LLP 3000.007#20.10 — app-owned receipts, completed steps and container-file signals
|
|
16
|
+
export function createWitness(container: WitnessContainer | null): Witness | null {
|
|
17
|
+
if (!container?.witnessLaunch) return null;
|
|
18
|
+
const { segment, timeoutMs } = JSON.parse(container.witnessLaunch);
|
|
19
|
+
if (typeof segment !== "string" || !segment || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
20
|
+
throw new Error("E_WITNESS_INPUT: invalid witness launch configuration");
|
|
21
|
+
}
|
|
22
|
+
const native = container;
|
|
23
|
+
let current = "launched";
|
|
24
|
+
let active = false;
|
|
25
|
+
const append = (object: Record<string, unknown>, step: string) => {
|
|
26
|
+
if (!object || Array.isArray(object) || typeof object !== "object" || "witness" in object) {
|
|
27
|
+
throw new Error("E_WITNESS_RECEIPT: receipt must be an object without the reserved witness key");
|
|
28
|
+
}
|
|
29
|
+
// Serialize app code before installing the protocol envelope: neither a
|
|
30
|
+
// getter nor toJSON can run again on the parsed plain object.
|
|
31
|
+
let serialized: Record<string, unknown>;
|
|
32
|
+
try {
|
|
33
|
+
serialized = JSON.parse(JSON.stringify(object));
|
|
34
|
+
if (!serialized || Array.isArray(serialized) || typeof serialized !== "object" || "witness" in serialized) throw Error();
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error("E_WITNESS_RECEIPT: serialized receipt must be an object without the reserved witness key");
|
|
37
|
+
}
|
|
38
|
+
native.witnessAppend(`${JSON.stringify({ ...serialized, witness: { at: new Date().toISOString(), step } })}\n`);
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
segment,
|
|
42
|
+
async receipt(object) { append(object, current); },
|
|
43
|
+
async step(name, fn) {
|
|
44
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name) || active) throw new Error("E_WITNESS_STEP: steps must be named and sequential");
|
|
45
|
+
active = true;
|
|
46
|
+
current = `${name}:running`;
|
|
47
|
+
try {
|
|
48
|
+
const value = await fn();
|
|
49
|
+
append({}, name);
|
|
50
|
+
return value;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// The failure's own message is the first thing anyone reading the receipts needs.
|
|
53
|
+
append({ error: error instanceof Error ? error.message : String(error) }, `${name}:failed`);
|
|
54
|
+
throw error;
|
|
55
|
+
} finally { active = false; current = "between-steps"; }
|
|
56
|
+
},
|
|
57
|
+
async waitFor(label) {
|
|
58
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(label)) throw new Error("E_WITNESS_INPUT: invalid signal label");
|
|
59
|
+
const end = Date.now() + timeoutMs;
|
|
60
|
+
while (!native.witnessSignaled(label)) {
|
|
61
|
+
if (Date.now() >= end) throw new Error(`E_WITNESS_TIMEOUT: waiting for ${label}`);
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
package/src/witness.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { requireOptionalNativeModule } from "expo-modules-core";
|
|
2
|
+
import { createWitness, type Witness, type WitnessContainer } from "./witness-core";
|
|
3
|
+
export type { Witness } from "./witness-core";
|
|
4
|
+
|
|
5
|
+
let instance: Witness | null | undefined;
|
|
6
|
+
/** Null on ordinary launches and outside an iOS simulator driven by `snapback4 witness ios`. */
|
|
7
|
+
export function witness(): Witness | null {
|
|
8
|
+
if (instance === undefined) instance = createWitness(requireOptionalNativeModule<WitnessContainer>("Snapback4"));
|
|
9
|
+
return instance;
|
|
10
|
+
}
|