@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.
- package/assets/operation-contract.wasm +0 -0
- package/compile_proof.mjs +16 -0
- package/dart/CHANGELOG.md +45 -0
- package/dart/lib/src/application_status.dart +58 -2
- package/dart/lib/src/peer_application_streams.dart +7 -1
- package/dart/lib/src/reception/dart_reception.dart +92 -12
- package/dart/lib/src/reception/reception_session.dart +10 -2
- package/dart/lib/src/replica_readiness.dart +200 -50
- package/dart/lib/src/runner/node-entry.mjs +2480 -1021
- package/dart/lib/src/sync_status_projection.dart +346 -4
- package/dart/lib/src/sync_status_reducer.dart +44 -0
- package/dart/lib/src/test_harness.dart +1 -0
- package/dart/lib/src/typed_watch_registry.dart +67 -7
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/copy_repair_status_test.dart +238 -0
- package/dart/test/reads_never_wait_test.dart +387 -0
- package/dart/test/replica_readiness_test.dart +223 -109
- package/dart/test/sync_status_reducer_test.dart +39 -0
- package/dart/test/typed_watch_registry_test.dart +69 -0
- package/dart/test/unshared_work_status_test.dart +269 -0
- package/dart_types/CHANGELOG.md +18 -0
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +320 -85
- package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +57 -1
- package/dart_types/lib/src/proto/nomos/runtime/v2/maintenance_desk.pb.dart +14 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +25 -1
- package/dart_types/lib/src/runtime_outcome.dart +17 -0
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +6 -2
- package/src/codegen_ts.ts +7 -8
- package/src/compile_package_main.ts +11 -4
- package/src/reception_template.ts +4 -3
- package/src/reception_unit_artifact.ts +105 -0
- package/src/reception_unit_artifact_contract.ts +220 -0
- package/src/reception_unit_context.ts +47 -0
- package/src/reception_unit_source.ts +205 -0
|
@@ -1,82 +1,220 @@
|
|
|
1
|
-
///
|
|
1
|
+
/// Reads answer at once from what is on this device. They never wait for the network.
|
|
2
2
|
///
|
|
3
|
-
///
|
|
4
|
-
///
|
|
5
|
-
///
|
|
6
|
-
///
|
|
7
|
-
///
|
|
8
|
-
///
|
|
9
|
-
///
|
|
10
|
-
///
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
26
|
-
/// per attempt, up to 16 times this value
|
|
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
|
|
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
|
-
///
|
|
38
|
-
///
|
|
39
|
-
///
|
|
40
|
-
///
|
|
41
|
-
///
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
///
|
|
46
|
-
///
|
|
47
|
-
/// [
|
|
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>
|
|
86
|
+
Future<T> nmsReadNow<T>(
|
|
50
87
|
Future<T> Function() read, {
|
|
51
|
-
|
|
88
|
+
required T? Function(NomosUnavailable unavailable) pending,
|
|
89
|
+
NmsLocalCopyReport? report,
|
|
52
90
|
}) async {
|
|
53
|
-
final
|
|
54
|
-
final wake = NmsReplicaWake(synced);
|
|
91
|
+
final T answer;
|
|
55
92
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
///
|
|
79
|
-
///
|
|
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 <<
|
|
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;
|