@githolon/dsl 0.13.0 → 0.14.0
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.
|
@@ -41,6 +41,16 @@ class NomosBridge {
|
|
|
41
41
|
final BridgeTransport _transport;
|
|
42
42
|
final Map<int, Completer<Object?>> _pending = {};
|
|
43
43
|
final Map<String, StreamController<List<dynamic>>> _watches = {};
|
|
44
|
+
final StreamController<Map<String, Object?>> _syncEvents =
|
|
45
|
+
StreamController<Map<String, Object?>>.broadcast();
|
|
46
|
+
final StreamController<Map<String, Object?>> _signalEvents =
|
|
47
|
+
StreamController<Map<String, Object?>>.broadcast();
|
|
48
|
+
final StreamController<Map<String, Object?>> _flameEvents =
|
|
49
|
+
StreamController<Map<String, Object?>>.broadcast();
|
|
50
|
+
final StreamController<Map<String, Object?>> _spanEvents =
|
|
51
|
+
StreamController<Map<String, Object?>>.broadcast();
|
|
52
|
+
final List<Map<String, Object?>> _spanBuffer =
|
|
53
|
+
[]; // recent span events — so BOOT spans (emitted during connect, before the UI subscribes) survive
|
|
44
54
|
final Completer<void> _ready = Completer<void>();
|
|
45
55
|
int _seq = 0;
|
|
46
56
|
late final StreamSubscription<Map<String, Object?>> _sub;
|
|
@@ -53,7 +63,8 @@ class NomosBridge {
|
|
|
53
63
|
Future<void> get ready => _ready.future;
|
|
54
64
|
|
|
55
65
|
void _onMessage(Map<String, Object?> m) {
|
|
56
|
-
if (m['v'] != kWebviewBridgeVersion)
|
|
66
|
+
if (m['v'] != kWebviewBridgeVersion)
|
|
67
|
+
return; // a foreign-version message is ignored (forward-compatible)
|
|
57
68
|
final event = m['event'];
|
|
58
69
|
if (event == 'ready') {
|
|
59
70
|
if (!_ready.isCompleted) _ready.complete();
|
|
@@ -61,7 +72,37 @@ class NomosBridge {
|
|
|
61
72
|
}
|
|
62
73
|
if (event == 'watch') {
|
|
63
74
|
final ctrl = _watches[m['subId']];
|
|
64
|
-
if (ctrl != null && !ctrl.isClosed)
|
|
75
|
+
if (ctrl != null && !ctrl.isClosed) {
|
|
76
|
+
if (m['ok'] == false) {
|
|
77
|
+
ctrl.addError(NomosBridgeError(
|
|
78
|
+
(m['error'] ?? 'the watch read failed').toString()));
|
|
79
|
+
} else {
|
|
80
|
+
ctrl.add((m['rows'] as List?) ?? const []);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (event == 'sync' || event == 'link') {
|
|
86
|
+
// autoSync telemetry: each round { event:'sync', reason, result } and socket state { event:'link', status }.
|
|
87
|
+
if (!_syncEvents.isClosed) _syncEvents.add(m);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (event == 'trace') {
|
|
91
|
+
// the realtime cloud flamepath: { event:'trace', trace:{at, oid, edge, clients} } — one per admission.
|
|
92
|
+
if (!_flameEvents.isClosed) _flameEvents.add(m);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (event == 'signal') {
|
|
96
|
+
// signal holon projection: ephemeral coordination such as peer frontiers / lag.
|
|
97
|
+
if (!_signalEvents.isClosed) _signalEvents.add(m);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (event == 'span') {
|
|
101
|
+
// LIVE per-layer span: { event:'span', span:{ phase:'begin'|'end', id, track, name, kind, t0|t1 } }.
|
|
102
|
+
_spanBuffer.add(m);
|
|
103
|
+
if (_spanBuffer.length > 400)
|
|
104
|
+
_spanBuffer.removeRange(0, _spanBuffer.length - 400);
|
|
105
|
+
if (!_spanEvents.isClosed) _spanEvents.add(m);
|
|
65
106
|
return;
|
|
66
107
|
}
|
|
67
108
|
final id = m['id'];
|
|
@@ -71,7 +112,8 @@ class NomosBridge {
|
|
|
71
112
|
if (m['ok'] == true) {
|
|
72
113
|
completer.complete(m['result']);
|
|
73
114
|
} else {
|
|
74
|
-
completer.completeError(NomosBridgeError(
|
|
115
|
+
completer.completeError(NomosBridgeError(
|
|
116
|
+
(m['error'] ?? 'the githolon refused the request').toString()));
|
|
75
117
|
}
|
|
76
118
|
}
|
|
77
119
|
|
|
@@ -81,65 +123,164 @@ class NomosBridge {
|
|
|
81
123
|
final id = ++_seq;
|
|
82
124
|
final completer = Completer<Object?>();
|
|
83
125
|
_pending[id] = completer;
|
|
84
|
-
_transport
|
|
126
|
+
_transport
|
|
127
|
+
.send({'v': kWebviewBridgeVersion, 'id': id, 'op': op, 'args': args});
|
|
85
128
|
return completer.future;
|
|
86
129
|
}
|
|
87
130
|
|
|
88
|
-
Map<String, Object?> _asMap(Object? v) =>
|
|
131
|
+
Map<String, Object?> _asMap(Object? v) =>
|
|
132
|
+
(v as Map?)?.cast<String, Object?>() ?? const {};
|
|
89
133
|
List<Map<String, dynamic>> _asRows(Object? v) =>
|
|
90
|
-
((_asMap(v)['rows'] as List?) ?? const [])
|
|
134
|
+
((_asMap(v)['rows'] as List?) ?? const [])
|
|
135
|
+
.map((e) => (e as Map).cast<String, dynamic>())
|
|
136
|
+
.toList();
|
|
91
137
|
|
|
92
138
|
// ── the runtime-aware surface (the boundary the generated Dart client wraps) ──
|
|
93
139
|
|
|
94
140
|
/// Open the user/estate workspace: pull its ledger, boot the local holon under the installed law.
|
|
95
|
-
Future<void> connect(
|
|
96
|
-
|
|
141
|
+
Future<void> connect(
|
|
142
|
+
{required String cloud,
|
|
143
|
+
required String workspace,
|
|
144
|
+
String? clientId,
|
|
145
|
+
String? authToken,
|
|
146
|
+
String? restoreFrom}) async {
|
|
147
|
+
await _call('connect', {
|
|
148
|
+
'cloud': cloud,
|
|
149
|
+
'workspace': workspace,
|
|
150
|
+
if (clientId != null) 'clientId': clientId,
|
|
151
|
+
if (authToken != null) 'authToken': authToken,
|
|
152
|
+
if (restoreFrom != null) 'restoreFrom': restoreFrom
|
|
153
|
+
});
|
|
97
154
|
}
|
|
98
155
|
|
|
99
156
|
/// LOCAL-FIRST write: author `directiveId` under `domainHash`. Returns the new local head; durable on [sync].
|
|
100
|
-
Future<String> dispatch(
|
|
101
|
-
|
|
157
|
+
Future<String> dispatch(
|
|
158
|
+
{required String domain,
|
|
159
|
+
required String directiveId,
|
|
160
|
+
required Map<String, Object?> payload,
|
|
161
|
+
required String domainHash}) async {
|
|
162
|
+
final r = _asMap(await _call('dispatch', {
|
|
163
|
+
'domain': domain,
|
|
164
|
+
'directiveId': directiveId,
|
|
165
|
+
'payload': payload,
|
|
166
|
+
'domainHash': domainHash
|
|
167
|
+
}));
|
|
102
168
|
return r['head'].toString();
|
|
103
169
|
}
|
|
104
170
|
|
|
105
171
|
/// §5 cross-custody delivery: a `recordShare` carrying a pre-signed §3 quote into a home holon.
|
|
106
|
-
Future<String> deliverShare(
|
|
107
|
-
|
|
172
|
+
Future<String> deliverShare(
|
|
173
|
+
{required Map<String, Object?> quote, required String domainHash}) async {
|
|
174
|
+
final r = _asMap(await _call(
|
|
175
|
+
'deliverShare', {'quote': quote, 'domainHash': domainHash}));
|
|
108
176
|
return r['head'].toString();
|
|
109
177
|
}
|
|
110
178
|
|
|
111
179
|
/// A declared query's rows from the local projection.
|
|
112
|
-
Future<List<Map<String, dynamic>>> query(String queryId,
|
|
180
|
+
Future<List<Map<String, dynamic>>> query(String queryId,
|
|
181
|
+
[Map<String, Object?> params = const {}]) async =>
|
|
113
182
|
_asRows(await _call('query', {'queryId': queryId, 'params': params}));
|
|
114
183
|
|
|
115
184
|
/// A single aggregate by id (the by-id read).
|
|
116
|
-
Future<List<Map<String, dynamic>>> queryById(String id) async =>
|
|
185
|
+
Future<List<Map<String, dynamic>>> queryById(String id) async =>
|
|
186
|
+
_asRows(await _call('queryById', {'id': id}));
|
|
117
187
|
|
|
118
188
|
/// A maintained O(1) count / sum.
|
|
119
|
-
Future<int> count(String countId, [String group = '']) async =>
|
|
120
|
-
|
|
189
|
+
Future<int> count(String countId, [String group = '']) async =>
|
|
190
|
+
((_asMap(await _call('count', {'countId': countId, 'group': group}))[
|
|
191
|
+
'count']) as num)
|
|
192
|
+
.toInt();
|
|
193
|
+
Future<int> sum(String sumId, [String group = '']) async =>
|
|
194
|
+
((_asMap(await _call('sum', {'sumId': sumId, 'group': group}))['sum'])
|
|
195
|
+
as num)
|
|
196
|
+
.toInt();
|
|
121
197
|
|
|
122
198
|
/// Push local writes → the session branch → cloud admission, and pull canonical main back.
|
|
123
|
-
Future<Map<String, Object?>> sync({bool admit = true}) async =>
|
|
199
|
+
Future<Map<String, Object?>> sync({bool admit = true}) async =>
|
|
200
|
+
_asMap(await _call('sync', {'admit': admit}));
|
|
201
|
+
|
|
202
|
+
/// PERSIST: a base64 snapshot of the whole local holon (the bare nomos.git incl. un-synced writes + the
|
|
203
|
+
/// DLQ). Store it on disk and pass it back as `connect(restoreFrom:)` next launch — the app then boots from
|
|
204
|
+
/// LOCAL CUSTODY (no network clone), instant at any chain length. Returns the snapshot string.
|
|
205
|
+
Future<String> export() async =>
|
|
206
|
+
(_asMap(await _call('export'))['snapshot'] ?? '').toString();
|
|
207
|
+
|
|
208
|
+
/// The local ledger tip (an O(1) ref read). Cheap change signal — persistence skips re-exporting an
|
|
209
|
+
/// unchanged snapshot when [head] matches the last saved tip.
|
|
210
|
+
Future<String> head() async =>
|
|
211
|
+
(_asMap(await _call('head'))['head'] ?? '').toString();
|
|
212
|
+
|
|
213
|
+
/// AUTO-SYNC — event-driven background sync. The edge DOORBELL converges peers' writes in real time and a
|
|
214
|
+
/// local write flushes the up leg at once; `fallbackMs` is only a just-in-case safety pull (default 60s).
|
|
215
|
+
/// Each round arrives on [syncEvents] as `{ event:'sync', reason, result }`; socket state as `{event:'link'}`.
|
|
216
|
+
/// Call once after [connect] — then the app never polls.
|
|
217
|
+
Future<void> autoSync(
|
|
218
|
+
{int? debounceMs, int? fallbackMs, String? signalId}) async =>
|
|
219
|
+
await _call('autoSync', {
|
|
220
|
+
if (debounceMs != null) 'debounceMs': debounceMs,
|
|
221
|
+
if (fallbackMs != null) 'fallbackMs': fallbackMs,
|
|
222
|
+
if (signalId != null) 'signalId': signalId
|
|
223
|
+
});
|
|
224
|
+
Future<void> stopAutoSync() async {
|
|
225
|
+
await _call('stopAutoSync');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// Background-sync telemetry: each autoSync round (`event:'sync'`) and doorbell socket state (`event:'link'`).
|
|
229
|
+
Stream<Map<String, Object?>> get syncEvents => _syncEvents.stream;
|
|
230
|
+
|
|
231
|
+
/// Ephemeral signal-holon projections (`event:'signal'`), currently peer frontiers / lag for the workspace.
|
|
232
|
+
Stream<Map<String, Object?>> get signalEvents => _signalEvents.stream;
|
|
233
|
+
|
|
234
|
+
/// THE REALTIME FLAMEPATH: one event per admission the edge broadcasts over the doorbell —
|
|
235
|
+
/// `{ at, oid, edge:{totalMs,scanMs,fetchMs,gateFoldMs,sealMs,intents}, clients:[{client,n}] }`.
|
|
236
|
+
/// Every subscriber sees EVERY admission (yours and your peers') — the cloud track of the flame chart.
|
|
237
|
+
Stream<Map<String, Object?>> get flameEvents => _flameEvents.stream;
|
|
238
|
+
|
|
239
|
+
/// LIVE per-layer spans of the local pipeline (begin/end as they happen): mutex wait, githolon author,
|
|
240
|
+
/// push, converge … The host renders in-progress bars so a hang is visible WHILE it blocks, not after.
|
|
241
|
+
Stream<Map<String, Object?>> get spanEvents => _spanEvents.stream;
|
|
242
|
+
|
|
243
|
+
/// The buffered recent span events — replay these on first build to recover the BOOT flamepath
|
|
244
|
+
/// (fetch wasm / clone ledger / instantiate) that streamed during connect(), before the UI subscribed.
|
|
245
|
+
List<Map<String, Object?>> get recentSpans => List.unmodifiable(_spanBuffer);
|
|
246
|
+
|
|
247
|
+
/// Ledger size (commits) + branch-vs-main (ahead) + projection bytes — the dashboard's slow state.
|
|
248
|
+
Future<Map<String, Object?>> metrics() async =>
|
|
249
|
+
_asMap(await _call('metrics'));
|
|
124
250
|
|
|
125
251
|
/// Diagnostics — is the expected law installed/active/drift-free here? (The generated client surfaces it
|
|
126
252
|
/// so the app can answer "can I author this directive, and if not what deploy fixes it".)
|
|
127
|
-
Future<Map<String, Object?>> lawStatus(String hash) async =>
|
|
253
|
+
Future<Map<String, Object?>> lawStatus(String hash) async =>
|
|
254
|
+
_asMap(await _call('lawStatus', {'hash': hash}));
|
|
128
255
|
|
|
129
256
|
/// Reactive local read: a stream of row-sets that re-emits on every local change. Cancelling the
|
|
130
257
|
/// subscription tells the runner to drop the watch.
|
|
131
|
-
Stream<List<dynamic>> watch(String queryId,
|
|
258
|
+
Stream<List<dynamic>> watch(String queryId,
|
|
259
|
+
[Map<String, Object?> params = const {}]) =>
|
|
132
260
|
_watchStream(kind: 'query', queryId: queryId, params: params);
|
|
133
|
-
Stream<List<dynamic>> watchById(String id) =>
|
|
261
|
+
Stream<List<dynamic>> watchById(String id) =>
|
|
262
|
+
_watchStream(kind: 'byId', id: id);
|
|
134
263
|
|
|
135
|
-
Stream<List<dynamic>> _watchStream(
|
|
264
|
+
Stream<List<dynamic>> _watchStream(
|
|
265
|
+
{required String kind,
|
|
266
|
+
String? queryId,
|
|
267
|
+
Map<String, Object?>? params,
|
|
268
|
+
String? id}) {
|
|
136
269
|
final subId = 'w${++_seq}';
|
|
137
270
|
late final StreamController<List<dynamic>> ctrl;
|
|
138
271
|
ctrl = StreamController<List<dynamic>>(
|
|
139
|
-
onListen: () => _call('watch', {
|
|
272
|
+
onListen: () => _call('watch', {
|
|
273
|
+
'subId': subId,
|
|
274
|
+
'kind': kind,
|
|
275
|
+
if (queryId != null) 'queryId': queryId,
|
|
276
|
+
if (params != null) 'params': params,
|
|
277
|
+
if (id != null) 'id': id
|
|
278
|
+
}),
|
|
140
279
|
onCancel: () async {
|
|
141
280
|
_watches.remove(subId);
|
|
142
|
-
try {
|
|
281
|
+
try {
|
|
282
|
+
await _call('unwatch', {'subId': subId});
|
|
283
|
+
} catch (_) {}
|
|
143
284
|
},
|
|
144
285
|
);
|
|
145
286
|
_watches[subId] = ctrl;
|
|
@@ -157,5 +298,9 @@ class NomosBridge {
|
|
|
157
298
|
if (!w.isClosed) await w.close();
|
|
158
299
|
}
|
|
159
300
|
_watches.clear();
|
|
301
|
+
if (!_syncEvents.isClosed) await _syncEvents.close();
|
|
302
|
+
if (!_signalEvents.isClosed) await _signalEvents.close();
|
|
303
|
+
if (!_flameEvents.isClosed) await _flameEvents.close();
|
|
304
|
+
if (!_spanEvents.isClosed) await _spanEvents.close();
|
|
160
305
|
}
|
|
161
306
|
}
|
|
@@ -19,7 +19,8 @@ class FakeRunner implements BridgeTransport {
|
|
|
19
19
|
|
|
20
20
|
FakeRunner() {
|
|
21
21
|
// the runner announces readiness at construction.
|
|
22
|
-
scheduleMicrotask(
|
|
22
|
+
scheduleMicrotask(
|
|
23
|
+
() => _incoming.add({'v': kWebviewBridgeVersion, 'event': 'ready'}));
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
@override
|
|
@@ -32,34 +33,98 @@ class FakeRunner implements BridgeTransport {
|
|
|
32
33
|
final op = message['op'] as String;
|
|
33
34
|
final args = (message['args'] as Map).cast<String, Object?>();
|
|
34
35
|
// reply exactly as the runner's ops do (the contract both ends share).
|
|
35
|
-
Map<String, Object?> reply(Object? result) =>
|
|
36
|
+
Map<String, Object?> reply(Object? result) =>
|
|
37
|
+
{'v': kWebviewBridgeVersion, 'id': id, 'ok': true, 'result': result};
|
|
36
38
|
scheduleMicrotask(() {
|
|
37
39
|
switch (op) {
|
|
38
|
-
case 'connect':
|
|
40
|
+
case 'connect':
|
|
41
|
+
_incoming.add(reply({'workspace': args['workspace']}));
|
|
42
|
+
break;
|
|
39
43
|
case 'dispatch':
|
|
40
44
|
// a directive the law refuses answers ok:false (the runner frames the gate's verdict).
|
|
41
45
|
if (args['directiveId'] == 'refuseMe') {
|
|
42
|
-
_incoming.add({
|
|
46
|
+
_incoming.add({
|
|
47
|
+
'v': kWebviewBridgeVersion,
|
|
48
|
+
'id': id,
|
|
49
|
+
'ok': false,
|
|
50
|
+
'error': 'the law refused this intent'
|
|
51
|
+
});
|
|
43
52
|
} else {
|
|
44
53
|
_incoming.add(reply({'head': 'HEAD-${args['directiveId']}'}));
|
|
45
54
|
}
|
|
46
55
|
break;
|
|
47
|
-
case 'deliverShare':
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
case '
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
case 'deliverShare':
|
|
57
|
+
_incoming.add(reply({'head': 'HEAD-share'}));
|
|
58
|
+
break;
|
|
59
|
+
case 'query':
|
|
60
|
+
_incoming.add(reply({
|
|
61
|
+
'rows': [
|
|
62
|
+
{
|
|
63
|
+
'id': 'a',
|
|
64
|
+
'data': {'q': args['queryId']}
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}));
|
|
68
|
+
break;
|
|
69
|
+
case 'queryById':
|
|
70
|
+
_incoming.add(reply({
|
|
71
|
+
'rows': [
|
|
72
|
+
{
|
|
73
|
+
'id': args['id'],
|
|
74
|
+
'data': {'ok': true}
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}));
|
|
78
|
+
break;
|
|
79
|
+
case 'count':
|
|
80
|
+
_incoming.add(reply({'count': 7}));
|
|
81
|
+
break;
|
|
82
|
+
case 'sync':
|
|
83
|
+
_incoming.add(reply({'pushed': true}));
|
|
84
|
+
break;
|
|
85
|
+
case 'autoSync':
|
|
86
|
+
_incoming.add(reply({'started': true}));
|
|
87
|
+
break;
|
|
88
|
+
case 'lawStatus':
|
|
89
|
+
_incoming.add(reply({'installed': true, 'drifted': false}));
|
|
90
|
+
break;
|
|
91
|
+
case 'badop':
|
|
92
|
+
_incoming.add({
|
|
93
|
+
'v': kWebviewBridgeVersion,
|
|
94
|
+
'id': id,
|
|
95
|
+
'ok': false,
|
|
96
|
+
'error': 'the law refused this intent'
|
|
97
|
+
});
|
|
98
|
+
break;
|
|
99
|
+
case 'watch':
|
|
100
|
+
lastWatchSubId = args['subId'] as String;
|
|
101
|
+
_incoming.add(reply({'subId': args['subId']}));
|
|
102
|
+
break;
|
|
103
|
+
case 'unwatch':
|
|
104
|
+
_incoming.add(reply({'subId': args['subId']}));
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
_incoming.add({
|
|
108
|
+
'v': kWebviewBridgeVersion,
|
|
109
|
+
'id': id,
|
|
110
|
+
'ok': false,
|
|
111
|
+
'error': "unknown op '$op'"
|
|
112
|
+
});
|
|
57
113
|
}
|
|
58
114
|
});
|
|
59
115
|
}
|
|
60
116
|
|
|
61
117
|
/// Simulate the holon firing a reactive change to an active watch.
|
|
62
|
-
void pushWatch(String subId, List<dynamic> rows) => _incoming.add({
|
|
118
|
+
void pushWatch(String subId, List<dynamic> rows) => _incoming.add({
|
|
119
|
+
'v': kWebviewBridgeVersion,
|
|
120
|
+
'event': 'watch',
|
|
121
|
+
'subId': subId,
|
|
122
|
+
'rows': rows
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/// Simulate the runner forwarding a signal holon projection.
|
|
126
|
+
void pushSignal(Map<String, Object?> signal) => _incoming
|
|
127
|
+
.add({'v': kWebviewBridgeVersion, 'event': 'signal', 'signal': signal});
|
|
63
128
|
}
|
|
64
129
|
|
|
65
130
|
/// A transport that answers EVERY request with one canned `result` (the golden corpus's reply) and records
|
|
@@ -69,7 +134,8 @@ class ReplayTransport implements BridgeTransport {
|
|
|
69
134
|
final _incoming = StreamController<Map<String, Object?>>.broadcast();
|
|
70
135
|
Map<String, Object?>? lastRequest;
|
|
71
136
|
ReplayTransport(this.result) {
|
|
72
|
-
scheduleMicrotask(
|
|
137
|
+
scheduleMicrotask(
|
|
138
|
+
() => _incoming.add({'v': kWebviewBridgeVersion, 'event': 'ready'}));
|
|
73
139
|
}
|
|
74
140
|
@override
|
|
75
141
|
Stream<Map<String, Object?>> get messages => _incoming.stream;
|
|
@@ -77,7 +143,8 @@ class ReplayTransport implements BridgeTransport {
|
|
|
77
143
|
void send(Map<String, Object?> message) {
|
|
78
144
|
lastRequest = message;
|
|
79
145
|
final id = message['id'];
|
|
80
|
-
scheduleMicrotask(() => _incoming.add(
|
|
146
|
+
scheduleMicrotask(() => _incoming.add(
|
|
147
|
+
{'v': kWebviewBridgeVersion, 'id': id, 'ok': true, 'result': result}));
|
|
81
148
|
}
|
|
82
149
|
}
|
|
83
150
|
|
|
@@ -91,24 +158,37 @@ Map<String, Object?> _loadCorpus() {
|
|
|
91
158
|
];
|
|
92
159
|
for (final p in candidates) {
|
|
93
160
|
final f = File(p);
|
|
94
|
-
if (f.existsSync())
|
|
161
|
+
if (f.existsSync())
|
|
162
|
+
return (jsonDecode(f.readAsStringSync()) as Map).cast<String, Object?>();
|
|
95
163
|
}
|
|
96
|
-
fail(
|
|
164
|
+
fail(
|
|
165
|
+
'golden corpus not found from ${Directory.current.path}; tried $candidates');
|
|
97
166
|
}
|
|
98
167
|
|
|
99
168
|
/// Drive one corpus op through the typed bridge method and return its typed result (so we can compare it to
|
|
100
169
|
/// what the corpus says the unwrapped value should be).
|
|
101
|
-
Future<Object?> _invoke(
|
|
170
|
+
Future<Object?> _invoke(
|
|
171
|
+
NomosBridge b, String op, Map<String, Object?> a) async {
|
|
102
172
|
switch (op) {
|
|
103
173
|
case 'connect':
|
|
104
|
-
await b.connect(
|
|
174
|
+
await b.connect(
|
|
175
|
+
cloud: a['cloud'] as String,
|
|
176
|
+
workspace: a['workspace'] as String,
|
|
177
|
+
clientId: a['clientId'] as String?);
|
|
105
178
|
return null;
|
|
106
179
|
case 'dispatch':
|
|
107
|
-
return b.dispatch(
|
|
180
|
+
return b.dispatch(
|
|
181
|
+
domain: a['domain'] as String,
|
|
182
|
+
directiveId: a['directiveId'] as String,
|
|
183
|
+
payload: (a['payload'] as Map).cast<String, Object?>(),
|
|
184
|
+
domainHash: a['domainHash'] as String);
|
|
108
185
|
case 'deliverShare':
|
|
109
|
-
return b.deliverShare(
|
|
186
|
+
return b.deliverShare(
|
|
187
|
+
quote: (a['quote'] as Map).cast<String, Object?>(),
|
|
188
|
+
domainHash: a['domainHash'] as String);
|
|
110
189
|
case 'query':
|
|
111
|
-
return b.query(
|
|
190
|
+
return b.query(
|
|
191
|
+
a['queryId'] as String, (a['params'] as Map).cast<String, Object?>());
|
|
112
192
|
case 'queryById':
|
|
113
193
|
return b.queryById(a['id'] as String);
|
|
114
194
|
case 'count':
|
|
@@ -153,7 +233,8 @@ void main() {
|
|
|
153
233
|
group('golden corpus (shared with the JS runner)', () {
|
|
154
234
|
final corpus = _loadCorpus();
|
|
155
235
|
final cases = (corpus['cases'] as List).cast<Map<String, Object?>>();
|
|
156
|
-
test('version pins the bridge version',
|
|
236
|
+
test('version pins the bridge version',
|
|
237
|
+
() => expect(corpus['version'], kWebviewBridgeVersion));
|
|
157
238
|
for (final c in cases) {
|
|
158
239
|
final op = c['op'] as String;
|
|
159
240
|
final args = (c['args'] as Map).cast<String, Object?>();
|
|
@@ -163,8 +244,12 @@ void main() {
|
|
|
163
244
|
final bridge = NomosBridge(transport);
|
|
164
245
|
await bridge.ready;
|
|
165
246
|
final typed = await _invoke(bridge, op, args);
|
|
166
|
-
expect((transport.lastRequest!['args'] as Map).cast<String, Object?>(),
|
|
167
|
-
|
|
247
|
+
expect((transport.lastRequest!['args'] as Map).cast<String, Object?>(),
|
|
248
|
+
equals(args),
|
|
249
|
+
reason: 'sent request args');
|
|
250
|
+
if (op != 'connect')
|
|
251
|
+
expect(typed, equals(_expectedTyped(op, result)),
|
|
252
|
+
reason: 'parsed typed result');
|
|
168
253
|
await bridge.dispose();
|
|
169
254
|
});
|
|
170
255
|
}
|
|
@@ -172,50 +257,107 @@ void main() {
|
|
|
172
257
|
|
|
173
258
|
late FakeRunner runner;
|
|
174
259
|
late NomosBridge bridge;
|
|
175
|
-
setUp(() {
|
|
260
|
+
setUp(() {
|
|
261
|
+
runner = FakeRunner();
|
|
262
|
+
bridge = NomosBridge(runner);
|
|
263
|
+
});
|
|
176
264
|
tearDown(() => bridge.dispose());
|
|
177
265
|
|
|
178
266
|
test('ready completes once the runner boots', () async {
|
|
179
267
|
await bridge.ready.timeout(const Duration(seconds: 1));
|
|
180
268
|
});
|
|
181
269
|
|
|
182
|
-
test('dispatch round-trips a typed head; args reach the runner verbatim',
|
|
270
|
+
test('dispatch round-trips a typed head; args reach the runner verbatim',
|
|
271
|
+
() async {
|
|
183
272
|
await bridge.ready;
|
|
184
|
-
final head = await bridge.dispatch(
|
|
273
|
+
final head = await bridge.dispatch(
|
|
274
|
+
domain: 'guestbook',
|
|
275
|
+
directiveId: 'signGuestbook',
|
|
276
|
+
payload: {'author': 'ada'},
|
|
277
|
+
domainHash: 'abc');
|
|
185
278
|
expect(head, 'HEAD-signGuestbook');
|
|
186
279
|
final req = runner.sent.firstWhere((m) => m['op'] == 'dispatch');
|
|
187
280
|
expect((req['args'] as Map)['payload'], {'author': 'ada'});
|
|
188
281
|
expect((req['args'] as Map)['domainHash'], 'abc');
|
|
189
282
|
});
|
|
190
283
|
|
|
191
|
-
test(
|
|
284
|
+
test(
|
|
285
|
+
'connect / deliverShare / query / queryById / count / sync / lawStatus all wrap the runner',
|
|
286
|
+
() async {
|
|
192
287
|
await bridge.ready;
|
|
193
288
|
await bridge.connect(cloud: 'https://c', workspace: 'ws', clientId: 'c1');
|
|
194
|
-
expect(
|
|
195
|
-
|
|
289
|
+
expect(
|
|
290
|
+
await bridge.deliverShare(quote: {'publicKey': 'k'}, domainHash: 'h'),
|
|
291
|
+
'HEAD-share');
|
|
292
|
+
expect((await bridge.query('byAuthor', {'author': 'ada'})).first['data'],
|
|
293
|
+
{'q': 'byAuthor'});
|
|
196
294
|
expect((await bridge.queryById('Entry_1')).first['id'], 'Entry_1');
|
|
197
295
|
expect(await bridge.count('entries'), 7);
|
|
198
296
|
expect(await bridge.sync(), {'pushed': true});
|
|
199
|
-
expect(
|
|
297
|
+
expect(
|
|
298
|
+
await bridge.lawStatus('abc'), {'installed': true, 'drifted': false});
|
|
200
299
|
});
|
|
201
300
|
|
|
202
|
-
test(
|
|
301
|
+
test(
|
|
302
|
+
'watch is a reactive stream; a runner push reaches the listener; cancel unwatches',
|
|
303
|
+
() async {
|
|
203
304
|
await bridge.ready;
|
|
204
305
|
final got = <List<dynamic>>[];
|
|
205
306
|
final sub = bridge.watch('byAuthor', {'author': 'ada'}).listen(got.add);
|
|
206
307
|
await Future<void>.delayed(Duration.zero); // let the watch register
|
|
207
|
-
runner.pushWatch(runner.lastWatchSubId!, [
|
|
308
|
+
runner.pushWatch(runner.lastWatchSubId!, [
|
|
309
|
+
{
|
|
310
|
+
'id': 'a',
|
|
311
|
+
'data': {'n': 2}
|
|
312
|
+
}
|
|
313
|
+
]);
|
|
208
314
|
await Future<void>.delayed(Duration.zero);
|
|
209
|
-
expect(got, [
|
|
315
|
+
expect(got, [
|
|
316
|
+
[
|
|
317
|
+
{
|
|
318
|
+
'id': 'a',
|
|
319
|
+
'data': {'n': 2}
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
]);
|
|
210
323
|
await sub.cancel();
|
|
211
324
|
expect(runner.sent.any((m) => m['op'] == 'unwatch'), isTrue);
|
|
212
325
|
});
|
|
213
326
|
|
|
214
|
-
test('
|
|
327
|
+
test('signal events are forwarded on their own stream', () async {
|
|
328
|
+
await bridge.ready;
|
|
329
|
+
final got = <Map<String, Object?>>[];
|
|
330
|
+
final sub = bridge.signalEvents.listen(got.add);
|
|
331
|
+
runner.pushSignal({
|
|
332
|
+
'type': 'signal.projection',
|
|
333
|
+
'domain': 'presence',
|
|
334
|
+
'projection': {
|
|
335
|
+
'peers': [
|
|
336
|
+
{'clientId': 'prof-a', 'behindBest': 141},
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
await Future<void>.delayed(Duration.zero);
|
|
341
|
+
expect(got.single['signal'], isA<Map>());
|
|
342
|
+
expect(((got.single['signal'] as Map)['projection'] as Map)['peers'],
|
|
343
|
+
isA<List>());
|
|
344
|
+
await sub.cancel();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('autoSync forwards a scoped signal id', () async {
|
|
348
|
+
await bridge.ready;
|
|
349
|
+
await bridge.autoSync(signalId: 'signal:ws:list:main');
|
|
350
|
+
final req = runner.sent.firstWhere((m) => m['op'] == 'autoSync');
|
|
351
|
+
expect((req['args'] as Map)['signalId'], 'signal:ws:list:main');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('a law refusal throws NomosBridgeError with the verbatim message',
|
|
355
|
+
() async {
|
|
215
356
|
await bridge.ready;
|
|
216
357
|
Object? err;
|
|
217
358
|
try {
|
|
218
|
-
await bridge.dispatch(
|
|
359
|
+
await bridge.dispatch(
|
|
360
|
+
domain: 'x', directiveId: 'refuseMe', payload: {}, domainHash: 'h');
|
|
219
361
|
} on NomosBridgeError catch (e) {
|
|
220
362
|
err = e;
|
|
221
363
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
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",
|
|
@@ -641,6 +641,11 @@ export const AuthProvider = aggregate("AuthProvider", {
|
|
|
641
641
|
issuer: t.string().merge(Lww),
|
|
642
642
|
/** The provider's public JWKS endpoint (asymmetric verification keys). */
|
|
643
643
|
jwksUrl: t.string().merge(Lww),
|
|
644
|
+
/** OPTIONAL expected `aud` claim. When the platform sets it, a token's `aud` MUST carry this value
|
|
645
|
+
* (defense-in-depth beyond the issuer match — e.g. a Firebase project id, an Auth0 API audience).
|
|
646
|
+
* Absent ⇒ issuer match alone. This is LAW-STATE the PLATFORM owns; the host merely enforces it as a
|
|
647
|
+
* capability (it never chooses the audience). */
|
|
648
|
+
audience: t.string().merge(Lww),
|
|
644
649
|
/** Human label for audit ("acme supabase", ...). */
|
|
645
650
|
label: t.string().merge(Lww),
|
|
646
651
|
/** Who set it (attribution). */
|
|
@@ -661,6 +666,7 @@ export const setAuthProvider = directive("setAuthProvider")
|
|
|
661
666
|
// deterministic engine does not provide — a regex keeps the plan pure.
|
|
662
667
|
issuer: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
|
|
663
668
|
jwksUrl: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
|
|
669
|
+
audience: z.string().min(1).optional(),
|
|
664
670
|
label: z.string().min(1),
|
|
665
671
|
setBy: z.string().min(1),
|
|
666
672
|
setAt: epochMs,
|
|
@@ -672,6 +678,7 @@ export const setAuthProvider = directive("setAuthProvider")
|
|
|
672
678
|
withMarker(set(ap, "scope", "platform"), "ensures"),
|
|
673
679
|
set(ap, "issuer", p.issuer),
|
|
674
680
|
set(ap, "jwksUrl", p.jwksUrl),
|
|
681
|
+
...(p.audience ? [set(ap, "audience", p.audience)] : []),
|
|
675
682
|
set(ap, "label", p.label),
|
|
676
683
|
set(ap, "setBy", p.setBy),
|
|
677
684
|
set(ap, "setAt", p.setAt),
|