@githolon/dsl 0.110.0 → 0.110.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.
Binary file
package/dart/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.66.6
2
+
3
+ - Let reception close promptly when presentation stream listeners are paused.
4
+
1
5
  ## 0.66.5
2
6
 
3
7
  - Resolve the Node runtime runner through `@githolon/runtime`, the public name for the application runtime package.
@@ -194,17 +194,20 @@ final class ReceptionEventDesk {
194
194
 
195
195
  Future<void> close() async {
196
196
  await _syncStatusChanges.cancel();
197
- if (!_copyChanges.isClosed) await _copyChanges.close();
197
+ // Public broadcast streams can have paused UI listeners. Their close()
198
+ // futures wait for those listeners to resume, so signal done without
199
+ // letting an external subscriber hold reception teardown open.
200
+ if (!_copyChanges.isClosed) unawaited(_copyChanges.close());
198
201
  await typedWatches.close();
199
- if (!_sync.isClosed) await _sync.close();
200
- if (!_custody.isClosed) await _custody.close();
201
- if (!_localWorkRejected.isClosed) await _localWorkRejected.close();
202
+ if (!_sync.isClosed) unawaited(_sync.close());
203
+ if (!_custody.isClosed) unawaited(_custody.close());
204
+ if (!_localWorkRejected.isClosed) unawaited(_localWorkRejected.close());
202
205
  if (!_receivingGroupConvergence.isClosed) {
203
- await _receivingGroupConvergence.close();
206
+ unawaited(_receivingGroupConvergence.close());
204
207
  }
205
- if (!_modelAdoption.isClosed) await _modelAdoption.close();
206
- if (!_preserved.isClosed) await _preserved.close();
207
- if (!_foreignPlacements.isClosed) await _foreignPlacements.close();
208
+ if (!_modelAdoption.isClosed) unawaited(_modelAdoption.close());
209
+ if (!_preserved.isClosed) unawaited(_preserved.close());
210
+ if (!_foreignPlacements.isClosed) unawaited(_foreignPlacements.close());
208
211
  await progress.close();
209
212
  await syncStatuses.close();
210
213
  }
@@ -260,8 +260,9 @@ final class ReceptionProgressDesk {
260
260
  }
261
261
 
262
262
  Future<void> close() async {
263
- if (!_spans.isClosed) await _spans.close();
264
- if (!_transfers.isClosed) await _transfers.close();
265
- if (!_kernel.isClosed) await _kernel.close();
263
+ // Observers may be paused while their widget tree is being replaced.
264
+ if (!_spans.isClosed) unawaited(_spans.close());
265
+ if (!_transfers.isClosed) unawaited(_transfers.close());
266
+ if (!_kernel.isClosed) unawaited(_kernel.close());
266
267
  }
267
268
  }
@@ -21835,6 +21835,15 @@ var ReplicaFollowController = class {
21835
21835
  }
