@githolon/dsl 0.117.0 → 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 (36) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/compile_proof.mjs +16 -0
  3. package/dart/CHANGELOG.md +45 -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 +6 -2
  30. package/src/codegen_ts.ts +7 -8
  31. package/src/compile_package_main.ts +11 -4
  32. package/src/reception_template.ts +4 -3
  33. package/src/reception_unit_artifact.ts +105 -0
  34. package/src/reception_unit_artifact_contract.ts +220 -0
  35. package/src/reception_unit_context.ts +47 -0
  36. package/src/reception_unit_source.ts +205 -0
@@ -1,82 +1,220 @@
1
- /// One-shot reads wait out a local copy that is not ready yet.
1
+ /// Reads answer at once from what is on this device. They never wait for the network.
2
2
  ///
3
- /// A workspace this peer is still fetching, refetching, opening or indexing
4
- /// answers a read with a retryable [NomosUnavailable] whose reason is
5
- /// `localReplicaNotReady` or `workspaceOpening`. That is "not yet", not a
6
- /// failure. [NmsTypedWatchRegistry] already waits it out before registering a
7
- /// watch; this is the same rule for every one-shot read (queries, pages, counts,
8
- /// sums, by-id and by-type USDA, historic queries), applied once where they all
9
- /// reach the kernel. Callers therefore see either an answer or a real error.
10
- /// They never see a state that passes by itself a moment later.
3
+ /// Jack's rule (11 Oct 2026): sync is a background effect, and every read answers immediately
4
+ /// from the device. A workspace this peer has never fetched, is fetching or refetching with
5
+ /// nothing servable yet, or is still opening, answers a read with a retryable
6
+ /// [NomosUnavailable] whose reason is `localReplicaNotReady` or `workspaceOpening`; a copy whose
7
+ /// read index is being rebuilt under a law its head adopted (and which this application cannot
8
+ /// decode) answers `readIndexRefreshing`. None of these is a failure, and none is waited out
9
+ /// here any more. An application data read (rows, a page, by-id or by-type rows, a count or a
10
+ /// sum) answers EMPTY at once, and the session's [SyncStatus.localCopy] says the copy is
11
+ /// [LocalCopyReadiness.arriving] or [LocalCopyReadiness.updating], so a presentation shows what it
12
+ /// has beside "downloading". A watch emits that empty result at once and emits again when the copy
13
+ /// arrives ([NmsTypedWatchRegistry]). A framework read with no empty form (child workspaces,
14
+ /// authentication posture, …) reports the typed [NomosUnavailable] at once.
15
+ ///
16
+ /// The download itself is the peer's work: the answer that a copy is not ready marks that
17
+ /// workspace urgent in the peer's background queue (NativeBackgroundRouter.applicationWaitsOn),
18
+ /// and a watch asking again keeps it urgent while someone is looking.
19
+ ///
20
+ /// Before this rule `nmsUntilReplicaReady` re-asked every such read for up to ten minutes. On
21
+ /// Jack's iPhone (TestFlight 1334, 11 Oct 2026 10:07 UTC) the Lippold catalogue panel's read of a
22
+ /// never-fetched data profile was held 11 s, until its download finished, behind a full-panel
23
+ /// spinner.
11
24
  @internal
12
25
  library;
13
26
 
14
27
  import 'dart:async';
28
+ import 'dart:convert' show utf8;
15
29
 
30
+ import 'package:fixnum/fixnum.dart' show Int64;
16
31
  import 'package:meta/meta.dart' show internal, visibleForTesting;
32
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
17
33
  import 'package:nomos_types/nomos_types.dart'
18
34
  show NomosUnavailable, NomosUnavailableReason;
35
+ import 'package:nomos_types/src/proto/google/protobuf/any.pb.dart'
36
+ as google_protobuf;
19
37
 
20
- /// How long one read keeps asking before it reports the copy as unavailable.
21
- /// A first sync of a large workspace at cutover takes minutes, not hours.
22
- @visibleForTesting
23
- Duration nmsReplicaReadyGiveUpAfter = const Duration(minutes: 10);
38
+ import 'sync_status_projection.dart'
39
+ show LocalCopyReadiness, SyncStatus, nmsSaysCopyDamaged;
24
40
 
