@githolon/dsl 0.117.1 → 0.119.9

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.
Files changed (35) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/compile_proof.mjs +16 -0
  3. package/dart/CHANGELOG.md +41 -0
  4. package/dart/lib/src/application_status.dart +58 -2
  5. package/dart/lib/src/peer_application_streams.dart +7 -1
  6. package/dart/lib/src/reception/dart_reception.dart +92 -12
  7. package/dart/lib/src/reception/reception_session.dart +10 -2
  8. package/dart/lib/src/replica_readiness.dart +200 -50
  9. package/dart/lib/src/runner/node-entry.mjs +2480 -1021
  10. package/dart/lib/src/sync_status_projection.dart +346 -4
  11. package/dart/lib/src/sync_status_reducer.dart +44 -0
  12. package/dart/lib/src/test_harness.dart +1 -0
  13. package/dart/lib/src/typed_watch_registry.dart +67 -7
  14. package/dart/pubspec.lock +1 -1
  15. package/dart/pubspec.yaml +2 -2
  16. package/dart/test/copy_repair_status_test.dart +238 -0
  17. package/dart/test/reads_never_wait_test.dart +387 -0
  18. package/dart/test/replica_readiness_test.dart +223 -109
  19. package/dart/test/sync_status_reducer_test.dart +39 -0
  20. package/dart/test/typed_watch_registry_test.dart +69 -0
  21. package/dart/test/unshared_work_status_test.dart +269 -0
  22. package/dart_types/CHANGELOG.md +18 -0
  23. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +320 -85
  24. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +57 -1
  25. package/dart_types/lib/src/proto/nomos/runtime/v2/maintenance_desk.pb.dart +14 -0
  26. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +25 -1
  27. package/dart_types/lib/src/runtime_outcome.dart +17 -0
  28. package/dart_types/pubspec.yaml +1 -1
  29. package/package.json +5 -2
  30. package/src/codegen_ts.ts +7 -8
  31. package/src/compile_package_main.ts +11 -4
  32. package/src/reception_unit_artifact.ts +105 -0
  33. package/src/reception_unit_artifact_contract.ts +220 -0
  34. package/src/reception_unit_context.ts +47 -0
  35. package/src/reception_unit_source.ts +205 -0
@@ -1,10 +1,16 @@
1
- // One-shot reads wait out a copy that is not ready yet; nothing else waits.
2
- import 'dart:async';
1
+ // Reads answer at once from what is on this device; nothing waits for the network.
3
2
  import 'dart:io';
3
+ import 'dart:typed_data';
4
4
 
5
5
  import 'package:nomos_client/src/replica_readiness.dart';
6
+ import 'package:nomos_client/src/result_usda.dart' show NmsUsdaResult;
7
+ import 'package:nomos_client/src/sync_status_projection.dart'
8
+ show LocalCopyReadiness;
6
9
  import 'package:nomos_types/nomos_types.dart'
7
10
  show NomosUnavailable, NomosUnavailableReason;
11
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as protocol;
12
+ import 'package:nomos_types/src/proto/google/protobuf/any.pb.dart'
13
+ as google_protobuf;
8
14
  import 'package:test/test.dart';
9
15
 
