@githolon/dsl 0.77.0 → 0.77.1
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/dart/lib/src/webview_bridge.dart +66 -2
- package/dart/test/acquire_session_test.dart +1 -1
- package/dart/test/bundle_version_pairing_test.dart +40 -0
- package/dart/test/fixtures/wedge/wedge-entry.mjs +1 -1
- package/dart/test/stale_bundle_test.dart +65 -0
- package/dart/test/webview_bridge_test.dart +2 -2
- package/package.json +1 -1
|
@@ -43,6 +43,34 @@ typedef NomosEngine = NomosBridge;
|
|
|
43
43
|
/// The protocol version the runner and the bridge agree on (`webview-runner.mjs` `WEBVIEW_BRIDGE_VERSION`).
|
|
44
44
|
const int kWebviewBridgeVersion = 1;
|
|
45
45
|
|
|
46
|
+
/// THE EXPECTED RUNNER-BUNDLE VERSION FLOOR — the @githolon/client release this nomos_client version
|
|
47
|
+
/// ships alongside. The bundle stamps its own version into the ready frame; anything OLDER (or a
|
|
48
|
+
/// pre-stamp bundle, which reports nothing) is a stale baked asset and fails loudly at boot.
|
|
49
|
+
/// MUST move with the paired @githolon/client release — a release-hygiene test pairs it with the
|
|
50
|
+
/// runner stamp (the pubspec-pins pattern).
|
|
51
|
+
const String kExpectedRunnerBundleVersion = '0.77.1';
|
|
52
|
+
|
|
53
|
+
/// A STALE BAKED RUNNER BUNDLE: the app's built assets carry an older nomos-runner.js than this
|
|
54
|
+
/// nomos_client release ships — Flutter's build cache kept it across a pub upgrade. Thrown from
|
|
55
|
+
/// [NomosBridge.ready] at boot (loud, immediate, named) instead of letting one-version-old runtime
|
|
56
|
+
/// behavior masquerade as framework bugs.
|
|
57
|
+
class StaleRunnerBundle implements Exception {
|
|
58
|
+
StaleRunnerBundle({required this.found, required this.expected});
|
|
59
|
+
|
|
60
|
+
/// The bundle's self-reported version (null = a pre-stamp bundle, definitionally stale).
|
|
61
|
+
final String? found;
|
|
62
|
+
|
|
63
|
+
/// The minimum version this nomos_client release expects ([kExpectedRunnerBundleVersion]).
|
|
64
|
+
final String expected;
|
|
65
|
+
|
|
66
|
+
@override
|
|
67
|
+
String toString() =>
|
|
68
|
+
'StaleRunnerBundle: the baked nomos-runner.js is ${found ?? "un-stamped (pre-0.77.1)"} but this '
|
|
69
|
+
'nomos_client expects >= $expected. Flutter kept an old bundle across the upgrade — run '
|
|
70
|
+
'`flutter clean` and rebuild (verify with shasum of flutter_assets/.../nomos-runner.js), or pass '
|
|
71
|
+
'NomosBridge(..., allowStaleRunnerBundle: true) for a deliberate mixed-version setup.';
|
|
72
|
+
}
|
|
73
|
+
|
|
46
74
|
/// The platform channel to the WebView runner: send a JSON-shaped message; receive the runner's messages.
|
|
47
75
|
/// Web impl jsifies/dartifies over `dart:js_util`; mobile impl JSON-encodes over a WebView JS-channel. The
|
|
48
76
|
/// bridge treats messages as plain `Map<String, Object?>` — the transport owns the channel serialization.
|
|
@@ -743,13 +771,37 @@ class NomosBridge {
|
|
|
743
771
|
final String _traceId = _newTraceId();
|
|
744
772
|
late final StreamSubscription<Map<String, Object?>> _sub;
|
|
745
773
|
|
|
746
|
-
NomosBridge(this._transport) {
|
|
774
|
+
NomosBridge(this._transport, {this.allowStaleRunnerBundle = false}) {
|
|
747
775
|
_sub = _transport.messages.listen(_onMessage);
|
|
748
776
|
}
|
|
749
777
|
|
|
778
|
+
/// Escape hatch for DELIBERATE mixed-version setups (framework dev, A/B probes): accept a runner
|
|
779
|
+
/// bundle older than [kExpectedRunnerBundleVersion] instead of failing [ready].
|
|
780
|
+
final bool allowStaleRunnerBundle;
|
|
781
|
+
|
|
782
|
+
/// null = acceptable; otherwise the typed boot error. "unbundled-dev" (the in-repo source lane)
|
|
783
|
+
/// always passes; a missing stamp means a pre-stamp (definitionally stale) bundle.
|
|
784
|
+
StaleRunnerBundle? _staleBundleError(Object? bundleVersion) {
|
|
785
|
+
final v = bundleVersion is String ? bundleVersion : null;
|
|
786
|
+
if (v == 'unbundled-dev') return null;
|
|
787
|
+
if (v != null && _semverAtLeast(v, kExpectedRunnerBundleVersion)) return null;
|
|
788
|
+
return StaleRunnerBundle(found: v, expected: kExpectedRunnerBundleVersion);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
static bool _semverAtLeast(String v, String min) {
|
|
792
|
+
List<int> parse(String s) =>
|
|
793
|
+
s.split('-').first.split('.').map((p) => int.tryParse(p) ?? 0).toList();
|
|
794
|
+
final a = parse(v), b = parse(min);
|
|
795
|
+
for (var i = 0; i < 3; i++) {
|
|
796
|
+
final x = i < a.length ? a[i] : 0, y = i < b.length ? b[i] : 0;
|
|
797
|
+
if (x != y) return x > y;
|
|
798
|
+
}
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
|
|
750
802
|
/// A SESSION VIEW's super-constructor: shares the parent's [_transport] but does NOT subscribe (the parent
|
|
751
803
|
/// owns the one message loop + all correlation). Only [NomosSession] uses it.
|
|
752
|
-
NomosBridge._view(this._transport);
|
|
804
|
+
NomosBridge._view(this._transport) : allowStaleRunnerBundle = false;
|
|
753
805
|
|
|
754
806
|
static Random _traceRandom() {
|
|
755
807
|
try {
|
|
@@ -798,6 +850,18 @@ class NomosBridge {
|
|
|
798
850
|
}
|
|
799
851
|
final event = m['event'];
|
|
800
852
|
if (event == 'ready') {
|
|
853
|
+
// THE STALE-BAKED-BUNDLE CHECK: Flutter's build system can silently keep an OLD runner bundle
|
|
854
|
+
// baked into an app across a correct pub upgrade — invisible version skew that cost a real
|
|
855
|
+
// tenant two archaeology hunts. The bundle stamps its release version into the ready frame;
|
|
856
|
+
// an older-than-expected (or missing — i.e. pre-stamp) bundle fails LOUDLY at boot with the
|
|
857
|
+
// remedy, instead of misbehaving subtly at runtime. "unbundled-dev" (the in-repo source lane
|
|
858
|
+
// the test harness drives) always passes; `allowStaleRunnerBundle: true` on the constructor is
|
|
859
|
+
// the deliberate-mixed-versions escape hatch.
|
|
860
|
+
final stale = _staleBundleError(m['bundleVersion']);
|
|
861
|
+
if (stale != null && !allowStaleRunnerBundle) {
|
|
862
|
+
if (!_ready.isCompleted) _ready.completeError(stale);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
801
865
|
if (!_ready.isCompleted) _ready.complete();
|
|
802
866
|
return;
|
|
803
867
|
}
|
|
@@ -17,7 +17,7 @@ class FakeRunner implements BridgeTransport {
|
|
|
17
17
|
|
|
18
18
|
FakeRunner({this.resident = const []}) {
|
|
19
19
|
scheduleMicrotask(
|
|
20
|
-
() => _incoming.add({'v': kWebviewBridgeVersion, 'event': 'ready'}));
|
|
20
|
+
() => _incoming.add({'v': kWebviewBridgeVersion, 'event': 'ready', 'bundleVersion': 'unbundled-dev'}));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
int get connectCount => sent.where((m) => m['op'] == 'connect').length;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// THE VERSION-PAIRING TRIPWIRE (the pubspec-pins pattern): kExpectedRunnerBundleVersion must equal
|
|
2
|
+
// the @githolon/client version this repo is about to release (cloud/web-client/package.json) — a
|
|
3
|
+
// client release without the Dart floor bump (or vice versa) fails the suite, so the stale-bundle
|
|
4
|
+
// check can neither lag (missing real staleness) nor lead (refusing the current bundle).
|
|
5
|
+
import 'dart:convert';
|
|
6
|
+
import 'dart:io';
|
|
7
|
+
|
|
8
|
+
import 'package:nomos_client/nomos_headless.dart';
|
|
9
|
+
import 'package:test/test.dart';
|
|
10
|
+
|
|
11
|
+
void main() {
|
|
12
|
+
test('kExpectedRunnerBundleVersion pairs with cloud/web-client/package.json', () {
|
|
13
|
+
final f = File('${Directory.current.path}/../../cloud/web-client/package.json');
|
|
14
|
+
if (!f.existsSync()) {
|
|
15
|
+
markTestSkipped('monorepo layout not present (published-package test run)');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
final v = (jsonDecode(f.readAsStringSync()) as Map)['version'] as String;
|
|
19
|
+
if (kExpectedRunnerBundleVersion == v) return; // released state: exact pair
|
|
20
|
+
// PENDING-RELEASE state: the floor may lead package.json by exactly the bump a queued
|
|
21
|
+
// changeset will apply (the changesets flow bumps package.json only when the version PR
|
|
22
|
+
// merges). Anything else — lagging, or leading with no changeset queued — is drift.
|
|
23
|
+
final changesets = Directory('${Directory.current.path}/../../.changeset')
|
|
24
|
+
.listSync()
|
|
25
|
+
.whereType<File>()
|
|
26
|
+
.where((c) => c.path.endsWith('.md') && !c.path.endsWith('README.md'))
|
|
27
|
+
.where((c) => c.readAsStringSync().contains('@githolon/client'));
|
|
28
|
+
expect(changesets, isNotEmpty,
|
|
29
|
+
reason: 'kExpectedRunnerBundleVersion ($kExpectedRunnerBundleVersion) != @githolon/client '
|
|
30
|
+
'($v) and NO client changeset is queued — bump the floor WITH the client release '
|
|
31
|
+
'(the stale-bundle floor must move with the bundle it ships beside)');
|
|
32
|
+
List<int> parse(String x) =>
|
|
33
|
+
x.split('.').map((p) => int.tryParse(p) ?? 0).toList();
|
|
34
|
+
final a = parse(kExpectedRunnerBundleVersion), b = parse(v);
|
|
35
|
+
final leads = a[0] > b[0] ||
|
|
36
|
+
(a[0] == b[0] && (a[1] > b[1] || (a[1] == b[1] && a[2] > b[2])));
|
|
37
|
+
expect(leads, isTrue,
|
|
38
|
+
reason: 'the floor must LEAD (never lag) package.json while a release is queued');
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -7,7 +7,7 @@ import { createInterface } from "node:readline";
|
|
|
7
7
|
const post = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
|
8
8
|
const wedgeOp = process.env.NOMOS_WEDGE_OP || "connect"; // default: wedge the KEYED reconnect (co2's shape)
|
|
9
9
|
const wedgeOnlyKeyed = process.env.NOMOS_WEDGE_ONLY_KEYED !== "0";
|
|
10
|
-
post({ v: 1, event: "ready" });
|
|
10
|
+
post({ v: 1, event: "ready", bundleVersion: "unbundled-dev" });
|
|
11
11
|
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
12
12
|
rl.on("line", (line) => {
|
|
13
13
|
const s = line.trim();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// THE STALE-BAKED-BUNDLE CHECK (the countermeasure to the co2 incidents where Flutter's build
|
|
2
|
+
// cache silently kept an old nomos-runner.js across a correct pub upgrade). ready carries the
|
|
3
|
+
// bundle's self-stamped version; the bridge refuses stale/pre-stamp bundles LOUDLY at boot.
|
|
4
|
+
import 'dart:async';
|
|
5
|
+
|
|
6
|
+
import 'package:nomos_client/nomos_headless.dart';
|
|
7
|
+
import 'package:test/test.dart';
|
|
8
|
+
|
|
9
|
+
class _StampedRunner implements BridgeTransport {
|
|
10
|
+
_StampedRunner(this.stamp) {
|
|
11
|
+
scheduleMicrotask(() => _incoming.add({
|
|
12
|
+
'v': kWebviewBridgeVersion,
|
|
13
|
+
'event': 'ready',
|
|
14
|
+
if (stamp != null) 'bundleVersion': stamp,
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
final String? stamp;
|
|
18
|
+
final _incoming = StreamController<Map<String, Object?>>.broadcast();
|
|
19
|
+
@override
|
|
20
|
+
Stream<Map<String, Object?>> get messages => _incoming.stream;
|
|
21
|
+
@override
|
|
22
|
+
void send(Map<String, Object?> message) {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
void main() {
|
|
26
|
+
test('a PRE-STAMP bundle (no bundleVersion) fails ready with StaleRunnerBundle', () async {
|
|
27
|
+
final b = NomosBridge(_StampedRunner(null));
|
|
28
|
+
await expectLater(b.ready, throwsA(isA<StaleRunnerBundle>()));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('an OLDER stamp fails ready, and the error names found/expected + the remedy', () async {
|
|
32
|
+
final b = NomosBridge(_StampedRunner('0.53.1'));
|
|
33
|
+
Object? err;
|
|
34
|
+
try {
|
|
35
|
+
await b.ready;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
err = e;
|
|
38
|
+
}
|
|
39
|
+
expect(err, isA<StaleRunnerBundle>());
|
|
40
|
+
final s = err.toString();
|
|
41
|
+
expect(s, contains('0.53.1'));
|
|
42
|
+
expect(s, contains(kExpectedRunnerBundleVersion));
|
|
43
|
+
expect(s, contains('flutter clean'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('the CURRENT stamp passes', () async {
|
|
47
|
+
final b = NomosBridge(_StampedRunner(kExpectedRunnerBundleVersion));
|
|
48
|
+
await b.ready; // completes
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('a NEWER stamp passes (forward-compatible)', () async {
|
|
52
|
+
final b = NomosBridge(_StampedRunner('99.0.0'));
|
|
53
|
+
await b.ready;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('unbundled-dev (the in-repo source lane) always passes', () async {
|
|
57
|
+
final b = NomosBridge(_StampedRunner('unbundled-dev'));
|
|
58
|
+
await b.ready;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('allowStaleRunnerBundle is the deliberate escape hatch', () async {
|
|
62
|
+
final b = NomosBridge(_StampedRunner('0.1.0'), allowStaleRunnerBundle: true);
|
|
63
|
+
await b.ready;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -20,7 +20,7 @@ class FakeRunner implements BridgeTransport {
|
|
|
20
20
|
FakeRunner() {
|
|
21
21
|
// the runner announces readiness at construction.
|
|
22
22
|
scheduleMicrotask(
|
|
23
|
-
() => _incoming.add({'v': kWebviewBridgeVersion, 'event': 'ready'}));
|
|
23
|
+
() => _incoming.add({'v': kWebviewBridgeVersion, 'event': 'ready', 'bundleVersion': 'unbundled-dev'}));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
@override
|
|
@@ -183,7 +183,7 @@ class ReplayTransport implements BridgeTransport {
|
|
|
183
183
|
Map<String, Object?>? lastRequest;
|
|
184
184
|
ReplayTransport(this.result) {
|
|
185
185
|
scheduleMicrotask(
|
|
186
|
-
() => _incoming.add({'v': kWebviewBridgeVersion, 'event': 'ready'}));
|
|
186
|
+
() => _incoming.add({'v': kWebviewBridgeVersion, 'event': 'ready', 'bundleVersion': 'unbundled-dev'}));
|
|
187
187
|
}
|
|
188
188
|
@override
|
|
189
189
|
Stream<Map<String, Object?>> get messages => _incoming.stream;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.77.
|
|
3
|
+
"version": "0.77.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|