25
- /// First pause between asks when the runtime gives no `retryAfter`. It doubles
26
- /// per attempt, up to 16 times this value (the watch registry's schedule).
41
+ /// First pause between a watch's registration attempts when the runtime gives no `retryAfter`.
42
+ /// It doubles per attempt, up to 16 times this value. Only a watch asks again: by then it has
43
+ /// already emitted what is on the device, so nobody waits on these asks.
27
44
  @visibleForTesting
28
45
  Duration nmsReplicaReadyFirstDelay = const Duration(milliseconds: 250);
29
46
 
30
- /// True for an unavailable answer that means "this copy is not ready yet".
47
+ /// True for an unavailable answer that means "this copy is not readable here yet": still
48
+ /// fetching or opening, or its read index being rebuilt under a law its head has just adopted
49
+ /// (CAP-900: that answer reached the app as an error on Jack's phone).
31
50
  bool nmsIsReplicaNotReady(Object error) =>
32
51
  error is NomosUnavailable &&
33
52
  error.retryable &&
34
53
  (error.reason == NomosUnavailableReason.localReplicaNotReady ||
35
- error.reason == NomosUnavailableReason.workspaceOpening);
54
+ error.reason == NomosUnavailableReason.workspaceOpening ||
55
+ error.reason == NomosUnavailableReason.readIndexRefreshing ||
56
+ error.reason == NomosUnavailableReason.firstDownload);
36
57
 
37
- /// Runs [read], asking again while it answers [nmsIsReplicaNotReady].
38
- ///
39
- /// [synced] names each workspace whose sync round just completed; the office
40
- /// emits it the moment a copy it was fetching or opening is ready. A read
41
- /// waiting on that workspace asks again at once instead of sleeping out its
42
- /// `retryAfter`, which stays only as the fallback for a copy that becomes ready
43
- /// without a sync (an index built in the background).
58
+ /// The [LocalCopyReadiness] a "not readable here yet" answer reports. A copy that is HERE but
59
+ /// damaged (`localReplicaNotReady` whose detail says its local Git history is incomplete, it cannot
60
+ /// be read, or it is being refetched) is [LocalCopyReadiness.repairing], never "arriving": on
61
+ /// Jack's iPad (TestFlight 1338, 11 Oct 2026) such an estate read as arriving while its rounds
62
+ /// "completed", and the app said every workspace was in sync while every edit failed.
63
+ @internal
64
+ LocalCopyReadiness nmsLocalCopyReadiness(NomosUnavailable unavailable) =>
65
+ switch (unavailable.reason) {
66
+ NomosUnavailableReason.readIndexRefreshing => LocalCopyReadiness.updating,
67
+ NomosUnavailableReason.firstDownload => LocalCopyReadiness.firstDownload,
68
+ NomosUnavailableReason.localReplicaNotReady
69
+ when nmsSaysCopyDamaged(unavailable.detail) =>
70
+ LocalCopyReadiness.repairing,
71
+ _ => LocalCopyReadiness.arriving,
72
+ };
73
+
74
+ /// Where a read reports what it found of its workspace's local copy: the reception records it on
75
+ /// the session's [SyncStatus.localCopy].
76
+ @internal
77
+ typedef NmsLocalCopyReport = void Function(LocalCopyReadiness readiness);
78
+
79
+ /// Runs [read] ONCE and answers at once.
44
80
  ///
45
- /// Any other error, including a non-retryable or remote-side unavailability,
46
- /// is rethrown at once, as is the last not-ready answer after
47
- /// [nmsReplicaReadyGiveUpAfter].
81
+ /// A "not readable here yet" answer ([nmsIsReplicaNotReady]) is reported to [report] and turned
82
+ /// into [pending]'s empty answer; when [pending] has none (returns null) the typed
83
+ /// [NomosUnavailable] is rethrown at once. An answer reports [LocalCopyReadiness.ready]. Any other
84
+ /// error is rethrown at once and reports nothing. There is no retry, sleep or wait here.
48
85
  @internal