21836
21836
  if (result.stillAhead || result.pushFailed || result.directOfferDeferred || result.reconnectRequired) {
21837
21837
  if (result.directOfferDeferred) {
21838
+ if (!result.reconnectRequired) {
21839
+ this.#desk.transition(result.workspace, {
21840
+ case: ReplicaFollowEventKind.Failed,
21841
+ nowUnixMillis,
21842
+ failure: "remote workspace opening; retrying",
21843
+ retryWithinMillis: 5e3
21844
+ });
21845
+ return delayUntilDue(this.#desk.get(result.workspace), nowUnixMillis);
21846
+ }
21838
21847
  this.#desk.transition(result.workspace, {
21839
21848
  case: ReplicaFollowEventKind.DependencyDeferred,
21840
21849
  nowUnixMillis,
@@ -23740,6 +23749,34 @@ function resolveCustodyDisagreement(kernel, mount, requestId, policy, verdict) {
23740
23749
  init_define_NOMOS_KERNEL_RELEASE();
23741
23750
  var CUSTODY_OFFER_MEDIA_TYPE = "application/vnd.nomos.custody-offer-batch+protobuf";
23742
23751
  var CUSTODY_OFFER_RESULT_MEDIA_TYPE = "application/vnd.nomos.custody-offer-result+protobuf";
23752
+ function settleCustodyOfferExchange(awaiting, response) {
23753
+ if (response.status === 202 && response.contentType?.includes("application/json")) {
23754
+ try {
23755
+ const pending = JSON.parse(new TextDecoder().decode(response.body));
23756
+ if (pending?.retryable === true && typeof pending.preparing === "string") {
23757
+ return { case: "maintenance", kind: pending.preparing };
23758
+ }
23759
+ } catch {
23760
+ }
23761
+ }
23762
+ if (!response.contentType?.includes(CUSTODY_OFFER_RESULT_MEDIA_TYPE)) {
23763
+ return {
23764
+ case: "unexpected",
23765
+ status: response.status,
23766
+ ...response.contentType === void 0 ? {} : { contentType: response.contentType }
23767
+ };
23768
+ }
23769
+ const verdict = decodeCustodyOffer(response.body, awaiting.requestId);
23770
+ const receipt = custodyOfferReceipt(awaiting.requestId, verdict);
23771
+ switch (verdict.case) {
23772
+ case "admission":
23773
+ return { case: "admission", value: verdict.value, receipt, status: response.status };
23774
+ case "failure":
23775
+ return { case: "failure", value: verdict.value, receipt, status: response.status };
23776
+ case "blocked":
23777
+ return { case: "blocked", value: verdict.value, receipt, status: response.status };
23778
+ }
23779
+ }
23743
23780
  function encodeInlineCustodyOffer(args) {
23744
23781
  return toBinary(CustodyOfferBatchRequestSchema, create(CustodyOfferBatchRequestSchema, {
23745
23782
  protocolVersion: 2,
@@ -23866,6 +23903,7 @@ function summarizeCustodyAdmission(admission) {
23866
23903
  var PLACEMENT_REF3 = "refs/nomos/placement";
23867
23904
  var LAW_PRODUCTS_MEDIA_TYPE = "application/vnd.nomos.export-law-products-response+protobuf";
23868
23905
  var WorkspaceSyncPhase = Object.freeze({
23906
+ Idle: "idle",
23869
23907
  Probe: "probe",
23870
23908
  Offer: "offer",
23871
23909
  Lease: "lease",
@@ -23899,7 +23937,6 @@ var WorkspaceSyncWork = class {
23899
23937
  #rediscover;
23900
23938
  /** The route the directory's answer yields, recomputed once that answer is observed. */
23901
23939
  #reroute;
23902
- #placementFetch;
23903
23940
  #discoveredFrontier;
23904
23941
  #observePlacement;
23905
23942
  /** Ask the directory for the provider's current placement frontier this round although a
@@ -23908,19 +23945,14 @@ var WorkspaceSyncWork = class {
23908
23945
  #directoryAnswered;
23909
23946
  #placementFetched;
23910
23947
  /** A round which converged without leasing, finishing once placement alone is fetched. */
23911
- #afterPlacement;
23912
23948
  #leased = false;
23913
- #lease;
23914
23949
  #command;
23915
23950
  #requestId;
23916
23951
  #started;
23917
- #phase = WorkspaceSyncPhase.Lease;
23952
+ #state = { phase: WorkspaceSyncPhase.Idle };
23918
23953
  #localHead;
23919
23954
  #offerHead;
23920
23955
  #previousHead;
23921
- #fetchStarted;
23922
- #offerStarted;
23923
- #offerRequestId;
23924
23956
  #pushMillis = 0;
23925
23957
  #pushBytes = 0n;
23926
23958
  #desiredHead;
@@ -24015,7 +24047,7 @@ var WorkspaceSyncWork = class {
24015
24047
  throw new Error(`replica '${this.#binding.replicaId}' has no custody remote`);
24016
24048
  }
24017
24049
  if (this.#rediscover || this.#recheckPlacement || !this.#route?.placementObserved && !this.#route?.placementFrontier && !this.#kernel.resolveRef(this.#mount, this.#requestId, PLACEMENT_REF3)) {
24018
- this.#phase = WorkspaceSyncPhase.Discover;
24050
+ this.#state = { phase: WorkspaceSyncPhase.Discover };
24019
24051
  return this.#remember({
24020
24052
  case: "pending",
24021
24053
  value: create(CorrespondencePendingSchema, {
@@ -24035,13 +24067,13 @@ var WorkspaceSyncWork = class {
24035
24067
  #openRound() {
24036
24068
  if (this.#receiveState || this.#replaceLocalHead !== void 0) return this.#leaseEffect();
24037
24069
  if (!this.#localHead) return this.#leaseEffect();
24038
- this.#offerRequestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:frontier`;
24070
+ const requestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:frontier`;
24039
24071
  return this.#beginOffer(encodeInlineCustodyOffer({
24040
24072
  session: this.#binding.replicaId,
24041
- requestId: this.#offerRequestId,
24073
+ requestId,
24042
24074
  observedHead: this.#localHead,
24043
24075
  intents: []
24044
- }), true);
24076
+ }), { case: "awaiting", requestId }, true);
24045
24077
  }
24046
24078
  #offerOrReconcile() {
24047
24079
  if (this.#command.admit && this.#localHead && this.#localHead !== this.#previousHead) {
@@ -24062,13 +24094,13 @@ var WorkspaceSyncWork = class {
24062
24094
  throw new Error(`kernel did not classify transport for intent '${unspecified.intentId}'`);
24063
24095
  }
24064
24096
  if (suffix.length > 0) {
24065
- this.#offerStarted = this.#clock.monotonicNanoseconds();
24066
- this.#offerRequestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}`;
24097
+ const requestId2 = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}`;
24098
+ const startedAt = this.#clock.monotonicNanoseconds();
24067
24099
  this.#offerAfterConverge = true;
24068
24100
  this.#rebaseFrom = this.#previousHead;
24069
24101
  return this.#beginOffer(encodeInlineCustodyOffer({
24070
24102
  session: this.#binding.replicaId,
24071
- requestId: this.#offerRequestId,
24103
+ requestId: requestId2,
24072
24104
  ...this.#previousHead === void 0 ? {} : { baseHead: this.#previousHead },
24073
24105
  observedHead: this.#localHead,
24074
24106
  intents: suffix,
@@ -24079,17 +24111,17 @@ var WorkspaceSyncWork = class {
24079
24111
  this.#previousHead
24080
24112
  ) : void 0,
24081
24113
  fastForwardOnly: true
24082
- }));
24114
+ }), { case: "awaiting", requestId: requestId2, startedAt });
24083
24115
  }
24084
24116
  }
24085
- this.#offerRequestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:reconcile`;
24117
+ const requestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:reconcile`;
24086
24118
  return this.#beginOffer(encodeInlineCustodyOffer({
24087
24119
  session: this.#binding.replicaId,
24088
- requestId: this.#offerRequestId,
24120
+ requestId,
24089
24121
  baseHead: this.#previousHead,
24090
24122
  observedHead: this.#localHead,
24091
24123
  intents: []
24092
- }));
24124
+ }), { case: "awaiting", requestId });
24093
24125
  }
24094
24126
  /**
24095
24127
  * NO COMMON BASE — a round that cannot converge, and must not pay for history to find out.
@@ -24165,18 +24197,18 @@ var WorkspaceSyncWork = class {
24165
24197
  #pullBeforeOffer() {
24166
24198
  this.#offerAfterConverge = true;
24167
24199
  this.#rebaseFrom = this.#previousHead;
24168
- this.#offerRequestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:pull-${this.#rebases}`;
24200
+ const requestId = `${this.#binding.replicaId}:${this.#requestId}:${this.workId}:pull-${this.#rebases}`;
24169
24201
  return this.#beginOffer(encodeInlineCustodyOffer({
24170
24202
  session: this.#binding.replicaId,
24171
- requestId: this.#offerRequestId,
24203
+ requestId,
24172
24204
  ...this.#previousHead === void 0 ? {} : { baseHead: this.#previousHead },
24173
24205
  observedHead: this.#localHead,
24174
24206
  intents: []
24175
- }));
24207
+ }), { case: "awaiting", requestId });
24176
24208
  }
24177
- #leaseEffect() {
24209
+ #leaseEffect(afterPlacement) {
24178
24210
  const token = this.#authToken();
24179
- this.#phase = WorkspaceSyncPhase.Lease;
24211
+ this.#state = { phase: WorkspaceSyncPhase.Lease, ...afterPlacement ? { afterPlacement } : {} };
24180
24212
  return this.#remember({
24181
24213
  case: "pending",
24182
24214
  value: create(CorrespondencePendingSchema, {
@@ -24205,35 +24237,38 @@ var WorkspaceSyncWork = class {
24205
24237
  `workspace sync '${this.workId}' expected effect '${expected}', received '${receipt.effectId}'`
24206
24238
  );
24207
24239
  }
24240
+ const state = this.#state;
24208
24241
  const previousReceipt = this.#lastReceiptEffectId;
24209
24242
  this.#lastReceiptEffectId = receipt.effectId;
24210
24243
  try {
24211
24244
  if (receipt.outcome.case === "failure") {
24212
- if (this.#phase === WorkspaceSyncPhase.Discover) return this.#openRound();
24245
+ if (state.phase === WorkspaceSyncPhase.Discover) return this.#openRound();
24213
24246
  return this.#transportFailure(
24214
24247
  receipt.outcome.value.message,
24215
24248
  receipt.outcome.value.reason === PeerEffectFailureReason.OFFLINE
24216
24249
  );
24217
24250
  }
24218
- if (this.#phase === WorkspaceSyncPhase.Offer || this.#phase === WorkspaceSyncPhase.Probe) return this.#acceptOffer(receipt);
24219
- if (this.#phase === WorkspaceSyncPhase.Lease) return this.#acceptLease(receipt);
24220
- if (this.#phase === WorkspaceSyncPhase.Discover) return this.#acceptDiscovery(receipt);
24221
- if (this.#phase === WorkspaceSyncPhase.Placement) {
24222
- if (!this.#placementFetch || !this.#lease) throw new Error("placement transfer lost its lease");
24223
- this.#placementFetch.accept(receipt);
24251
+ if (state.phase === WorkspaceSyncPhase.Offer || state.phase === WorkspaceSyncPhase.Probe) {
24252
+ return this.#acceptOffer(receipt, state);
24253
+ }
24254
+ if (state.phase === WorkspaceSyncPhase.Lease) return this.#acceptLease(receipt, state);
24255
+ if (state.phase === WorkspaceSyncPhase.Discover) return this.#acceptDiscovery(receipt);
24256
+ if (state.phase === WorkspaceSyncPhase.Placement) {
24257
+ state.transfer.accept(receipt);
24224
24258
  this.#placementFetched?.();
24225
- if (this.#afterPlacement) return this.#finish(this.#afterPlacement);
24226
- return this.#beginMainFetch(this.#lease);
24259
+ if (state.afterPlacement) return this.#finish(state.afterPlacement);
24260
+ return this.#beginMainFetch(state.lease);
24227
24261
  }
24228
- if (this.#phase === WorkspaceSyncPhase.Fetch) return this.#acceptFetch(receipt);
24262
+ if (state.phase === WorkspaceSyncPhase.Fetch) return this.#acceptFetch(receipt, state);
24229
24263
  throw new Error(`workspace sync '${this.workId}' is already complete`);
24230
24264
  } catch (error) {
24231
24265
  this.#lastReceiptEffectId = previousReceipt;
24232
24266
  throw error;
24233
24267
  }
24234
24268
  }
24235
- #beginOffer(body, probe = false) {
24236
- this.#phase = probe ? WorkspaceSyncPhase.Probe : WorkspaceSyncPhase.Offer;
24269
+ #beginOffer(body, exchange, probe = false) {
24270
+ const phase = probe ? WorkspaceSyncPhase.Probe : WorkspaceSyncPhase.Offer;
24271
+ this.#state = { phase, exchange };
24237
24272
  const token = this.#authToken();
24238
24273
  this.#pushBytes += BigInt(body.byteLength);
24239
24274
  return this.#remember({
@@ -24243,7 +24278,7 @@ var WorkspaceSyncWork = class {
24243
24278
  effect: {
24244
24279
  case: "http",
24245
24280
  value: create(PeerHttpEffectSchema, {
24246
- effectId: `${this.workId}:${this.#phase}`,
24281
+ effectId: `${this.workId}:${phase}`,
24247
24282
  method: PeerHttpMethod.POST,
24248
24283
  url: this.#offerEndpoint(),
24249
24284
  contentType: CUSTODY_OFFER_MEDIA_TYPE,
@@ -24260,28 +24295,30 @@ var WorkspaceSyncWork = class {
24260
24295
  })
24261
24296
  });
24262
24297
  }
24263
- #acceptOffer(receipt) {
24264
- this.#pushMillis = this.#offerStarted === void 0 ? 0 : elapsedMillis(this.#clock, this.#offerStarted);
24298
+ #acceptOffer(receipt, state) {
24299
+ const exchange = state.exchange;
24300
+ this.#pushMillis = exchange.startedAt === void 0 ? 0 : elapsedMillis(this.#clock, exchange.startedAt);
24265
24301
  if (receipt.outcome.case !== "response") {
24266
24302
  throw new Error("platform returned the wrong receipt for a custody offer");
24267
24303
  }
24268
- const response = receipt.outcome.value;
24269
- if (!response.contentType?.includes(CUSTODY_OFFER_RESULT_MEDIA_TYPE)) {
24304
+ const outcome = settleCustodyOfferExchange(exchange, receipt.outcome.value);
24305
+ if (outcome.case === "maintenance") {
24306
+ return this.#finishOfferOutcome(true, false);
24307
+ }
24308
+ if (outcome.case === "unexpected") {
24270
24309
  this.#pushError = create(RuntimeTransportFailureSchema, {
24271
24310
  endpoint: this.#offerEndpoint(),
24272
- status: response.status,
24311
+ status: outcome.status,
24273
24312
  refused: false,
24274
- message: `Sync received an unexpected server response (HTTP ${response.status}; ${response.contentType || "no content type"}). No custody receipt was returned.`
24313
+ message: `Sync received an unexpected server response (HTTP ${outcome.status}; ${outcome.contentType || "no content type"}). No custody receipt was returned.`
24275
24314
  });
24276
24315
  return this.#finishOfferOutcome(false);
24277
24316
  }
24278
- if (!this.#offerRequestId) throw new Error("custody offer has no request identity");
24279
- const outcome = decodeCustodyOffer(response.body, this.#offerRequestId);
24280
- this.#offerReceipt = custodyOfferReceipt(this.#offerRequestId, outcome);
24317
+ this.#offerReceipt = outcome.receipt;
24281
24318
  if (outcome.case === "failure") {
24282
24319
  this.#pushError = create(RuntimeTransportFailureSchema, {
24283
24320
  endpoint: this.#offerEndpoint(),
24284
- status: response.status,
24321
+ status: outcome.status,
24285
24322
  refused: false,
24286
24323
  message: `${outcome.value.code}: ${outcome.value.message}`
24287
24324
  });
@@ -24290,12 +24327,12 @@ var WorkspaceSyncWork = class {
24290
24327
  if (outcome.case === "blocked") {
24291
24328
  return this.#finishOfferOutcome(true);
24292
24329
  }
24293
- if (response.status < 200 || response.status >= 300) {
24330
+ if (outcome.status < 200 || outcome.status >= 300) {
24294
24331
  this.#pushError = create(RuntimeTransportFailureSchema, {
24295
24332
  endpoint: this.#offerEndpoint(),
24296
- status: response.status,
24297
- refused: response.status === 422,
24298
- message: `custody offer returned an admission verdict with HTTP ${response.status}`
24333
+ status: outcome.status,
24334
+ refused: outcome.status === 422,
24335
+ message: `custody offer returned an admission verdict with HTTP ${outcome.status}`
24299
24336
  });
24300
24337
  return this.#finishOfferOutcome(false);
24301
24338
  }
@@ -24303,7 +24340,7 @@ var WorkspaceSyncWork = class {
24303
24340
  this.#desiredHead = admission.main || void 0;
24304
24341
  if (!this.#desiredHead) return this.#transportFailure("resident peer returned no admitted frontier", false);
24305
24342
  this.#candidateDurable = admission.sealedMain === this.#desiredHead;
24306
- if (this.#phase === WorkspaceSyncPhase.Probe && this.#desiredHead !== this.#localHead) {
24343
+ if (state.phase === WorkspaceSyncPhase.Probe && this.#desiredHead !== this.#localHead) {
24307
24344
  if (!this.#establishBase(this.#desiredHead)) return this.#noCommonBaseResult();
24308
24345
  if (this.#alreadyJudged(this.#desiredHead)) {
24309
24346
  return this.#finish(create(SyncWorkspaceResponseSchema, {
@@ -24365,7 +24402,7 @@ var WorkspaceSyncWork = class {
24365
24402
  if (admission.recoveryRequired !== false) return this.#leaseEffect();
24366
24403
  return this.#transportFailure("resident peer supplied neither the missing delta nor a history gap", false);
24367
24404
  }
24368
- #acceptLease(receipt) {
24405
+ #acceptLease(receipt, state) {
24369
24406
  if (receipt.outcome.case !== "response") {
24370
24407
  throw new Error("platform returned the wrong receipt for a Git read lease");
24371
24408
  }
@@ -24381,21 +24418,25 @@ var WorkspaceSyncWork = class {
24381
24418
  this.#leased = true;
24382
24419
  const frontier = this.#placementFrontier();
24383
24420
  if (frontier) {
24384
- this.#placementFetch = new PlacementFetch(
24421
+ const transfer = new PlacementFetch(
24385
24422
  this.#kernel,
24386
24423
  this.#mount,
24387
24424
  this.#requestId,
24388
24425
  this.workId,
24389
24426
  frontier
24390
24427
  );
24391
- const pending = this.#placementFetch.start(lease);
24428
+ const pending = transfer.start(lease);
24392
24429
  if (pending) {
24393
- this.#phase = WorkspaceSyncPhase.Placement;
24394
- this.#lease = lease;
24430
+ this.#state = {
24431
+ phase: WorkspaceSyncPhase.Placement,
24432
+ transfer,
24433
+ lease,
24434
+ ...state.afterPlacement ? { afterPlacement: state.afterPlacement } : {}
24435
+ };
24395
24436
  return this.#remember({ case: "pending", value: pending });
24396
24437
  }
24397
24438
  }
24398
- if (this.#afterPlacement) return this.#finish(this.#afterPlacement);
24439
+ if (state.afterPlacement) return this.#finish(state.afterPlacement);
24399
24440
  return this.#beginMainFetch(lease);
24400
24441
  }
24401
24442
  /** The provider's placement frontier as this round knows it: what the directory named in this
@@ -24415,8 +24456,7 @@ var WorkspaceSyncWork = class {
24415
24456
  if (!new PlacementFetch(this.#kernel, this.#mount, this.#requestId, this.workId, frontier).wanted()) {
24416
24457
  return void 0;
24417
24458
  }
24418
- this.#afterPlacement = presented;
24419
- return this.#leaseEffect();
24459
+ return this.#leaseEffect(presented);
24420
24460
  }
24421
24461
  #acceptDiscovery(receipt) {
24422
24462
  this.#directoryAnswered?.();
@@ -24440,9 +24480,11 @@ var WorkspaceSyncWork = class {
24440
24480
  return this.#replaceLocalHead !== void 0 || this.#localHead === void 0 && this.#previousHead === void 0;
24441
24481
  }
24442
24482
  #beginMainFetch(lease) {
24443
- this.#phase = WorkspaceSyncPhase.Fetch;
24444
24483
  const lawProducts = this.#lawProductsAlongside();
24445
- this.#fetchStarted = this.#clock.monotonicNanoseconds();
24484
+ this.#state = {
24485
+ phase: WorkspaceSyncPhase.Fetch,
24486
+ startedAt: this.#clock.monotonicNanoseconds()
24487
+ };
24446
24488
  const haves = this.#replaceLocalHead !== void 0 ? this.#repairMissingCommit ? [] : [this.#kernel.resolveRef(this.#mount, this.#requestId, deferredRemote)].filter(
24447
24489
  (head) => !!head
24448
24490
  ) : [...new Set([
@@ -24473,7 +24515,7 @@ var WorkspaceSyncWork = class {
24473
24515
  })
24474
24516
  });
24475
24517
  }
24476
- #acceptFetch(receipt) {
24518
+ #acceptFetch(receipt, state) {
24477
24519
  if (receipt.outcome.case !== "gitFetch") {
24478
24520
  throw new Error("platform returned the wrong receipt for a Git object fetch");
24479
24521
  }
@@ -24483,7 +24525,7 @@ var WorkspaceSyncWork = class {
24483
24525
  if (fetched.pack.byteLength > 0) {
24484
24526
  this.#kernel.applyPack(this.#mount, this.#requestId, fetched.pack, fetched.head, fetched.shallow);
24485
24527
  }
24486
- const prefetchMillis = this.#fetchStarted === void 0 ? 0 : elapsedMillis(this.#clock, this.#fetchStarted);
24528
+ const prefetchMillis = elapsedMillis(this.#clock, state.startedAt);
24487
24529
  const candidateHead = this.#receiveState ? this.#kernel.resolveStatePosition?.(this.#mount, this.#requestId, fetched.head, this.#receiveState) : fetched.head;
24488
24530
  if (!candidateHead) {
24489
24531
  return this.#finish(create(SyncWorkspaceResponseSchema, {
@@ -24834,11 +24876,11 @@ var WorkspaceSyncWork = class {
24834
24876
  pushBytes: this.#pushBytes,
24835
24877
  pullBytes: BigInt(receivedBytes),
24836
24878
  serverMillis: this.#admissionSummary?.serverMillis ?? 0,
24837
- offerInPush: this.#offerRequestId !== void 0
24879
+ offerInPush: this.#pushBytes > 0n
24838
24880
  })
24839
24881
  });
24840
24882
  }
24841
- #finishOfferOutcome(blocked) {
24883
+ #finishOfferOutcome(blocked, reconnectRequired = true) {
24842
24884
  return this.#finish(create(SyncWorkspaceResponseSchema, {
24843
24885
  workspace: this.#mount.workspace,
24844
24886
  previousHead: this.#previousHead,
@@ -24846,7 +24888,7 @@ var WorkspaceSyncWork = class {
24846
24888
  admitted: BigInt(this.#admissionSummary?.admittedIntentIds.length ?? 0),
24847
24889
  refused: BigInt((this.#admissionSummary?.refusedMessages.length ?? 0) + this.#parkedLocal),
24848
24890
  deferred: BigInt(this.#admissionSummary ? this.#admissionSummary.blockedMessages.length + this.#admissionSummary.deadLetteredMessages.length + this.#admissionSummary.quarantinedMessages.length + this.#admissionSummary.deferredMessages.length : blocked ? 1 : 0),
24849
- reconnectRequired: true,
24891
+ reconnectRequired,
24850
24892
  stillAhead: this.#localHead !== this.#previousHead,
24851
24893
  directOfferDeferred: blocked,
24852
24894
  offerReceipt: this.#offerReceipt,
@@ -24860,15 +24902,16 @@ var WorkspaceSyncWork = class {
24860
24902
  }));
24861
24903
  }
24862
24904
  #transportFailure(message, offline, status = -1) {
24863
- const pushFailed = this.#phase === WorkspaceSyncPhase.Offer;
24905
+ const state = this.#state;
24906
+ const pushFailed = state.phase === WorkspaceSyncPhase.Offer;
24864
24907
  const failedEndpoint = this.#offerEndpoint();
24865
24908
  const failure = create(RuntimeTransportFailureSchema, {
24866
24909
  endpoint: pushFailed ? failedEndpoint : this.#binding.cloud ?? "",
24867
24910
  status,
24868
24911
  message
24869
24912
  });
24870
- if (pushFailed) {
24871
- this.#pushMillis = this.#offerStarted === void 0 ? 0 : elapsedMillis(this.#clock, this.#offerStarted);
24913
+ if (state.phase === WorkspaceSyncPhase.Offer) {
24914
+ this.#pushMillis = state.exchange.startedAt === void 0 ? 0 : elapsedMillis(this.#clock, state.exchange.startedAt);
24872
24915
  this.#pushError = failure;
24873
24916
  }
24874
24917
  return this.#finish(create(SyncWorkspaceResponseSchema, {
@@ -24885,7 +24928,7 @@ var WorkspaceSyncWork = class {
24885
24928
  pushMillis: this.#pushMillis,
24886
24929
  pushBytes: this.#pushBytes,
24887
24930
  prefetchMillis: elapsedMillis(this.#clock, this.#started),
24888
- offerInPush: this.#offerRequestId !== void 0
24931
+ offerInPush: this.#pushBytes > 0n
24889
24932
  })
24890
24933
  }));
24891
24934
  }
@@ -24895,7 +24938,7 @@ var WorkspaceSyncWork = class {
24895
24938
  return step;
24896
24939
  }
24897
24940
  #finish(value) {
24898
- this.#phase = WorkspaceSyncPhase.Done;
24941
+ this.#state = { phase: WorkspaceSyncPhase.Done };
24899
24942
  this.#current = void 0;
24900
24943
  return this.#remember({ case: "synced", value });
24901
24944
  }
@@ -543,6 +543,8 @@ final class NmsSyncStatusProjection {
543
543
  for (final stream in streams) {
544
544
  if (!stream.isClosed) unawaited(stream.close());
545
545
  }
546
- if (!_application.isClosed) await _application.close();
546
+ // A paused UI subscriber may not process the done event until it resumes.
547
+ // Closing this broadcast stream must not hold reception teardown open.
548
+ if (!_application.isClosed) unawaited(_application.close());
547
549
  }
548
550
  }
package/dart/pubspec.yaml CHANGED
@@ -7,7 +7,7 @@ repository: https://github.com/Captain-App/nomos2
7
7
  description: >-
8
8
  Nomos 2 Dart client — generated domain values, intent payloads and application
9
9
  lifecycle types over the typed Nomos kernel protocol and USDA custody model.
10
- version: 0.66.5
10
+ version: 0.66.6
11
11
 
12
12
  environment:
13
13
  sdk: ^3.5.0
@@ -0,0 +1,16 @@
1
+ import 'package:nomos_client/src/reception/event_desk.dart';
2
+ import 'package:test/test.dart';
3
+
4
+ void main() {
5
+ test('reception closes while presentation observers are paused', () async {
6
+ final desk = ReceptionEventDesk();
7
+ final copies = desk.copyChanges.listen((_) {})..pause();
8
+ final application = desk.syncStatuses.applicationStream.listen((_) {})
9
+ ..pause();
10
+
11
+ await desk.close().timeout(const Duration(seconds: 1));
12
+
13
+ await copies.cancel();
14
+ await application.cancel();
15
+ });
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.110.0",
3
+ "version": "0.110.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
@@ -72,7 +72,7 @@
72
72
  "test": "vitest run"
73
73
  },
74
74
  "dependencies": {
75
- "@githolon/runtime": "^0.110.0",
75
+ "@githolon/runtime": "^0.110.1",
76
76
  "@bufbuild/protobuf": "^2.13.0",
77
77
  "@bufbuild/protoc-gen-es": "^2.13.0",
78
78
  "esbuild": "0.24.2",
@@ -1237,7 +1237,7 @@ export function emitDartCallProtoEncoder(
1237
1237
  for (const field of fields) {
1238
1238
  const fieldAccess = access.get(field.name);
1239
1239
  if (fieldAccess?.omit === true || fieldAccess?.present === "false") continue;
1240
- const raw = fieldAccess?.slot === true ? `${fieldAccess.value}.value` : fieldAccess?.value ?? `payload.${dartMemberName(field.name)}`;
1240
+ const raw = fieldAccess?.slot === true ? `${fieldAccess.value}.valueOrNull` : fieldAccess?.value ?? `payload.${dartMemberName(field.name)}`;
1241
1241
  const fieldSemantic = typeNode(spec.nodes, field.value.id).expression.semantic;
1242
1242
  const fieldValueIsCanonical = fieldSemantic?.kind === "id-reference" ||
1243
1243
  fieldSemantic?.kind === "address-reference" ||
@@ -1252,7 +1252,9 @@ export function emitDartCallProtoEncoder(
1252
1252
  emitValue(body, raw, field.value.id, field.name, (expression) => {
1253
1253
  body.push(`${member} = ${expression};`);
1254
1254
  }, fieldValueIsCanonical);
1255
- if (field.required) lines.push(...body.map((line) => ` ${line}`));
1255
+ if (fieldAccess?.slot === true) {
1256
+ lines.push(` if (!${fieldAccess.value}.isOmitted) {`, ...body.map((line) => ` ${line}`), ` }`);
1257
+ } else if (field.required) lines.push(...body.map((line) => ` ${line}`));
1256
1258
  else lines.push(` if (${fieldAccess?.present ?? `${raw} != null`}) {`, ...body.map((line) => ` ${line}`), ` }`);
1257
1259
  }
1258
1260
  lines.push(
@@ -5479,6 +5479,10 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
5479
5479
  * Every tuple and subject is nominally typed from the same DSL graph; there is no JSON leaf or map-shaped
5480
5480
  * boundary. INFORMATIONAL only (the one wasm gate is still authoritative). */
5481
5481
  export function dartRoleHelpers(mod: DomainModule): string {
5482
+ // Some packages have role policy declarations but do not project the framework
5483
+ // RelationTuple aggregate into this domain. In that case there is no typed
5484
+ // read model for a client-side hint to consume.
5485
+ if (!mod.aggregates.some((aggregate) => aggregate.id === "RelationTuple")) return "";
5482
5486
  const editRels = new Map<string, Set<string>>();
5483
5487
  const viewRels = new Map<string, Set<string>>();
5484
5488
  for (const r of mod.roles ?? []) {
@@ -2544,9 +2544,10 @@ const NOMOS_TYPES_DEP = "^0.11.2";
2544
2544
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
2545
2545
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
2546
2546
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
2547
- const NOMOS_CLIENT_DEP = "^0.66.5";
2548
- // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
2549
- // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
2547
+ const NOMOS_CLIENT_DEP = "^0.66.6";
2548
+ const NOMOS_FLUTTER_DEP = "^0.65.8";
2549
+ // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in the Dart and Flutter
2550
+ // package pubspecs — a release without this bump fails the suite, so tenants never
2550
2551
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
2551
2552
 
2552
2553
  const isPubPackageName = (s: string): boolean => /^[a-z][a-z0-9_]*$/.test(s);
@@ -3166,10 +3167,13 @@ function reconcilePubspec(
3166
3167
  index < dependenciesEnd &&
3167
3168
  line.startsWith(prefix)
3168
3169
  );
3169
- const replacement = ` ${name}: ${constraint}`;
3170
+ const replacement =
3171
+ name === "flutter" && constraint === "sdk:flutter"
3172
+ ? [" flutter:", " sdk: flutter"]
3173
+ : [` ${name}: ${constraint}`];
3170
3174
  if (existing < 0) {
3171
- lines.splice(dependenciesEnd, 0, replacement);
3172
- dependenciesEnd += 1;
3175
+ lines.splice(dependenciesEnd, 0, ...replacement);
3176
+ dependenciesEnd += replacement.length;
3173
3177
  continue;
3174
3178
  }
3175
3179
  let nestedEnd = existing + 1;
@@ -3178,8 +3182,8 @@ function reconcilePubspec(
3178
3182
  /^(?: {4,}|\t)/.test(lines[nestedEnd]!)
3179
3183
  )
3180
3184
  nestedEnd += 1;
3181
- lines.splice(existing, nestedEnd - existing, replacement);
3182
- dependenciesEnd -= nestedEnd - existing - 1;
3185
+ lines.splice(existing, nestedEnd - existing, ...replacement);
3186
+ dependenciesEnd += replacement.length - (nestedEnd - existing);
3183
3187
  }
3184
3188
 
3185
3189
  const next = `${lines.join("\n")}${trailingNewline ? "\n" : ""}`;
@@ -4045,6 +4049,7 @@ function emitDartPackages(
4045
4049
  ` flutter:`,
4046
4050
  ` sdk: flutter`,
4047
4051
  ` nomos_client: ${NOMOS_CLIENT_DEP}`,
4052
+ ` nomos_flutter: ${NOMOS_FLUTTER_DEP}`,
4048
4053
  ` ${packages.client}:`,
4049
4054
  ` path: ../${packages.client} # co-emitted sibling; replace with a hosted constraint when publishing`,
4050
4055
  `dev_dependencies:`,
@@ -4054,7 +4059,11 @@ function emitDartPackages(
4054
4059
  ` - lib/${genRel}/${embeddedLawName}`,
4055
4060
  ``,
4056
4061
  ].join("\n"),
4057
- { nomos_client: NOMOS_CLIENT_DEP }
4062
+ {
4063
+ flutter: "sdk:flutter",
4064
+ nomos_client: NOMOS_CLIENT_DEP,
4065
+ nomos_flutter: NOMOS_FLUTTER_DEP,
4066
+ }
4058
4067
  );
4059
4068
  reconcileGeneratedLawAsset(
4060
4069
  pkgDir(packages.test),
@@ -8918,12 +8927,14 @@ async function main(): Promise<void> {
8918
8927
  const client = path.resolve(selfDir, "..", "dart");
8919
8928
  const types = path.resolve(selfDir, "..", "dart_types");
8920
8929
  const peerHost = path.resolve(selfDir, "..", "..", "nomos_peer_host");
8930
+ const flutter = path.resolve(selfDir, "..", "..", "nomos_flutter");
8921
8931
  if (
8922
8932
  existsSync(path.join(client, "pubspec.yaml")) &&
8923
8933
  existsSync(path.join(types, "pubspec.yaml")) &&
8924
- existsSync(path.join(peerHost, "pubspec.yaml"))
8934
+ existsSync(path.join(peerHost, "pubspec.yaml")) &&
8935
+ existsSync(path.join(flutter, "pubspec.yaml"))
8925
8936
  ) {
8926
- return { client, types, peerHost };
8937
+ return { client, types, peerHost, flutter };
8927
8938
  }
8928
8939
  return undefined;
8929
8940
  })();
@@ -8961,6 +8972,7 @@ async function main(): Promise<void> {
8961
8972
  const lines = ["dependency_overrides:"];
8962
8973
  const usesClient = /(^|\n)\s+nomos_client\s*:/.test(spec);
8963
8974
  const usesTypes = /(^|\n)\s+nomos_types\s*:/.test(spec);
8975
+ const usesFlutter = /(^|\n)\s+nomos_flutter\s*:/.test(spec);
8964
8976
  if (usesClient)
8965
8977
  lines.push(` nomos_client:`, ` path: ${localDart.client}`);
8966
8978
  // A generated CLIENT or TEST package may only mention nomos_client directly, but that local
@@ -8978,6 +8990,8 @@ async function main(): Promise<void> {
8978
8990
  ` nomos_peer_host:`,
8979
8991
  ` path: ${localDart.peerHost}`
8980
8992
  );
8993
+ if (usesFlutter)
8994
+ lines.push(` nomos_flutter:`, ` path: ${localDart.flutter}`);
8981
8995
  if (lines.length > 1) {
8982
8996
  writeTextIfChanged(overrideFile, lines.join("\n") + "\n");
8983
8997
  }
@@ -9039,7 +9053,10 @@ async function main(): Promise<void> {
9039
9053
  for (const pkg of pkgDirs) {
9040
9054
  if (dartPackageResolutionCurrent(pkg)) continue;
9041
9055
  try {
9042
- execFileSync("dart", ["pub", "get"], {
9056
+ const isFlutterPackage = /^\s{2}flutter:\s*\n\s{4}sdk:\s*flutter\s*$/m.test(
9057
+ readFileSync(path.join(pkg, "pubspec.yaml"), "utf8")
9058
+ );
9059
+ execFileSync(isFlutterPackage ? "flutter" : "dart", ["pub", "get"], {
9043
9060
  cwd: pkg,
9044
9061
  stdio: "pipe",
9045
9062
  encoding: "utf8",