snapback4-expo 0.2.6 → 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 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 (`WITNESS_DEV_CLIENT=1 node witness/expo.mjs`, recorded in
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.
@@ -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.6",
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
+ }