49
- Future<T> nmsUntilReplicaReady<T>(
86
+ Future<T> nmsReadNow<T>(
50
87
  Future<T> Function() read, {
51
- Stream<String>? synced,
88
+ required T? Function(NomosUnavailable unavailable) pending,
89
+ NmsLocalCopyReport? report,
52
90
  }) async {
53
- final waited = Stopwatch()..start();
54
- final wake = NmsReplicaWake(synced);
91
+ final T answer;
55
92
  try {
56
- for (var attempt = 0;; attempt++) {
57
- wake.asking();
58
- try {
59
- return await read();
60
- } on NomosUnavailable catch (unavailable) {
61
- if (!nmsIsReplicaNotReady(unavailable) ||
62
- waited.elapsed >= nmsReplicaReadyGiveUpAfter) {
63
- rethrow;
64
- }
65
- await wake.wait(unavailable, attempt);
93
+ answer = await read();
94
+ } on NomosUnavailable catch (unavailable) {
95
+ if (!nmsIsReplicaNotReady(unavailable)) {
96
+ // A damaged copy no refetch is under way for still says so on its status.
97
+ if (unavailable.reason == NomosUnavailableReason.localReplicaNotReady &&
98
+ nmsSaysCopyDamaged(unavailable.detail)) {
99
+ report?.call(LocalCopyReadiness.repairing);
66
100
  }
101
+ rethrow;
102
+ }
103
+ report?.call(nmsLocalCopyReadiness(unavailable));
104
+ final empty = pending(unavailable);
105
+ if (empty == null) rethrow;
106
+ return empty;
107
+ }
108
+ report?.call(LocalCopyReadiness.ready);
109
+ return answer;
110
+ }
111
+
112
+ /// The nominal USDA of a read result with no rows: what an application data read answers while
113
+ /// its copy is not readable here yet.
114
+ @internal
115
+ final List<int> nmsEmptyReadResultUsda = utf8.encode('''#usda 1.0
116
+ (
117
+ defaultPrim = "Result"
118
+ )
119
+
120
+ def NomosReadResult "Result"
121
+ {
122
+ custom uniform bool nomos:done = true
123
+
124
+ def NomosListValue "Rows"
125
+ {
67
126
  }
68
- } finally {
69
- await wake.close();
127
+ }
128
+ ''');
129
+
130
+ const String _queryTypePrefix = 'type.nomos.dev/nomos.operation.v1.Query_';
131
+ const String _queryResultTypePrefix =
132
+ 'type.nomos.dev/nomos.operation.v1.QueryResult_';
133
+
134
+ /// The generated result type of a declared application query: `Query_<sid>` arguments answer
135
+ /// `QueryResult_<sid>` (codegen_query_proto.ts / codegen_result_proto.ts). Null when [arguments]
136
+ /// is not a generated query's arguments.
137
+ String? _resultTypeOf(google_protobuf.Any arguments) {
138
+ final type = arguments.typeUrl;
139
+ if (!type.startsWith(_queryTypePrefix)) return null;
140
+ final sid = type.substring(_queryTypePrefix.length);
141
+ return sid.isEmpty ? null : '$_queryResultTypePrefix$sid';
142
+ }
143
+
144
+ /// The empty answer of an application data read whose copy is not readable here yet, in the
145
+ /// result arm and format [query] asked for: no rows (the generated `QueryResult_<sid>` with no
146
+ /// rows, or a nominal USDA result with none), or a zero tally. Null for a query with no empty
147
+ /// form: a framework read, or a format this read does not answer in.
148
+ @internal
149
+ nomos_protocol.QueryResponse? nmsPendingQueryResponse(
150
+ nomos_protocol.QueryRequest query,
151
+ ) {
152
+ final format = query.resultFormat;
153
+ switch (query.whichQuery()) {
154
+ case nomos_protocol.QueryRequest_Query.rows:
155
+ case nomos_protocol.QueryRequest_Query.page:
156
+ if (format ==
157
+ nomos_protocol
158
+ .QueryResultFormat.QUERY_RESULT_FORMAT_APPLICATION_PROTOBUF) {
159
+ final resultType = _resultTypeOf(
160
+ query.hasRows()
161
+ ? query.rows.applicationArguments
162
+ : query.page.applicationArguments,
163
+ );
164
+ if (resultType == null) return null;
165
+ return nomos_protocol.QueryResponse(
166
+ applicationResult: google_protobuf.Any(typeUrl: resultType),
167
+ );
168
+ }
169
+ return _emptyUsda(format);
170
+ case nomos_protocol.QueryRequest_Query.byId:
171
+ case nomos_protocol.QueryRequest_Query.byType:
172
+ case nomos_protocol.QueryRequest_Query.spatial:
173
+ return _emptyUsda(format);
174
+ case nomos_protocol.QueryRequest_Query.count:
175
+ case nomos_protocol.QueryRequest_Query.sum:
176
+ return nomos_protocol.QueryResponse(integerValue: Int64.ZERO);
177
+ default:
178
+ return null;
70
179
  }
71
180
  }
72
181
 
73
- /// The pause after a "not ready yet" answer: the runtime's `retryAfter` (or
74
- /// [nmsReplicaReadyFirstDelay]), doubling per attempt up to 16 times, cut short
75
- /// the moment the workspace's sync completes.
182
+ nomos_protocol.QueryResponse? _emptyUsda(
183
+ nomos_protocol.QueryResultFormat format,
184
+ ) =>
185
+ format == nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA
186
+ ? nomos_protocol.QueryResponse(resultUsda: nmsEmptyReadResultUsda)
187
+ : null;
188
+
189
+ /// The first emission of a watch on [query] whose copy is not readable here yet: the same empty
190
+ /// answer as [nmsPendingQueryResponse], as the watch event its decoder reads. Null when the query
191
+ /// has no empty form.
192
+ @internal
193
+ nomos_protocol.WatchChanged? nmsPendingWatchChanged(
194
+ nomos_protocol.QueryRequest query,
195
+ String subscriptionId,
196
+ ) {
197
+ final empty = nmsPendingQueryResponse(query);
198
+ if (empty == null) return null;
199
+ final changed = nomos_protocol.WatchChanged(subscriptionId: subscriptionId);
200
+ if (empty.hasApplicationResult()) {
201
+ changed.applicationResult = empty.applicationResult;
202
+ } else if (empty.hasResultUsda()) {
203
+ changed.resultUsda = empty.resultUsda;
204
+ } else if (empty.hasIntegerValue()) {
205
+ changed.integerValue = empty.integerValue;
206
+ }
207
+ return changed;
208
+ }
209
+
210
+ /// The pause before a watch asks again after a "not ready yet" answer: the runtime's
211
+ /// `retryAfter` (or [nmsReplicaReadyFirstDelay]), doubling per attempt up to 16 times, cut short
212
+ /// the moment the workspace's sync completes. The watch has already emitted what is on the device,
213
+ /// so this pause holds nobody up; it only paces the re-registration.
76
214
  ///
77
- /// It listens from construction, so a sync that completes between the ask and
78
- /// the wait is not missed: [asking] marks the start of each ask, and a wait for
79
- /// a workspace that synced since then returns at once.
215
+ /// It listens from construction, so a sync that completes between the ask and the wait is not
216
+ /// missed: [asking] marks the start of each ask, and a wait for a workspace that synced since then
217
+ /// returns at once.
80
218
  @internal
81
219
  final class NmsReplicaWake {
82
220
  NmsReplicaWake(Stream<String>? synced) {
@@ -97,9 +235,15 @@ final class NmsReplicaWake {
97
235
  return;
98
236
  }
99
237
  final base = unavailable.retryAfter ?? nmsReplicaReadyFirstDelay;
238
+ // A rebuild under a new law finishes without any sync to wake this wait, so its
239
+ // re-ask stays near the runtime's retryAfter (at most twice it), never 16 times it.
240
+ final growth =
241
+ unavailable.reason == NomosUnavailableReason.readIndexRefreshing
242
+ ? (attempt < 1 ? attempt : 1)
243
+ : (attempt < 4 ? attempt : 4);
100
244
  final woken = _woken = Completer<void>();
101
245
  _waitingOn = workspace;
102
- final timer = Timer(base * (1 << (attempt < 4 ? attempt : 4)), () {
246
+ final timer = Timer(base * (1 << growth), () {
103
247
  if (!woken.isCompleted) woken.complete();
104
248
  });
105
249
  try {
@@ -111,6 +255,12 @@ final class NmsReplicaWake {
111
255
  }
112
256
  }
113
257
 
258
+ /// Ends a wait in progress now (the watch it paces was cancelled or already delivers).
259
+ void wakeNow() {
260
+ final woken = _woken;
261
+ if (woken != null && !woken.isCompleted) woken.complete();
262
+ }
263
+
114
264
  void _synced(String workspace) {
115
265
  _syncedSinceAsk.add(workspace);
116
266
  final woken = _woken;