feat: installModuleStreaming for live install progress

Wraps the new HubAdmin/InstallModuleStream: yields one update per
phase and, during the download, as bytes arrive; completes on success
and surfaces the hub's failure text as a stream error, so callers
handle failures exactly as with the unary installModule. Generated
stubs regenerated from the protos (additive only).

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-09-08 13:38:35 +02:00
parent 5759ce4c7a
commit 1d3d33d55a
4 changed files with 381 additions and 0 deletions

View file

@ -3576,6 +3576,246 @@ class InstallModuleResponse extends $pb.GeneratedMessage {
void clearCapabilityCount() => $_clearField(3); void clearCapabilityCount() => $_clearField(3);
} }
enum InstallProgressEvent_Event { update, finished, failed, notSet }
/// One message in an InstallModuleStream.
class InstallProgressEvent extends $pb.GeneratedMessage {
factory InstallProgressEvent({
InstallProgressUpdate? update,
InstallModuleResponse? finished,
$core.String? failed,
}) {
final result = create();
if (update != null) result.update = update;
if (finished != null) result.finished = finished;
if (failed != null) result.failed = failed;
return result;
}
InstallProgressEvent._();
factory InstallProgressEvent.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory InstallProgressEvent.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static const $core.Map<$core.int, InstallProgressEvent_Event>
_InstallProgressEvent_EventByTag = {
1: InstallProgressEvent_Event.update,
2: InstallProgressEvent_Event.finished,
3: InstallProgressEvent_Event.failed,
0: InstallProgressEvent_Event.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'InstallProgressEvent',
package: const $pb.PackageName(_omitMessageNames ? '' : 'chain.v1'),
createEmptyInstance: create)
..oo(0, [1, 2, 3])
..aOM<InstallProgressUpdate>(1, _omitFieldNames ? '' : 'update',
subBuilder: InstallProgressUpdate.create)
..aOM<InstallModuleResponse>(2, _omitFieldNames ? '' : 'finished',
subBuilder: InstallModuleResponse.create)
..aOS(3, _omitFieldNames ? '' : 'failed')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InstallProgressEvent clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InstallProgressEvent copyWith(void Function(InstallProgressEvent) updates) =>
super.copyWith((message) => updates(message as InstallProgressEvent))
as InstallProgressEvent;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static InstallProgressEvent create() => InstallProgressEvent._();
@$core.override
InstallProgressEvent createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static InstallProgressEvent getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<InstallProgressEvent>(create);
static InstallProgressEvent? _defaultInstance;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
@$pb.TagNumber(3)
InstallProgressEvent_Event whichEvent() =>
_InstallProgressEvent_EventByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
@$pb.TagNumber(3)
void clearEvent() => $_clearField($_whichOneof(0));
/// Progress moved, or a phase started or finished.
@$pb.TagNumber(1)
InstallProgressUpdate get update => $_getN(0);
@$pb.TagNumber(1)
set update(InstallProgressUpdate value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasUpdate() => $_has(0);
@$pb.TagNumber(1)
void clearUpdate() => $_clearField(1);
@$pb.TagNumber(1)
InstallProgressUpdate ensureUpdate() => $_ensure(0);
/// The install succeeded; always the final message.
@$pb.TagNumber(2)
InstallModuleResponse get finished => $_getN(1);
@$pb.TagNumber(2)
set finished(InstallModuleResponse value) => $_setField(2, value);
@$pb.TagNumber(2)
$core.bool hasFinished() => $_has(1);
@$pb.TagNumber(2)
void clearFinished() => $_clearField(2);
@$pb.TagNumber(2)
InstallModuleResponse ensureFinished() => $_ensure(1);
/// The install failed; always the final message. Carries the same
/// text the unary call would have returned as its status message.
@$pb.TagNumber(3)
$core.String get failed => $_getSZ(2);
@$pb.TagNumber(3)
set failed($core.String value) => $_setString(2, value);
@$pb.TagNumber(3)
$core.bool hasFailed() => $_has(2);
@$pb.TagNumber(3)
void clearFailed() => $_clearField(3);
}
/// A single progress observation.
class InstallProgressUpdate extends $pb.GeneratedMessage {
factory InstallProgressUpdate({
$core.String? phase,
$core.String? stage,
$core.double? percent,
$fixnum.Int64? bytesReceived,
$fixnum.Int64? bytesTotal,
$core.bool? phaseDone,
}) {
final result = create();
if (phase != null) result.phase = phase;
if (stage != null) result.stage = stage;
if (percent != null) result.percent = percent;
if (bytesReceived != null) result.bytesReceived = bytesReceived;
if (bytesTotal != null) result.bytesTotal = bytesTotal;
if (phaseDone != null) result.phaseDone = phaseDone;
return result;
}
InstallProgressUpdate._();
factory InstallProgressUpdate.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory InstallProgressUpdate.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'InstallProgressUpdate',
package: const $pb.PackageName(_omitMessageNames ? '' : 'chain.v1'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'phase')
..aOS(2, _omitFieldNames ? '' : 'stage')
..aD(3, _omitFieldNames ? '' : 'percent', fieldType: $pb.PbFieldType.OF)
..a<$fixnum.Int64>(
4, _omitFieldNames ? '' : 'bytesReceived', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..a<$fixnum.Int64>(
5, _omitFieldNames ? '' : 'bytesTotal', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..aOB(6, _omitFieldNames ? '' : 'phaseDone')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InstallProgressUpdate clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InstallProgressUpdate copyWith(
void Function(InstallProgressUpdate) updates) =>
super.copyWith((message) => updates(message as InstallProgressUpdate))
as InstallProgressUpdate;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static InstallProgressUpdate create() => InstallProgressUpdate._();
@$core.override
InstallProgressUpdate createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static InstallProgressUpdate getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<InstallProgressUpdate>(create);
static InstallProgressUpdate? _defaultInstance;
/// Stable phase identifier: "resolve", "download" or "install".
/// Clients map it to their own wording; it is not display text.
@$pb.TagNumber(1)
$core.String get phase => $_getSZ(0);
@$pb.TagNumber(1)
set phase($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasPhase() => $_has(0);
@$pb.TagNumber(1)
void clearPhase() => $_clearField(1);
/// English fallback label for the phase ("Downloading"). Clients with
/// their own translations key off `phase` and ignore this.
@$pb.TagNumber(2)
$core.String get stage => $_getSZ(1);
@$pb.TagNumber(2)
set stage($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasStage() => $_has(1);
@$pb.TagNumber(2)
void clearStage() => $_clearField(2);
/// Overall progress across all phases, 0..100, monotonic. Absent
/// means the extent is not knowable yet show an indeterminate
/// indicator, never a bar parked at zero.
@$pb.TagNumber(3)
$core.double get percent => $_getN(2);
@$pb.TagNumber(3)
set percent($core.double value) => $_setFloat(2, value);
@$pb.TagNumber(3)
$core.bool hasPercent() => $_has(2);
@$pb.TagNumber(3)
void clearPercent() => $_clearField(3);
/// Bytes received so far during the download phase.
@$pb.TagNumber(4)
$fixnum.Int64 get bytesReceived => $_getI64(3);
@$pb.TagNumber(4)
set bytesReceived($fixnum.Int64 value) => $_setInt64(3, value);
@$pb.TagNumber(4)
$core.bool hasBytesReceived() => $_has(3);
@$pb.TagNumber(4)
void clearBytesReceived() => $_clearField(4);
/// Total bytes the server announced. Zero means the server sent no
/// content-length, so only `bytes_received` is meaningful.
@$pb.TagNumber(5)
$fixnum.Int64 get bytesTotal => $_getI64(4);
@$pb.TagNumber(5)
set bytesTotal($fixnum.Int64 value) => $_setInt64(4, value);
@$pb.TagNumber(5)
$core.bool hasBytesTotal() => $_has(4);
@$pb.TagNumber(5)
void clearBytesTotal() => $_clearField(5);
/// The named phase just finished.
@$pb.TagNumber(6)
$core.bool get phaseDone => $_getBF(5);
@$pb.TagNumber(6)
set phaseDone($core.bool value) => $_setBool(5, value);
@$pb.TagNumber(6)
$core.bool hasPhaseDone() => $_has(5);
@$pb.TagNumber(6)
void clearPhaseDone() => $_clearField(6);
}
class UninstallModuleRequest extends $pb.GeneratedMessage { class UninstallModuleRequest extends $pb.GeneratedMessage {
factory UninstallModuleRequest({ factory UninstallModuleRequest({
$core.String? name, $core.String? name,

View file

@ -442,6 +442,21 @@ class HubAdminClient extends $grpc.Client {
return $createUnaryCall(_$installModule, request, options: options); return $createUnaryCall(_$installModule, request, options: options);
} }
/// Same install, with live progress. Emits an update per phase and,
/// during the download, as bytes arrive; the last message carries
/// either the finished response or the error. Clients that only need
/// the outcome keep using the unary InstallModule above this exists
/// so a waiting operator can see that a slow download is progressing
/// rather than hung.
$grpc.ResponseStream<$0.InstallProgressEvent> installModuleStream(
$0.InstallModuleRequest request, {
$grpc.CallOptions? options,
}) {
return $createStreamingCall(
_$installModuleStream, $async.Stream.fromIterable([request]),
options: options);
}
/// Remove an installed module by name. Wipes the module /// Remove an installed module by name. Wipes the module
/// directory and re-scans the registry; writes a /// directory and re-scans the registry; writes a
/// `module.uninstalled` audit event. Surfaced by the Studio /// `module.uninstalled` audit event. Surfaced by the Studio
@ -956,6 +971,11 @@ class HubAdminClient extends $grpc.Client {
'/chain.v1.HubAdmin/InstallModule', '/chain.v1.HubAdmin/InstallModule',
($0.InstallModuleRequest value) => value.writeToBuffer(), ($0.InstallModuleRequest value) => value.writeToBuffer(),
$0.InstallModuleResponse.fromBuffer); $0.InstallModuleResponse.fromBuffer);
static final _$installModuleStream =
$grpc.ClientMethod<$0.InstallModuleRequest, $0.InstallProgressEvent>(
'/chain.v1.HubAdmin/InstallModuleStream',
($0.InstallModuleRequest value) => value.writeToBuffer(),
$0.InstallProgressEvent.fromBuffer);
static final _$uninstallModule = static final _$uninstallModule =
$grpc.ClientMethod<$0.UninstallModuleRequest, $0.UninstallModuleResponse>( $grpc.ClientMethod<$0.UninstallModuleRequest, $0.UninstallModuleResponse>(
'/chain.v1.HubAdmin/UninstallModule', '/chain.v1.HubAdmin/UninstallModule',
@ -1285,6 +1305,15 @@ abstract class HubAdminServiceBase extends $grpc.Service {
($core.List<$core.int> value) => ($core.List<$core.int> value) =>
$0.InstallModuleRequest.fromBuffer(value), $0.InstallModuleRequest.fromBuffer(value),
($0.InstallModuleResponse value) => value.writeToBuffer())); ($0.InstallModuleResponse value) => value.writeToBuffer()));
$addMethod(
$grpc.ServiceMethod<$0.InstallModuleRequest, $0.InstallProgressEvent>(
'InstallModuleStream',
installModuleStream_Pre,
false,
true,
($core.List<$core.int> value) =>
$0.InstallModuleRequest.fromBuffer(value),
($0.InstallProgressEvent value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.UninstallModuleRequest, $addMethod($grpc.ServiceMethod<$0.UninstallModuleRequest,
$0.UninstallModuleResponse>( $0.UninstallModuleResponse>(
'UninstallModule', 'UninstallModule',
@ -1737,6 +1766,15 @@ abstract class HubAdminServiceBase extends $grpc.Service {
$async.Future<$0.InstallModuleResponse> installModule( $async.Future<$0.InstallModuleResponse> installModule(
$grpc.ServiceCall call, $0.InstallModuleRequest request); $grpc.ServiceCall call, $0.InstallModuleRequest request);
$async.Stream<$0.InstallProgressEvent> installModuleStream_Pre(
$grpc.ServiceCall $call,
$async.Future<$0.InstallModuleRequest> $request) async* {
yield* installModuleStream($call, await $request);
}
$async.Stream<$0.InstallProgressEvent> installModuleStream(
$grpc.ServiceCall call, $0.InstallModuleRequest request);
$async.Future<$0.UninstallModuleResponse> uninstallModule_Pre( $async.Future<$0.UninstallModuleResponse> uninstallModule_Pre(
$grpc.ServiceCall $call, $grpc.ServiceCall $call,
$async.Future<$0.UninstallModuleRequest> $request) async { $async.Future<$0.UninstallModuleRequest> $request) async {

View file

@ -998,6 +998,73 @@ final $typed_data.Uint8List installModuleResponseDescriptor = $convert.base64Dec
'IgASgJUgd2ZXJzaW9uEikKEGNhcGFiaWxpdHlfY291bnQYAyABKAVSD2NhcGFiaWxpdHlDb3Vu' 'IgASgJUgd2ZXJzaW9uEikKEGNhcGFiaWxpdHlfY291bnQYAyABKAVSD2NhcGFiaWxpdHlDb3Vu'
'dA=='); 'dA==');
@$core.Deprecated('Use installProgressEventDescriptor instead')
const InstallProgressEvent$json = {
'1': 'InstallProgressEvent',
'2': [
{
'1': 'update',
'3': 1,
'4': 1,
'5': 11,
'6': '.chain.v1.InstallProgressUpdate',
'9': 0,
'10': 'update'
},
{
'1': 'finished',
'3': 2,
'4': 1,
'5': 11,
'6': '.chain.v1.InstallModuleResponse',
'9': 0,
'10': 'finished'
},
{'1': 'failed', '3': 3, '4': 1, '5': 9, '9': 0, '10': 'failed'},
],
'8': [
{'1': 'event'},
],
};
/// Descriptor for `InstallProgressEvent`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List installProgressEventDescriptor = $convert.base64Decode(
'ChRJbnN0YWxsUHJvZ3Jlc3NFdmVudBI5CgZ1cGRhdGUYASABKAsyHy5jaGFpbi52MS5JbnN0YW'
'xsUHJvZ3Jlc3NVcGRhdGVIAFIGdXBkYXRlEj0KCGZpbmlzaGVkGAIgASgLMh8uY2hhaW4udjEu'
'SW5zdGFsbE1vZHVsZVJlc3BvbnNlSABSCGZpbmlzaGVkEhgKBmZhaWxlZBgDIAEoCUgAUgZmYW'
'lsZWRCBwoFZXZlbnQ=');
@$core.Deprecated('Use installProgressUpdateDescriptor instead')
const InstallProgressUpdate$json = {
'1': 'InstallProgressUpdate',
'2': [
{'1': 'phase', '3': 1, '4': 1, '5': 9, '10': 'phase'},
{'1': 'stage', '3': 2, '4': 1, '5': 9, '10': 'stage'},
{
'1': 'percent',
'3': 3,
'4': 1,
'5': 2,
'9': 0,
'10': 'percent',
'17': true
},
{'1': 'bytes_received', '3': 4, '4': 1, '5': 4, '10': 'bytesReceived'},
{'1': 'bytes_total', '3': 5, '4': 1, '5': 4, '10': 'bytesTotal'},
{'1': 'phase_done', '3': 6, '4': 1, '5': 8, '10': 'phaseDone'},
],
'8': [
{'1': '_percent'},
],
};
/// Descriptor for `InstallProgressUpdate`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List installProgressUpdateDescriptor = $convert.base64Decode(
'ChVJbnN0YWxsUHJvZ3Jlc3NVcGRhdGUSFAoFcGhhc2UYASABKAlSBXBoYXNlEhQKBXN0YWdlGA'
'IgASgJUgVzdGFnZRIdCgdwZXJjZW50GAMgASgCSABSB3BlcmNlbnSIAQESJQoOYnl0ZXNfcmVj'
'ZWl2ZWQYBCABKARSDWJ5dGVzUmVjZWl2ZWQSHwoLYnl0ZXNfdG90YWwYBSABKARSCmJ5dGVzVG'
'90YWwSHQoKcGhhc2VfZG9uZRgGIAEoCFIJcGhhc2VEb25lQgoKCF9wZXJjZW50');
@$core.Deprecated('Use uninstallModuleRequestDescriptor instead') @$core.Deprecated('Use uninstallModuleRequestDescriptor instead')
const UninstallModuleRequest$json = { const UninstallModuleRequest$json = {
'1': 'UninstallModuleRequest', '1': 'UninstallModuleRequest',

View file

@ -110,6 +110,7 @@ typedef ProjectInfo = pb.ProjectInfo;
typedef StoreEntry = pb.StoreEntry; typedef StoreEntry = pb.StoreEntry;
typedef StoreSearchResponse = pb.StoreSearchResponse; typedef StoreSearchResponse = pb.StoreSearchResponse;
typedef InstallModuleResponse = pb.InstallModuleResponse; typedef InstallModuleResponse = pb.InstallModuleResponse;
typedef InstallProgressUpdate = pb.InstallProgressUpdate;
typedef Payload = pb_common.Payload; typedef Payload = pb_common.Payload;
typedef SubmitResponse = pb.SubmitResponse; typedef SubmitResponse = pb.SubmitResponse;
// Live-stream (T2) + detached-invocation (T3) wire types, re-exported // Live-stream (T2) + detached-invocation (T3) wire types, re-exported
@ -831,6 +832,41 @@ class HubClient {
)); ));
} }
/// Install a module while watching it happen.
///
/// Emits an update per phase and, during the download, as bytes
/// arrive. The stream completes when the install succeeds, and
/// carries the hub's failure text as a stream error when it does
/// not, so callers handle failures the same way as with
/// [installModule]. Use [onFinished] to receive the installed
/// module's name and version.
///
/// A caller that only needs the outcome should keep using the
/// unary [installModule]; this exists so a waiting operator can
/// see that a slow download is progressing rather than hung.
Stream<InstallProgressUpdate> installModuleStreaming({
required String source,
String expectedSha256 = '',
String version = '',
void Function(InstallModuleResponse result)? onFinished,
}) async* {
final stream = _admin.installModuleStream(pb.InstallModuleRequest(
source: source,
expectedSha256: expectedSha256,
version: version,
));
await for (final event in stream) {
if (event.hasUpdate()) {
yield event.update;
} else if (event.hasFinished()) {
onFinished?.call(event.finished);
return;
} else if (event.hasFailed()) {
throw Exception(event.failed);
}
}
}
/// All saved flows known to the hub. Each entry carries the /// All saved flows known to the hub. Each entry carries the
/// flow name, on-disk path and byte size. /// flow name, on-disk path and byte size.
Future<List<FlowSummary>> listFlows() async { Future<List<FlowSummary>> listFlows() async {