10
16
  NomosUnavailable _unavailable(
@@ -15,135 +21,245 @@ NomosUnavailable _unavailable(
15
21
  reason: reason,
16
22
  workspace: 'catalogue-cars',
17
23
  retryable: retryable,
24
+ // A long hint: if anything still slept on it, the test would time out.
25
+ retryAfter: const Duration(minutes: 1),
18
26
  );
19
27
 
20
28
  void main() {
21
- setUp(() {
22
- nmsReplicaReadyFirstDelay = const Duration(milliseconds: 1);
23
- nmsReplicaReadyGiveUpAfter = const Duration(minutes: 10);
24
- });
25
-
26
- test('a copy still arriving is asked again until it answers', () async {
27
- var reads = 0;
28
- final rows = await nmsUntilReplicaReady(() async {
29
- reads++;
30
- if (reads == 1) {
31
- throw _unavailable(NomosUnavailableReason.workspaceOpening);
32
- }
33
- if (reads < 4) {
34
- throw _unavailable(NomosUnavailableReason.localReplicaNotReady);
35
- }
36
- return ['Hatchback', 'Saloon'];
37
- });
38
- expect(rows, ['Hatchback', 'Saloon']);
39
- expect(reads, 4);
40
- });
29
+ group('a one-shot read answers at once', () {
30
+ for (final (reason, readiness) in [
31
+ (
32
+ NomosUnavailableReason.localReplicaNotReady,
33
+ LocalCopyReadiness.arriving,
34
+ ),
35
+ (NomosUnavailableReason.workspaceOpening, LocalCopyReadiness.arriving),
36
+ (NomosUnavailableReason.readIndexRefreshing, LocalCopyReadiness.updating),
37
+ ]) {
38
+ test(
39
+ '${reason.name}: the empty answer and ${readiness.name}, asked once',
40
+ () async {
41
+ var reads = 0;
42
+ final reported = <LocalCopyReadiness>[];
43
+ final clock = Stopwatch()..start();
44
+ final rows = await nmsReadNow<List<String>>(
45
+ () async {
46
+ reads++;
47
+ throw _unavailable(reason);
48
+ },
49
+ pending: (_) => const [],
50
+ report: reported.add,
51
+ );
52
+ clock.stop();
53
+ expect(rows, isEmpty);
54
+ expect(reads, 1, reason: 'no retry');
55
+ expect(reported, [readiness]);
56
+ expect(clock.elapsedMilliseconds, lessThan(50));
57
+ },
58
+ );
59
+ }
41
60
 
42
- for (final (label, error) in [
43
- (
44
- 'a non-retryable unavailable answer',
45
- _unavailable(NomosUnavailableReason.localReplicaNotReady,
46
- retryable: false),
47
- ),
48
- (
49
- 'a remote-side unavailable answer',
50
- _unavailable(NomosUnavailableReason.remoteUnreachable),
51
- ),
52
- ('any other error', StateError('NMS-KERNEL-CALL: query failed')),
53
- ]) {
54
- test('$label is reported at once', () async {
55
- var reads = 0;
56
- await expectLater(
57
- nmsUntilReplicaReady<void>(() async {
58
- reads++;
59
- throw error;
60
- }),
61
- throwsA(same(error)),
61
+ test('a read that answers reports the copy ready', () async {
62
+ final reported = <LocalCopyReadiness>[];
63
+ final rows = await nmsReadNow<List<String>>(
64
+ () async => ['Hatchback'],
65
+ pending: (_) => const [],
66
+ report: reported.add,
62
67
  );
63
- expect(reads, 1);
68
+ expect(rows, ['Hatchback']);
69
+ expect(reported, [LocalCopyReadiness.ready]);
64
70
  });
65
- }
66
71
 
67
- test('a copy that never arrives still ends in an error', () async {
68
- nmsReplicaReadyGiveUpAfter = const Duration(milliseconds: 20);
69
- await expectLater(
70
- nmsUntilReplicaReady<void>(
71
- () async =>
72
- throw _unavailable(NomosUnavailableReason.localReplicaNotReady),
73
- ),
74
- throwsA(isA<NomosUnavailable>()),
72
+ test(
73
+ 'a read with no empty form reports the typed answer at once',
74
+ () async {
75
+ var reads = 0;
76
+ final reported = <LocalCopyReadiness>[];
77
+ final notReady = _unavailable(
78
+ NomosUnavailableReason.localReplicaNotReady,
79
+ );
80
+ await expectLater(
81
+ nmsReadNow<List<String>>(
82
+ () async {
83
+ reads++;
84
+ throw notReady;
85
+ },
86
+ pending: (_) => null,
87
+ report: reported.add,
88
+ ),
89
+ throwsA(same(notReady)),
90
+ );
91
+ expect(reads, 1);
92
+ expect(reported, [LocalCopyReadiness.arriving]);
93
+ },
75
94
  );
95
+
96
+ for (final (label, error) in [
97
+ (
98
+ 'a non-retryable unavailable answer',
99
+ _unavailable(
100
+ NomosUnavailableReason.localReplicaNotReady,
101
+ retryable: false,
102
+ ),
103
+ ),
104
+ (
105
+ 'a remote-side unavailable answer',
106
+ _unavailable(NomosUnavailableReason.remoteUnreachable),
107
+ ),
108
+ ('any other error', StateError('NMS-KERNEL-CALL: query failed')),
109
+ ]) {
110
+ test('$label is reported at once and is not "pending"', () async {
111
+ var reads = 0;
112
+ final reported = <LocalCopyReadiness>[];
113
+ await expectLater(
114
+ nmsReadNow<void>(
115
+ () async {
116
+ reads++;
117
+ throw error;
118
+ },
119
+ pending: (_) => fail('not a pending copy'),
120
+ report: reported.add,
121
+ ),
122
+ throwsA(same(error)),
123
+ );
124
+ expect(reads, 1);
125
+ expect(reported, isEmpty);
126
+ });
127
+ }
76
128
  });
77
129
 
78
- group('a completed sync wakes the wait', () {
79
- // The runtime asks for a long pause; only the sync event can end it early.
80
- NomosUnavailable notReady(String workspace) => NomosUnavailable(
81
- reason: NomosUnavailableReason.localReplicaNotReady,
82
- workspace: workspace,
83
- retryable: true,
84
- retryAfter: const Duration(minutes: 1),
130
+ group('the empty answer of a copy not readable here yet', () {
131
+ test(
132
+ 'a declared rows query answers its own generated result type with no rows',
133
+ () {
134
+ final empty = nmsPendingQueryResponse(
135
+ protocol.QueryRequest(
136
+ resultFormat: protocol
137
+ .QueryResultFormat.QUERY_RESULT_FORMAT_APPLICATION_PROTOBUF,
138
+ rows: protocol.RowsQuery(
139
+ id: 'listingsByCatalogueId',
140
+ applicationArguments: google_protobuf.Any(
141
+ typeUrl:
142
+ 'type.nomos.dev/nomos.operation.v1.Query_3407d8ac4c444f26',
143
+ value: [10, 1, 120],
144
+ ),
145
+ ),
146
+ ),
147
+ )!;
148
+ expect(
149
+ empty.applicationResult.typeUrl,
150
+ 'type.nomos.dev/nomos.operation.v1.QueryResult_3407d8ac4c444f26',
85
151
  );
152
+ expect(empty.applicationResult.value, isEmpty);
153
+ },
154
+ );
86
155
 
87
- test('the read asks again the moment its workspace syncs', () async {
88
- final synced = StreamController<String>.broadcast();
89
- final asked = Completer<void>();
90
- var reads = 0;
91
- final rows = nmsUntilReplicaReady(() async {
92
- reads++;
93
- if (!asked.isCompleted) asked.complete();
94
- if (reads == 1) throw notReady('estate-a');
95
- return ['Boiler'];
96
- }, synced: synced.stream);
97
- await asked.future;
98
- await pumpEventQueue();
99
- synced.add('estate-b');
100
- await pumpEventQueue();
101
- expect(reads, 1, reason: 'another workspace syncing is not this copy');
102
- synced.add('estate-a');
156
+ test('a page of the same query is empty too', () {
157
+ final empty = nmsPendingQueryResponse(
158
+ protocol.QueryRequest(
159
+ resultFormat: protocol
160
+ .QueryResultFormat.QUERY_RESULT_FORMAT_APPLICATION_PROTOBUF,
161
+ page: protocol.PageQuery(
162
+ id: 'listingsByCatalogueId',
163
+ limit: 100,
164
+ applicationArguments: google_protobuf.Any(
165
+ typeUrl: 'type.nomos.dev/nomos.operation.v1.Query_abc',
166
+ ),
167
+ ),
168
+ ),
169
+ )!;
103
170
  expect(
104
- await rows.timeout(const Duration(seconds: 1)),
105
- ['Boiler'],
171
+ empty.applicationResult.typeUrl,
172
+ 'type.nomos.dev/nomos.operation.v1.QueryResult_abc',
106
173
  );
107
- expect(reads, 2);
108
- await synced.close();
109
174
  });
110
175
 
111
- test('a sync that lands before the wait begins is not missed', () async {
112
- final synced = StreamController<String>.broadcast(sync: true);
113
- var reads = 0;
114
- final rows = await nmsUntilReplicaReady(() async {
115
- reads++;
116
- if (reads == 1) {
117
- // The copy becomes ready while the "not yet" answer is in flight.
118
- synced.add('estate-a');
119
- throw notReady('estate-a');
176
+ test(
177
+ 'nominal by-id, by-type and spatial reads answer a result with no rows',
178
+ () {
179
+ for (final query in [
180
+ protocol.QueryRequest(
181
+ byId: protocol.ByIdQuery(aggregateType: 'a', aggregateId: 'b'),
182
+ ),
183
+ protocol.QueryRequest(
184
+ byType: protocol.ByTypeQuery(aggregateType: 'a'),
185
+ ),
186
+ protocol.QueryRequest(spatial: protocol.SpatialQuery()),
187
+ ]) {
188
+ query.resultFormat =
189
+ protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA;
190
+ final empty = nmsPendingQueryResponse(query)!;
191
+ final parsed = NmsUsdaResult.parse(
192
+ Uint8List.fromList(empty.resultUsda),
193
+ );
194
+ expect(parsed.rows, isEmpty, reason: query.whichQuery().name);
195
+ expect(parsed.done, isTrue);
120
196
  }
121
- return ['Boiler'];
122
- }, synced: synced.stream)
123
- .timeout(const Duration(seconds: 1));
124
- expect(rows, ['Boiler']);
125
- await synced.close();
197
+ },
198
+ );
199
+
200
+ test('a count and a sum answer an exact zero', () {
201
+ for (final query in [
202
+ protocol.QueryRequest(count: protocol.TallyQuery(id: 'assets')),
203
+ protocol.QueryRequest(sum: protocol.TallyQuery(id: 'wattage')),
204
+ ]) {
205
+ final empty = nmsPendingQueryResponse(query)!;
206
+ expect(empty.hasIntegerValue(), isTrue);
207
+ expect(empty.integerValue.toInt(), 0);
208
+ final changed = nmsPendingWatchChanged(query, 'estate:watch:1')!;
209
+ expect(changed.hasIntegerValue(), isTrue);
210
+ expect(changed.subscriptionId, 'estate:watch:1');
211
+ }
212
+ });
213
+
214
+ test('a framework read has no empty form', () {
215
+ expect(
216
+ nmsPendingQueryResponse(
217
+ protocol.QueryRequest(
218
+ authenticationPosture: protocol.AuthenticationPostureQuery(),
219
+ ),
220
+ ),
221
+ isNull,
222
+ );
223
+ expect(
224
+ nmsPendingQueryResponse(
225
+ protocol.QueryRequest(
226
+ resultFormat: protocol
227
+ .QueryResultFormat.QUERY_RESULT_FORMAT_APPLICATION_PROTOBUF,
228
+ rows: protocol.RowsQuery(
229
+ applicationArguments: google_protobuf.Any(
230
+ typeUrl: 'type.nomos.dev/untyped',
231
+ ),
232
+ ),
233
+ ),
234
+ ),
235
+ isNull,
236
+ reason: 'only a generated Query_<sid> names its result type',
237
+ );
126
238
  });
127
239
  });
128
240
 
129
- // The guard: every one-shot read reaches the kernel through one of these two
130
- // request shapes. Each must sit inside nmsUntilReplicaReady, so a new read
131
- // path cannot hand callers a "not ready yet" as an error. A site is where the
132
- // request is BUILT (its requestId, or the historic request), never a trace line
133
- // that merely names the same logical id (CAP-741's `call.send`), and "inside"
134
- // means the readiness wait opens between the site's enclosing member and the
135
- // site, however much tracing the closure carries.
136
- test('every kernel query request is built inside nmsUntilReplicaReady', () {
137
- final source = File(
138
- 'lib/src/reception/dart_reception.dart',
139
- ).readAsLinesSync();
241
+ // The guard: every one-shot read reaches the kernel through one of these two request
242
+ // shapes. Each must sit inside nmsReadNow, so a new read path can neither wait for a copy
243
+ // nor hand callers a "not ready yet" as an error. A site is where the request is BUILT (its
244
+ // requestId, or the historic request), never a trace line that merely names the same logical
245
+ // id (CAP-741's `call.send`), and "inside" means the read opens between the site's enclosing
246
+ // member and the site, however much tracing the closure carries.
247
+ test('every kernel query request is built inside nmsReadNow', () {
248
+ final source =
249
+ File('lib/src/reception/dart_reception.dart').readAsLinesSync();
250
+ expect(
251
+ source.any((line) => line.contains('nmsUntilReplicaReady')),
252
+ isFalse,
253
+ reason: 'reads never wait for a copy to arrive',
254
+ );
140
255
  final member = RegExp(r'^ [A-Za-z_]');
141
256
  final requestSites = <int>[];
142
257
  for (var i = 0; i < source.length; i++) {
143
258
  final line = source[i];
144
259
  if ((line.contains('requestId:') && line.contains(r":query:$")) ||
145
260
  line.contains(
146
- 'historicQuery: nomos_protocol.HistoricQueryRequest(')) {
261
+ 'historicQuery: nomos_protocol.HistoricQueryRequest(',
262
+ )) {
147
263
  requestSites.add(i);
148
264
  }
149
265
  }
@@ -154,12 +270,10 @@ void main() {
154
270
  start--;
155
271
  }
156
272
  expect(
157
- source
158
- .sublist(start, site)
159
- .any((line) => line.contains('nmsUntilReplicaReady(')),
273
+ source.sublist(start, site).any((line) => line.contains('nmsReadNow')),
160
274
  isTrue,
161
275
  reason: 'dart_reception.dart:${site + 1} sends a kernel query outside '
162
- 'nmsUntilReplicaReady',
276
+ 'nmsReadNow',
163
277
  );
164
278
  }
165
279
  });
@@ -358,6 +358,45 @@ void main() {
358
358
  reason: 'a completed round never forgets the link state');
359
359
  });
360
360
 
361
+ test(
362
+ 'while every workspace stays live, re-folding emits no new application status (CAP-930)',
363
+ () async {
364
+ final projection = NmsSyncStatusProjection();
365
+ addTearDown(projection.close);
366
+ final settled = reduce(protocol.SyncWorkspaceResponse(
367
+ previousHead: 'head', currentHead: 'head'));
368
+ final emitted = <SyncStatus>[];
369
+ final subscription = projection.applicationStream.listen(emitted.add);
370
+ addTearDown(subscription.cancel);
371
+ projection.pushSession('estate', settled.copyWith(live: true));
372
+ projection.pushSession('home', settled.copyWith(live: true));
373
+ projection.foldApplication(['estate', 'home']);
374
+ await Future<void>.delayed(Duration.zero);
375
+ expect(projection.applicationStatus.live, isTrue);
376
+ final becameLive = projection.applicationStatus.lastSyncedAt;
377
+ final before = emitted.length;
378
+ // Every incoming change re-reports the workspaces and re-folds the summary.
379
+ for (var i = 0; i < 20; i++) {
380
+ await Future<void>.delayed(const Duration(microseconds: 50));
381
+ projection.pushSession('estate', settled.copyWith(live: true));
382
+ projection.foldApplication(['estate', 'home']);
383
+ }
384
+ await Future<void>.delayed(Duration.zero);
385
+ expect(emitted.length, before,
386
+ reason: 'an unchanged live status is not a new status');
387
+ expect(projection.applicationStatus.lastSyncedAt, becameLive);
388
+ // Losing the link and regaining it is a new "current now".
389
+ projection.pushSession('estate', settled);
390
+ projection.foldApplication(['estate', 'home']);
391
+ expect(projection.applicationStatus.live, isFalse);
392
+ await Future<void>.delayed(const Duration(milliseconds: 2));
393
+ projection.pushSession('estate', settled.copyWith(live: true));
394
+ projection.foldApplication(['estate', 'home']);
395
+ expect(projection.applicationStatus.live, isTrue);
396
+ expect(projection.applicationStatus.lastSyncedAt!.isAfter(becameLive!),
397
+ isTrue);
398
+ });
399
+
361
400
  test(
362
401
  'the runtime follow inventory survives every re-fold of the application status',
363
402
  () async {
@@ -1,4 +1,6 @@
1
1
  import 'dart:async';
2
+ import 'package:nomos_client/src/sync_status_projection.dart'
3
+ show LocalCopyReadiness;
2
4
  import 'package:nomos_client/src/typed_watch_registry.dart';
3
5
  import 'package:nomos_types/nomos_kernel_protocol.dart' as protocol;
4
6
  import 'package:nomos_types/nomos_types.dart'
@@ -246,4 +248,71 @@ void main() {
246
248
  );
247
249
  await expectLater(watch.stream.first, throwsA(isA<NomosUnavailable>()));
248
250
  });
251
+
252
+ test(
253
+ 'a copy not here yet: the empty answer at once, the copy reported arriving, never a wait',
254
+ () async {
255
+ final registry = NmsTypedWatchRegistry();
256
+ final reported = <LocalCopyReadiness>[];
257
+ final watch = registry.open(
258
+ session: 'catalogue',
259
+ decode: (event) => event.resultUsda,
260
+ pending: (id) => protocol.WatchChanged(subscriptionId: id),
261
+ report: reported.add,
262
+ start: (_) async => throw const NomosUnavailable(
263
+ reason: NomosUnavailableReason.localReplicaNotReady,
264
+ workspace: 'catalogue',
265
+ retryable: true,
266
+ retryAfter: Duration(minutes: 1),
267
+ ),
268
+ stop: (_) async {},
269
+ );
270
+ final clock = Stopwatch()..start();
271
+ expect(await watch.stream.first, isEmpty);
272
+ expect(clock.elapsedMilliseconds, lessThan(50));
273
+ expect(reported, [LocalCopyReadiness.arriving]);
274
+ await registry.close();
275
+ },
276
+ );
277
+
278
+ test(
279
+ 'a registration the peer finished after answering "not ready" ends the re-asks',
280
+ () async {
281
+ final registry = NmsTypedWatchRegistry();
282
+ var starts = 0;
283
+ final reported = <LocalCopyReadiness>[];
284
+ late final String subscription;
285
+ final watch = registry.open(
286
+ session: 'catalogue',
287
+ decode: (event) => event.resultUsda,
288
+ pending: (id) => protocol.WatchChanged(subscriptionId: id),
289
+ report: reported.add,
290
+ start: (id) async {
291
+ subscription = id;
292
+ starts++;
293
+ throw const NomosUnavailable(
294
+ reason: NomosUnavailableReason.localReplicaNotReady,
295
+ workspace: 'catalogue',
296
+ retryable: true,
297
+ retryAfter: Duration(milliseconds: 20),
298
+ );
299
+ },
300
+ stop: (_) async {},
301
+ );
302
+ final values = StreamIterator(watch.stream);
303
+ expect(await values.moveNext(), isTrue);
304
+ expect(values.current, isEmpty);
305
+ // The peer completed the registration in the background and delivers its first result.
306
+ registry.deliver(
307
+ protocol.WatchChanged(subscriptionId: subscription, resultUsda: [7]),
308
+ );
309
+ expect(await values.moveNext(), isTrue);
310
+ expect(values.current, [7]);
311
+ await Future<void>.delayed(const Duration(milliseconds: 100));
312
+ expect(starts, 1, reason: 'a delivering watch is registered');
313
+ expect(reported, [LocalCopyReadiness.arriving, LocalCopyReadiness.ready]);
314
+ await values.cancel();
315
+ await registry.close();
316
+ },
317
+ );
249
318
  }