diff --git a/.rsmrc b/.rsmrc index 2f75f99..2c0516e 100644 --- a/.rsmrc +++ b/.rsmrc @@ -45,10 +45,10 @@ dev:hello-constructors --env=PYTHONPATH=api/ # Tell `rsm` that this is a Python application. dev:hello-constructors --python -# Run this application! +# Run hello-constructors application! dev:hello-constructors hello-constructors/backend/src/main.py -# Watch for if any generated files are modified. +# Watch if any generated files are modified. dev:hello-world --watch=api/**/*.py # Watch if any of our source files are modified. @@ -61,7 +61,7 @@ dev:hello-world --env=PYTHONPATH=api/ dev:hello-world --python # Save state between chaos restarts. -dev:hello-world --name=helloworld +dev:hello-world --name=hello-world -# Run this application! +# Run hello-world application! dev:hello-world hello-world/backend/src/main.py diff --git a/api/google/protobuf/descriptor_rsm.py b/api/google/protobuf/descriptor_rsm.py deleted file mode 100644 index 03f9ad1..0000000 --- a/api/google/protobuf/descriptor_rsm.py +++ /dev/null @@ -1,84 +0,0 @@ -# yapf: disable -# isort: skip_file - -# Standard imports. -import grpc -import traceback -import uuid -import resemble.aio.state_managers -from abc import abstractmethod -from datetime import datetime, timedelta -from google.protobuf import any_pb2 -from google.protobuf.descriptor import FileDescriptor -from google.protobuf.message import Message -from google.rpc import code_pb2, status_pb2 -from grpc_status import rpc_status -from resemble.aio.contexts import ( - Context, - ReaderContext, - TransactionContext, - WriterContext, -) -from resemble.aio.errors import Aborted -from resemble.aio.headers import Headers -from resemble.aio.idempotency import IdempotencyManager, Idempotency -from resemble.aio.internals.channel_manager import _ChannelManager -from resemble.aio.internals.middleware import Middleware -from resemble.aio.internals.tasks_cache import TasksCache -from resemble.aio.internals.tasks_dispatcher import TasksDispatcher -from resemble.aio.headers import Headers -from resemble.aio.servicers import Servicer, Serviceable -from resemble.aio.state_managers import Effects, StateManager -from resemble.aio.stubs import Stub -from resemble.aio.tasks import TaskEffect -from resemble.aio.types import ActorId, assert_type, GrpcMetadata, ServiceName -from resemble.aio.workflows import Workflow -from typing import ( - AsyncIterable, - AsyncIterator, - Awaitable, - Callable, - Optional, - TypeAlias, - Union, -) - -# User defined or referenced imports. -import google.protobuf.descriptor_pb2 -import google.protobuf.descriptor_pb2_grpc -# Additionally re-export all messages from the pb2 module. -from google.protobuf.descriptor_pb2 import ( - DescriptorProto, - EnumDescriptorProto, - EnumOptions, - EnumValueDescriptorProto, - EnumValueOptions, - ExtensionRangeOptions, - FieldDescriptorProto, - FieldOptions, - FileDescriptorProto, - FileDescriptorSet, - FileOptions, - GeneratedCodeInfo, - MessageOptions, - MethodDescriptorProto, - MethodOptions, - OneofDescriptorProto, - OneofOptions, - ServiceDescriptorProto, - ServiceOptions, - SourceCodeInfo, - UninterpretedOption, -) - - -def MakeLegacyGrpcServiceable( - # A legacy gRPC servicer type can't be more specific than `type`, - # because legacy gRPC servicers (as generated by the gRPC `protoc` - # plugin) do not share any common base class other than `object`. - servicer_type: type -) -> Serviceable: - raise ValueError(f"Unknown legacy gRPC servicer type '{servicer_type}'") - - -# yapf: enable diff --git a/api/hello_world/v1/greeter_pb.d.ts b/api/hello_world/v1/greeter_pb.d.ts deleted file mode 100644 index fdf8be4..0000000 --- a/api/hello_world/v1/greeter_pb.d.ts +++ /dev/null @@ -1,125 +0,0 @@ -// @generated by protoc-gen-es v1.3.1 -// @generated from file hello_world/v1/greeter.proto (package hello_world.v1, syntax proto3) -/* eslint-disable */ -// @ts-nocheck - -import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; -import { Message, proto3 } from "@bufbuild/protobuf"; - -/** - * @generated from message hello_world.v1.GreeterState - */ -export declare class GreeterState extends Message { - /** - * @generated from field: repeated string greetings = 2; - */ - greetings: string[]; - - constructor(data?: PartialMessage); - - static readonly runtime: typeof proto3; - static readonly typeName = "hello_world.v1.GreeterState"; - static readonly fields: FieldList; - - static fromBinary(bytes: Uint8Array, options?: Partial): GreeterState; - - static fromJson(jsonValue: JsonValue, options?: Partial): GreeterState; - - static fromJsonString(jsonString: string, options?: Partial): GreeterState; - - static equals(a: GreeterState | PlainMessage | undefined, b: GreeterState | PlainMessage | undefined): boolean; -} - -/** - * @generated from message hello_world.v1.GreetingsRequest - */ -export declare class GreetingsRequest extends Message { - constructor(data?: PartialMessage); - - static readonly runtime: typeof proto3; - static readonly typeName = "hello_world.v1.GreetingsRequest"; - static readonly fields: FieldList; - - static fromBinary(bytes: Uint8Array, options?: Partial): GreetingsRequest; - - static fromJson(jsonValue: JsonValue, options?: Partial): GreetingsRequest; - - static fromJsonString(jsonString: string, options?: Partial): GreetingsRequest; - - static equals(a: GreetingsRequest | PlainMessage | undefined, b: GreetingsRequest | PlainMessage | undefined): boolean; -} - -/** - * @generated from message hello_world.v1.GreetingsResponse - */ -export declare class GreetingsResponse extends Message { - /** - * @generated from field: repeated string greetings = 1; - */ - greetings: string[]; - - constructor(data?: PartialMessage); - - static readonly runtime: typeof proto3; - static readonly typeName = "hello_world.v1.GreetingsResponse"; - static readonly fields: FieldList; - - static fromBinary(bytes: Uint8Array, options?: Partial): GreetingsResponse; - - static fromJson(jsonValue: JsonValue, options?: Partial): GreetingsResponse; - - static fromJsonString(jsonString: string, options?: Partial): GreetingsResponse; - - static equals(a: GreetingsResponse | PlainMessage | undefined, b: GreetingsResponse | PlainMessage | undefined): boolean; -} - -/** - * @generated from message hello_world.v1.GreetRequest - */ -export declare class GreetRequest extends Message { - /** - * E.g. "Hello, World". - * - * @generated from field: string greeting = 1; - */ - greeting: string; - - constructor(data?: PartialMessage); - - static readonly runtime: typeof proto3; - static readonly typeName = "hello_world.v1.GreetRequest"; - static readonly fields: FieldList; - - static fromBinary(bytes: Uint8Array, options?: Partial): GreetRequest; - - static fromJson(jsonValue: JsonValue, options?: Partial): GreetRequest; - - static fromJsonString(jsonString: string, options?: Partial): GreetRequest; - - static equals(a: GreetRequest | PlainMessage | undefined, b: GreetRequest | PlainMessage | undefined): boolean; -} - -/** - * @generated from message hello_world.v1.GreetResponse - */ -export declare class GreetResponse extends Message { - /** - * @generated from field: string greeting = 1; - */ - greeting: string; - - constructor(data?: PartialMessage); - - static readonly runtime: typeof proto3; - static readonly typeName = "hello_world.v1.GreetResponse"; - static readonly fields: FieldList; - - static fromBinary(bytes: Uint8Array, options?: Partial): GreetResponse; - - static fromJson(jsonValue: JsonValue, options?: Partial): GreetResponse; - - static fromJsonString(jsonString: string, options?: Partial): GreetResponse; - - static equals(a: GreetResponse | PlainMessage | undefined, b: GreetResponse | PlainMessage | undefined): boolean; -} - diff --git a/api/hello_world/v1/greeter_pb.js b/api/hello_world/v1/greeter_pb.js deleted file mode 100644 index 92da35d..0000000 --- a/api/hello_world/v1/greeter_pb.js +++ /dev/null @@ -1,55 +0,0 @@ -// @generated by protoc-gen-es v1.3.1 -// @generated from file hello_world/v1/greeter.proto (package hello_world.v1, syntax proto3) -/* eslint-disable */ -// @ts-nocheck - -import { proto3 } from "@bufbuild/protobuf"; - -/** - * @generated from message hello_world.v1.GreeterState - */ -export const GreeterState = proto3.makeMessageType( - "hello_world.v1.GreeterState", - () => [ - { no: 2, name: "greetings", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - ], -); - -/** - * @generated from message hello_world.v1.GreetingsRequest - */ -export const GreetingsRequest = proto3.makeMessageType( - "hello_world.v1.GreetingsRequest", - [], -); - -/** - * @generated from message hello_world.v1.GreetingsResponse - */ -export const GreetingsResponse = proto3.makeMessageType( - "hello_world.v1.GreetingsResponse", - () => [ - { no: 1, name: "greetings", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - ], -); - -/** - * @generated from message hello_world.v1.GreetRequest - */ -export const GreetRequest = proto3.makeMessageType( - "hello_world.v1.GreetRequest", - () => [ - { no: 1, name: "greeting", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ], -); - -/** - * @generated from message hello_world.v1.GreetResponse - */ -export const GreetResponse = proto3.makeMessageType( - "hello_world.v1.GreetResponse", - () => [ - { no: 1, name: "greeting", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ], -); - diff --git a/api/hello_world/v1/greeter_pb2.py b/api/hello_world/v1/greeter_pb2.py deleted file mode 100644 index 13ef172..0000000 --- a/api/hello_world/v1/greeter_pb2.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: hello_world/v1/greeter.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from resemble.v1alpha1 import options_pb2 as resemble_dot_v1alpha1_dot_options__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1chello_world/v1/greeter.proto\x12\x0ehello_world.v1\x1a\x1fresemble/v1alpha1/options.proto\"!\n\x0cGreeterState\x12\x11\n\tgreetings\x18\x02 \x03(\t\"\x12\n\x10GreetingsRequest\"&\n\x11GreetingsResponse\x12\x11\n\tgreetings\x18\x01 \x03(\t\" \n\x0cGreetRequest\x12\x10\n\x08greeting\x18\x01 \x01(\t\"!\n\rGreetResponse\x12\x10\n\x08greeting\x18\x01 \x01(\t2\xc5\x01\n\x07Greeter\x12X\n\tGreetings\x12 .hello_world.v1.GreetingsRequest\x1a!.hello_world.v1.GreetingsResponse\"\x06\x82\xb5\x18\x02\n\x00\x12L\n\x05Greet\x12\x1c.hello_world.v1.GreetRequest\x1a\x1d.hello_world.v1.GreetResponse\"\x06\x82\xb5\x18\x02\x12\x00\x1a\x12\x82\xb5\x18\x0e\n\x0cGreeterStateb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hello_world.v1.greeter_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - _GREETER._options = None - _GREETER._serialized_options = b'\202\265\030\016\n\014GreeterState' - _GREETER.methods_by_name['Greetings']._options = None - _GREETER.methods_by_name['Greetings']._serialized_options = b'\202\265\030\002\n\000' - _GREETER.methods_by_name['Greet']._options = None - _GREETER.methods_by_name['Greet']._serialized_options = b'\202\265\030\002\022\000' - _globals['_GREETERSTATE']._serialized_start=81 - _globals['_GREETERSTATE']._serialized_end=114 - _globals['_GREETINGSREQUEST']._serialized_start=116 - _globals['_GREETINGSREQUEST']._serialized_end=134 - _globals['_GREETINGSRESPONSE']._serialized_start=136 - _globals['_GREETINGSRESPONSE']._serialized_end=174 - _globals['_GREETREQUEST']._serialized_start=176 - _globals['_GREETREQUEST']._serialized_end=208 - _globals['_GREETRESPONSE']._serialized_start=210 - _globals['_GREETRESPONSE']._serialized_end=243 - _globals['_GREETER']._serialized_start=246 - _globals['_GREETER']._serialized_end=443 -# @@protoc_insertion_point(module_scope) diff --git a/api/hello_world/v1/greeter_pb2_grpc.py b/api/hello_world/v1/greeter_pb2_grpc.py deleted file mode 100644 index 456cc5d..0000000 --- a/api/hello_world/v1/greeter_pb2_grpc.py +++ /dev/null @@ -1,105 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc - -from hello_world.v1 import greeter_pb2 as hello__world_dot_v1_dot_greeter__pb2 - - -class GreeterStub(object): - """////////////////////////////////////////////////////////////////////// - - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Greetings = channel.unary_unary( - '/hello_world.v1.Greeter/Greetings', - request_serializer=hello__world_dot_v1_dot_greeter__pb2.GreetingsRequest.SerializeToString, - response_deserializer=hello__world_dot_v1_dot_greeter__pb2.GreetingsResponse.FromString, - ) - self.Greet = channel.unary_unary( - '/hello_world.v1.Greeter/Greet', - request_serializer=hello__world_dot_v1_dot_greeter__pb2.GreetRequest.SerializeToString, - response_deserializer=hello__world_dot_v1_dot_greeter__pb2.GreetResponse.FromString, - ) - - -class GreeterServicer(object): - """////////////////////////////////////////////////////////////////////// - - """ - - def Greetings(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Greet(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_GreeterServicer_to_server(servicer, server): - rpc_method_handlers = { - 'Greetings': grpc.unary_unary_rpc_method_handler( - servicer.Greetings, - request_deserializer=hello__world_dot_v1_dot_greeter__pb2.GreetingsRequest.FromString, - response_serializer=hello__world_dot_v1_dot_greeter__pb2.GreetingsResponse.SerializeToString, - ), - 'Greet': grpc.unary_unary_rpc_method_handler( - servicer.Greet, - request_deserializer=hello__world_dot_v1_dot_greeter__pb2.GreetRequest.FromString, - response_serializer=hello__world_dot_v1_dot_greeter__pb2.GreetResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'hello_world.v1.Greeter', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - # This class is part of an EXPERIMENTAL API. -class Greeter(object): - """////////////////////////////////////////////////////////////////////// - - """ - - @staticmethod - def Greetings(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary(request, target, '/hello_world.v1.Greeter/Greetings', - hello__world_dot_v1_dot_greeter__pb2.GreetingsRequest.SerializeToString, - hello__world_dot_v1_dot_greeter__pb2.GreetingsResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) - - @staticmethod - def Greet(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary(request, target, '/hello_world.v1.Greeter/Greet', - hello__world_dot_v1_dot_greeter__pb2.GreetRequest.SerializeToString, - hello__world_dot_v1_dot_greeter__pb2.GreetResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/api/hello_world/v1/greeter_rsm.py b/api/hello_world/v1/greeter_rsm.py deleted file mode 100644 index 67d1ae3..0000000 --- a/api/hello_world/v1/greeter_rsm.py +++ /dev/null @@ -1,789 +0,0 @@ -# yapf: disable -# isort: skip_file - -# Standard imports. -import grpc -import traceback -import uuid -import resemble.aio.state_managers -from abc import abstractmethod -from datetime import datetime, timedelta -from google.protobuf import any_pb2 -from google.protobuf.descriptor import FileDescriptor -from google.protobuf.message import Message -from google.rpc import code_pb2, status_pb2 -from grpc_status import rpc_status -from resemble.aio.contexts import ( - Context, - ReaderContext, - TransactionContext, - WriterContext, -) -from resemble.aio.errors import Aborted -from resemble.aio.headers import Headers -from resemble.aio.idempotency import IdempotencyManager, Idempotency -from resemble.aio.internals.channel_manager import _ChannelManager -from resemble.aio.internals.middleware import Middleware -from resemble.aio.internals.tasks_cache import TasksCache -from resemble.aio.internals.tasks_dispatcher import TasksDispatcher -from resemble.aio.headers import Headers -from resemble.aio.servicers import Servicer, Serviceable -from resemble.aio.state_managers import Effects, StateManager -from resemble.aio.stubs import Stub -from resemble.aio.tasks import TaskEffect -from resemble.aio.types import ActorId, assert_type, GrpcMetadata, ServiceName -from resemble.aio.workflows import Workflow -from typing import ( - AsyncIterable, - AsyncIterator, - Awaitable, - Callable, - Optional, - TypeAlias, - Union, -) - -# User defined or referenced imports. -import hello_world.v1.greeter_pb2 -import hello_world.v1.greeter_pb2_grpc -import resemble.v1alpha1.options_pb2 -# Additionally re-export all messages from the pb2 module. -from hello_world.v1.greeter_pb2 import ( - GreeterState, - GreetingsRequest, - GreetingsResponse, - GreetRequest, - GreetResponse, -) - - -def MakeLegacyGrpcServiceable( - # A legacy gRPC servicer type can't be more specific than `type`, - # because legacy gRPC servicers (as generated by the gRPC `protoc` - # plugin) do not share any common base class other than `object`. - servicer_type: type -) -> Serviceable: - raise ValueError(f"Unknown legacy gRPC servicer type '{servicer_type}'") - - -class GreeterServicerMiddleware(Middleware): - - def __init__( - self, - servicer: 'GreeterServicer', - state_manager: StateManager, - channel_manager: _ChannelManager, - tasks_cache: TasksCache, - ): - super().__init__( - channel_manager=channel_manager, - service_name='hello_world.v1.Greeter', - ) - - self._servicer = servicer - self._state_manager = state_manager - self.tasks_dispatcher = TasksDispatcher(self.dispatch, tasks_cache) - - # Store the type of each method's request so that stored requests can be - # deserialized into the correct type. - self.request_type_by_method_name: dict[str, type[Message]] = { - 'Greetings': hello_world.v1.greeter_pb2.GreetingsRequest, - 'Greet': hello_world.v1.greeter_pb2.GreetRequest, - } - - def add_to_server(self, server: grpc.aio.Server) -> None: - hello_world.v1.greeter_pb2_grpc.add_GreeterServicer_to_server( - self, server - ) - - async def react( - self, actor_id: ActorId, method: str, request_bytes: bytes - ) -> AsyncIterator[Message]: - """Returns the response of calling 'method' given a message - deserialized from the provided 'request_bytes' for each state - update that creates a different response. - - NOTE: only unary reader methods are supported.""" - # Need to define these up here since we can only do that once. - last_response: Optional[Message] = None - idempotency_keys: list[uuid.UUID] = [] - if 'Greetings' == method: - context = self.create_context( - headers=Headers( - service_name='hello_world.v1.Greeter', - actor_id=actor_id, - ), - context_type=ReaderContext, - ) - - request = hello_world.v1.greeter_pb2.GreetingsRequest() - request.ParseFromString(request_bytes) - - async with self._state_manager.streaming_reader_idempotency_key( - context, self._servicer.__state_type__ - ) as states: - async for (state, idempotency_key) in states: - response = await self._servicer.Greetings( - context, state, request - ) - if idempotency_key is not None: - idempotency_keys.append(idempotency_key) - if last_response != response: - yield (response, idempotency_keys) - last_response = response - idempotency_keys.clear() - elif 'Greet' == method: - # Invariant here is that users should not have called this - # directly but only through code generated React - # components which should not have been generated except - # for valid method candidates. - raise ValueError(f"Method '{method}' is invalid") - yield # Necessary for type checking. - else: - raise RuntimeError(f"Method '{method}' not found") - yield # Necessary for type checking. - - async def dispatch( - self, task: TaskEffect, *, only_validate: bool = False - ) -> Message: - """Dispatches the tasks to execute unless 'only_validate' is set to - true, in which case just ensures that the task actually exists. - Note that this function will be called *by* tasks_dispatcher; it will - not itself call into tasks_dispatcher.""" - # Need to forward declare context since we set it to different - # types depending on if the task is a reader or a writer. - context: Context - - - # There are no tasks for this service. - start_or_validate = "start" if not only_validate else "validate" - raise RuntimeError( - f"Attempted to {start_or_validate} task '{task.method_name}' " - f"on 'Greeter' which does not exist" - ) - - # Greeter specific methods: - async def Greetings( - self, - request: hello_world.v1.greeter_pb2.GreetingsRequest, - grpc_context: grpc.aio.ServicerContext, - ) -> hello_world.v1.greeter_pb2.GreetingsResponse: - - # Need to define 'transaction' in case we fail to parse headers, return - # early after checking idempotency, or if 'transactionally()' raises. - transaction: Optional[StateManager.Transaction] = None - - try: - context: ReaderContext = self.create_context_from_grpc( - grpc_context=grpc_context, context_type=ReaderContext - ) - - - async with self._state_manager.transactionally( - context, self.tasks_dispatcher - ) as transaction: - if transaction is not None: - context.participants.add( - self._servicer.__service_name__, context.actor_id - ) - - async with self._state_manager.reader( - context, self._servicer.__state_type__ - ) as state: - response = await self._servicer.Greetings( - context, - state, - request - ) - if transaction is not None: - # We need the transaction to be stored _before_ - # returning a response to the user otherwise they - # may read two different states if we crash. - await self._state_manager.transaction_participant_store( - transaction - ) - return response - except BaseException as exception: - # Print the exception stack trace for easier debugging. Note - # that we don't include the stack trace in an error message - # for the same reason that gRPC doesn't do so by default, - # see https://github.com/grpc/grpc/issues/14897, but since this - # should only get logged on the server side it is safe. - traceback.print_exc() - - # Re-raise the exception for gRPC to handle! - raise - finally: - if transaction is not None: - # Propagate transaction participants. - # - # NOTE: we'll want to propagate participants if we're - # in a transaction method once we support nested - # transactions, but for now we only need to propagate - # participants when we're not in a transaction method. - if not isinstance(context, TransactionContext): - grpc_context.set_trailing_metadata( - grpc_context.trailing_metadata() + - context.participants.to_grpc_metadata() - ) - - async def Greet( - self, - request: hello_world.v1.greeter_pb2.GreetRequest, - grpc_context: grpc.aio.ServicerContext, - ) -> hello_world.v1.greeter_pb2.GreetResponse: - - # Need to define 'transaction' in case we fail to parse headers, return - # early after checking idempotency, or if 'transactionally()' raises. - transaction: Optional[StateManager.Transaction] = None - - try: - context: WriterContext = self.create_context_from_grpc( - grpc_context=grpc_context, context_type=WriterContext - ) - - # Check if we already have performed this mutation! - # - # We do this _before_ calling 'transactionally()' because - # if this call is for a transaction method _and_ we've - # already performed the transaction then we don't want to - # become a transaction participant (again) we just want to - # return the transaction's response. - response_bytes: Optional[ - bytes - ] = self._state_manager.get_response_if_idempotent_mutation( - context - ) - - if response_bytes is not None: - response = hello_world.v1.greeter_pb2.GreetResponse() - response.ParseFromString(response_bytes) - return response - - async with self._state_manager.transactionally( - context, self.tasks_dispatcher - ) as transaction: - if transaction is not None: - context.participants.add( - self._servicer.__service_name__, context.actor_id - ) - - # TODO: this loads a `state` object even when we know we're calling a constructor, - # in which case (by definition) there is no state. Can we (and is it worth - # the effort to) change this call to avoid that overhead in that case? - async with self._state_manager.writer( - context, - self._servicer.__state_type__, - transaction=transaction, - from_constructor=False, - requires_constructor=False - ) as (state, writer): - effects = await self._servicer.Greet( - context, - state, - request - ) - - if effects.tasks is not None: - # NOTE: we validate tasks added as part of a - # transaction when we prepare. - if transaction is None: - await self.tasks_dispatcher.validate( - effects.tasks - ) - - await writer.complete(effects) - - if effects.tasks is not None: - if transaction is None: - self.tasks_dispatcher.dispatch(effects.tasks) - else: - assert all( - task.task_id.service == transaction.service - for task in effects.tasks - ), 'Task service does not match transaction service' - transaction.tasks.extend(effects.tasks) - - return effects.response - except BaseException as exception: - # Print the exception stack trace for easier debugging. Note - # that we don't include the stack trace in an error message - # for the same reason that gRPC doesn't do so by default, - # see https://github.com/grpc/grpc/issues/14897, but since this - # should only get logged on the server side it is safe. - traceback.print_exc() - - # Re-raise the exception for gRPC to handle! - raise - finally: - if transaction is not None: - # Propagate transaction participants. - # - # NOTE: we'll want to propagate participants if we're - # in a transaction method once we support nested - # transactions, but for now we only need to propagate - # participants when we're not in a transaction method. - if not isinstance(context, TransactionContext): - grpc_context.set_trailing_metadata( - grpc_context.trailing_metadata() + - context.participants.to_grpc_metadata() - ) - - -class _GreeterStub(Stub): - - def __init__( - self, - context_or_workflow: Context | Workflow, - actor_id: ActorId, - ): - super().__init__( - channel_manager=context_or_workflow.channel_manager, - idempotency_manager=context_or_workflow, - service_name='hello_world.v1.Greeter', - actor_id=actor_id, - context=( - context_or_workflow - if issubclass(type(context_or_workflow), Context) - else None # type: ignore - ), - ) - - channel = self._channel_manager.get_channel_for( - GreeterServicer, actor_id - ) - self._stub = hello_world.v1.greeter_pb2_grpc.GreeterStub(channel) - - -class GreeterReaderStub(_GreeterStub): - - def __init__( - self, - context_or_workflow: ReaderContext | WriterContext | TransactionContext | Workflow, - actor_id: ActorId - ): - assert_type( - context_or_workflow, - [ReaderContext, WriterContext, TransactionContext, Workflow] - ) - super().__init__(context_or_workflow, actor_id) - - # Greeter specific methods: - async def Greetings( - self, - request: hello_world.v1.greeter_pb2.GreetingsRequest, - *, - metadata: Optional[GrpcMetadata] = None, - ) -> hello_world.v1.greeter_pb2.GreetingsResponse: - async with self._call( - self._stub.Greetings, - request, - metadata=metadata, - ) as call: - return await call - - - -class GreeterWriterStub(_GreeterStub): - - def __init__( - self, - context_or_workflow: TransactionContext | Workflow, - actor_id: ActorId - ): - assert_type(context_or_workflow, [TransactionContext, Workflow]) - super().__init__(context_or_workflow, actor_id) - - # Greeter specific methods: - async def Greetings( - self, - request: hello_world.v1.greeter_pb2.GreetingsRequest, - *, - metadata: Optional[GrpcMetadata] = None, - ) -> hello_world.v1.greeter_pb2.GreetingsResponse: - async with self._call( - self._stub.Greetings, - request, - metadata=metadata, - ) as call: - return await call - - async def Greet( - self, - request: hello_world.v1.greeter_pb2.GreetRequest, - idempotency: Optional[Idempotency] = None, - *, - metadata: Optional[GrpcMetadata] = None, - ) -> hello_world.v1.greeter_pb2.GreetResponse: - idempotency_key: Optional[str] - with self._idempotency_manager.idempotently( - service=self._headers.service_name, - actor_id=self._headers.actor_id, - method='Greet', - request=request, - metadata=metadata, - idempotency=idempotency, - ) as idempotency_key: - async with self._call( - self._stub.Greet, - request, - idempotency_key=idempotency_key, - metadata=metadata, - ) as call: - return await call - - -class GreeterTransactionStub(_GreeterStub): - - def __init__( - self, - context_or_workflow: TransactionContext | Workflow, - actor_id: ActorId - ): - assert_type(context_or_workflow, [TransactionContext, Workflow]) - super().__init__(context_or_workflow, actor_id) - - # Greeter specific methods: - async def Greetings( - self, - request: hello_world.v1.greeter_pb2.GreetingsRequest, - *, - metadata: Optional[GrpcMetadata] = None, - ) -> hello_world.v1.greeter_pb2.GreetingsResponse: - async with self._call( - self._stub.Greetings, - request, - metadata=metadata, - ) as call: - return await call - - async def Greet( - self, - request: hello_world.v1.greeter_pb2.GreetRequest, - idempotency: Optional[Idempotency] = None, - *, - metadata: Optional[GrpcMetadata] = None, - ) -> hello_world.v1.greeter_pb2.GreetResponse: - idempotency_key: Optional[str] - with self._idempotency_manager.idempotently( - service=self._headers.service_name, - actor_id=self._headers.actor_id, - method='Greet', - request=request, - metadata=metadata, - idempotency=idempotency, - ) as idempotency_key: - async with self._call( - self._stub.Greet, - request, - idempotency_key=idempotency_key, - metadata=metadata, - ) as call: - return await call - - -class GreeterWorkflowStub(_GreeterStub): - - def __init__( - self, - workflow: Workflow, - actor_id: ActorId - ): - assert_type(workflow, [Workflow]) - super().__init__(workflow, actor_id) - - # Greeter specific methods: - async def Greetings( - self, - request: hello_world.v1.greeter_pb2.GreetingsRequest, - *, - metadata: Optional[GrpcMetadata] = None, - ) -> hello_world.v1.greeter_pb2.GreetingsResponse: - async with self._call( - self._stub.Greetings, - request, - metadata=metadata, - ) as call: - return await call - - async def Greet( - self, - request: hello_world.v1.greeter_pb2.GreetRequest, - idempotency: Optional[Idempotency] = None, - *, - metadata: Optional[GrpcMetadata] = None, - ) -> hello_world.v1.greeter_pb2.GreetResponse: - idempotency_key: Optional[str] - with self._idempotency_manager.idempotently( - service=self._headers.service_name, - actor_id=self._headers.actor_id, - method='Greet', - request=request, - metadata=metadata, - idempotency=idempotency, - ) as idempotency_key: - async with self._call( - self._stub.Greet, - request, - idempotency_key=idempotency_key, - metadata=metadata, - ) as call: - return await call - - -class GreeterTasksStub(_GreeterStub): - - def __init__(self, context: WriterContext, actor_id: ActorId): - assert_type(context, [WriterContext]) - super().__init__(context, actor_id) - - # Greeter specific methods: - - - -class GreeterServicer(Servicer): - __service_name__ = 'hello_world.v1.Greeter' - __file_descriptor__ = hello_world.v1.greeter_pb2.DESCRIPTOR - __state_type__: type[Message] = hello_world.v1.greeter_pb2.GreeterState - - def __init__(self): - # NOTE: need to hold on to the middleware so we can do inline - # writes (see 'self.write(...)'). - # - # Because '_middleware' is not really private this does mean - # users may do possibly dangerous things, but this is no more - # likely given they could have already overridden - # 'create_middleware()'. - self._middleware: Optional[GreeterServicerMiddleware] = None - - def create_middleware( - self, - state_manager: StateManager, - channel_manager: _ChannelManager, - tasks_cache: TasksCache, - ) -> GreeterServicerMiddleware: - self._middleware = GreeterServicerMiddleware( - self, state_manager, channel_manager, tasks_cache - ) - return self._middleware - - def instance(self, context: Context): - """Returns an instance of the service for the specified context.""" - # TODO(benh): improve this to not require passing 'context', - # e.g., by storing 'context' in a contextvar. - return Greeter(context.actor_id) - - class Tasks: - def __init__(self, when: Optional[datetime | timedelta] = None): - self._when = when - - # Greeter specific methods: - - def schedule( - self, - when: Optional[datetime | timedelta] = None, - ): - return self.Tasks(when) - - class Effects(resemble.aio.state_managers.Effects): - def __init__( - self, - *, - state: hello_world.v1.greeter_pb2.GreeterState, - response: Optional[Message] = None, - tasks: Optional[list[TaskEffect]] = None - ): - assert_type(state, [hello_world.v1.greeter_pb2.GreeterState]) - - super().__init__(state=state, response=response, tasks=tasks) - - - class GreetEffects(Effects): - def __init__( - self, - *, - state: hello_world.v1.greeter_pb2.GreeterState, - response: hello_world.v1.greeter_pb2.GreetResponse, - tasks: Optional[list[TaskEffect]] = None - ): - assert_type(state, [hello_world.v1.greeter_pb2.GreeterState]) - assert_type(response, [hello_world.v1.greeter_pb2.GreetResponse]) - - super().__init__(state=state, response=response, tasks=tasks) - - - - async def read( - self, context: TransactionContext - ) -> hello_world.v1.greeter_pb2.GreeterState: - """Read the current state within a transaction.""" - assert_type(context, [TransactionContext]) - - if self._middleware is None: - raise RuntimeError( - 'Resemble middleware was not created; ' - 'are you using this class without Resemble?' - ) - - return await self._middleware._state_manager.read( - context, self.__state_type__ - ) - - async def write( - self, - context: TransactionContext, - f: Callable[[WriterContext, Message], Awaitable[Effects]], - ): - """Perform an "inline write" within a transaction.""" - assert_type(context, [TransactionContext]) - - if self._middleware is None: - raise RuntimeError( - 'Resemble middleware was not created; ' - 'are you using this class without Resemble?' - ) - - writer_context: WriterContext = self._middleware.create_context( - headers=Headers( - service_name=self.__service_name__, - actor_id=context.actor_id, - transaction_id=context.transaction_id, - transaction_coordinator_service=context - .transaction_coordinator_service, - transaction_coordinator_actor_id=context - .transaction_coordinator_actor_id - ), - context_type=WriterContext, - ) - - async with self._middleware._state_manager.transactionally( - writer_context, self._middleware.tasks_dispatcher - ) as transaction: - assert transaction is not None - async with self._middleware._state_manager.writer( - writer_context, self.__state_type__, transaction=transaction - ) as (state, writer): - effects: GreeterServicer.Effects = await f( - writer_context, state - ) - - assert_type(effects, [GreeterServicer.Effects]) - - # NOTE: we validate tasks added as part of a - # transaction when we prepare. - - await writer.complete(effects) - - if effects.tasks is not None: - assert all( - task.task_id.service == transaction.service - for task in effects.tasks - ), 'Task service does not match transaction service' - transaction.tasks.extend(effects.tasks) - - return effects.response # May be 'None'. - - # Greeter specific methods: - @abstractmethod - async def Greetings( - self, - context: ReaderContext, - state: hello_world.v1.greeter_pb2.GreeterState, - request: hello_world.v1.greeter_pb2.GreetingsRequest, - ) -> hello_world.v1.greeter_pb2.GreetingsResponse: - raise NotImplementedError - - @abstractmethod - async def Greet( - self, - context: WriterContext, - state: hello_world.v1.greeter_pb2.GreeterState, - request: hello_world.v1.greeter_pb2.GreetRequest, - ) -> GreetEffects: - raise NotImplementedError - - -class Greeter: - - Interface: TypeAlias = GreeterServicer - - Effects: TypeAlias = GreeterServicer.Effects - - - GreetEffects: TypeAlias = Interface.GreetEffects - - - def __init__(self, id: str): - self._id = id - self._reader_stub: Optional[GreeterReaderStub] = None - self._writer_stub: Optional[GreeterWriterStub] = None - self._workflow_stub: Optional[GreeterWorkflowStub] = None - - def reader( - self, - context: ReaderContext | WriterContext | TransactionContext | Workflow, - ) -> GreeterReaderStub: - if self._reader_stub is None: - self._reader_stub = GreeterReaderStub( - context, self._id - ) - assert self._reader_stub is not None - assert self._reader_stub._idempotency_manager == context - return self._reader_stub - - def writer( - self, - context: TransactionContext | Workflow, - ) -> GreeterWriterStub: - if self._writer_stub is None: - self._writer_stub = GreeterWriterStub( - context, self._id - ) - assert self._writer_stub is not None - assert self._writer_stub._idempotency_manager == context - return self._writer_stub - - def workflow( - self, - workflow: Workflow, - ) -> GreeterWorkflowStub: - if self._workflow_stub is None: - self._workflow_stub = GreeterWorkflowStub( - workflow, self._id - ) - assert self._workflow_stub is not None - assert self._workflow_stub._idempotency_manager == workflow - return self._workflow_stub - - # Greeter specific methods: - async def Greetings( - self, - context: ReaderContext | WriterContext | TransactionContext | Workflow, - rsm_metadata: Optional[GrpcMetadata] = None, - ) -> hello_world.v1.greeter_pb2.GreetingsResponse: - request = hello_world.v1.greeter_pb2.GreetingsRequest( - ) - return await self.reader(context).Greetings( - request, - metadata=rsm_metadata, - ) - - async def Greet( - self, - rsm_context_or_workflow: TransactionContext | Workflow, - rsm_idempotency: Optional[Idempotency] = None, - rsm_metadata: Optional[GrpcMetadata] = None, - *, - greeting: Optional[str] = None, - ) -> hello_world.v1.greeter_pb2.GreetResponse: - request = hello_world.v1.greeter_pb2.GreetRequest( - greeting=greeting, - ) - return await self.writer(rsm_context_or_workflow).Greet( - request, - rsm_idempotency, - metadata=rsm_metadata, - ) - - -# yapf: enable diff --git a/api/resemble/v1alpha1/options_rsm.py b/api/resemble/v1alpha1/options_rsm.py deleted file mode 100644 index 917f363..0000000 --- a/api/resemble/v1alpha1/options_rsm.py +++ /dev/null @@ -1,69 +0,0 @@ -# yapf: disable -# isort: skip_file - -# Standard imports. -import grpc -import traceback -import uuid -import resemble.aio.state_managers -from abc import abstractmethod -from datetime import datetime, timedelta -from google.protobuf import any_pb2 -from google.protobuf.descriptor import FileDescriptor -from google.protobuf.message import Message -from google.rpc import code_pb2, status_pb2 -from grpc_status import rpc_status -from resemble.aio.contexts import ( - Context, - ReaderContext, - TransactionContext, - WriterContext, -) -from resemble.aio.errors import Aborted -from resemble.aio.headers import Headers -from resemble.aio.idempotency import IdempotencyManager, Idempotency -from resemble.aio.internals.channel_manager import _ChannelManager -from resemble.aio.internals.middleware import Middleware -from resemble.aio.internals.tasks_cache import TasksCache -from resemble.aio.internals.tasks_dispatcher import TasksDispatcher -from resemble.aio.headers import Headers -from resemble.aio.servicers import Servicer, Serviceable -from resemble.aio.state_managers import Effects, StateManager -from resemble.aio.stubs import Stub -from resemble.aio.tasks import TaskEffect -from resemble.aio.types import ActorId, assert_type, GrpcMetadata, ServiceName -from resemble.aio.workflows import Workflow -from typing import ( - AsyncIterable, - AsyncIterator, - Awaitable, - Callable, - Optional, - TypeAlias, - Union, -) - -# User defined or referenced imports. -import google.protobuf.descriptor_pb2 -import resemble.v1alpha1.options_pb2 -import resemble.v1alpha1.options_pb2_grpc -# Additionally re-export all messages from the pb2 module. -from resemble.v1alpha1.options_pb2 import ( - MethodOptions, - ReaderMethodOptions, - ServiceOptions, - TransactionMethodOptions, - WriterMethodOptions, -) - - -def MakeLegacyGrpcServiceable( - # A legacy gRPC servicer type can't be more specific than `type`, - # because legacy gRPC servicers (as generated by the gRPC `protoc` - # plugin) do not share any common base class other than `object`. - servicer_type: type -) -> Serviceable: - raise ValueError(f"Unknown legacy gRPC servicer type '{servicer_type}'") - - -# yapf: enable diff --git a/hello-constructors/backend/src/requirements.txt b/hello-constructors/backend/src/requirements.txt index e9123dc..be7c1d6 100644 --- a/hello-constructors/backend/src/requirements.txt +++ b/hello-constructors/backend/src/requirements.txt @@ -1,2 +1,2 @@ -reboot-resemble>=0.0.5 +reboot-resemble>=0.0.6 pytest>=7.4.2 diff --git a/hello-world/backend/src/requirements.txt b/hello-world/backend/src/requirements.txt index e9123dc..be7c1d6 100644 --- a/hello-world/backend/src/requirements.txt +++ b/hello-world/backend/src/requirements.txt @@ -1,2 +1,2 @@ -reboot-resemble>=0.0.5 +reboot-resemble>=0.0.6 pytest>=7.4.2 diff --git a/hello-world/web/config-overrides.js b/hello-world/web/config-overrides.js new file mode 100644 index 0000000..313bfaf --- /dev/null +++ b/hello-world/web/config-overrides.js @@ -0,0 +1,16 @@ +const { removeModuleScopePlugin, override, babelInclude } = require("customize-cra"); +const path = require("path"); + +module.exports = function (config, env) { + + return Object.assign( + config, + override( + removeModuleScopePlugin(), + babelInclude([ + path.resolve('src'), + path.resolve('../../api/hello_world/v1'), + ]) + )(config, env) + ) + } diff --git a/hello-world/web/package-lock.json b/hello-world/web/package-lock.json index e9adc30..5a66acb 100644 --- a/hello-world/web/package-lock.json +++ b/hello-world/web/package-lock.json @@ -21,6 +21,10 @@ "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "customize-cra": "^1.0.0", + "react-app-rewired": "^2.2.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -6493,6 +6497,15 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/customize-cra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/customize-cra/-/customize-cra-1.0.0.tgz", + "integrity": "sha512-DbtaLuy59224U+xCiukkxSq8clq++MOtJ1Et7LED1fLszWe88EoblEYFBJ895sB1mC6B4uu3xPT/IjClELhMbA==", + "dev": true, + "dependencies": { + "lodash.flow": "^3.5.0" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -11962,6 +11975,12 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.flow": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", + "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -14421,6 +14440,30 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-app-rewired": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.2.1.tgz", + "integrity": "sha512-uFQWTErXeLDrMzOJHKp0h8P1z0LV9HzPGsJ6adOtGlA/B9WfT6Shh4j2tLTTGlXOfiVx6w6iWpp7SOC5pvk+gA==", + "dev": true, + "dependencies": { + "semver": "^5.6.0" + }, + "bin": { + "react-app-rewired": "bin/index.js" + }, + "peerDependencies": { + "react-scripts": ">=2.1.3" + } + }, + "node_modules/react-app-rewired/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -22281,6 +22324,15 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "customize-cra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/customize-cra/-/customize-cra-1.0.0.tgz", + "integrity": "sha512-DbtaLuy59224U+xCiukkxSq8clq++MOtJ1Et7LED1fLszWe88EoblEYFBJ895sB1mC6B4uu3xPT/IjClELhMbA==", + "dev": true, + "requires": { + "lodash.flow": "^3.5.0" + } + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -26247,6 +26299,12 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "lodash.flow": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", + "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -27848,6 +27906,23 @@ } } }, + "react-app-rewired": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.2.1.tgz", + "integrity": "sha512-uFQWTErXeLDrMzOJHKp0h8P1z0LV9HzPGsJ6adOtGlA/B9WfT6Shh4j2tLTTGlXOfiVx6w6iWpp7SOC5pvk+gA==", + "dev": true, + "requires": { + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, "react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", diff --git a/hello-world/web/package.json b/hello-world/web/package.json index e3f1e09..136ec54 100644 --- a/hello-world/web/package.json +++ b/hello-world/web/package.json @@ -18,10 +18,10 @@ "web-vitals": "^2.1.4" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" + "start": "react-app-rewired start", + "build": "react-app-rewired build", + "test": "react-app-rewired test", + "eject": "react-app-rewired eject" }, "eslintConfig": { "extends": [ @@ -40,5 +40,9 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "customize-cra": "^1.0.0", + "react-app-rewired": "^2.2.1" } } diff --git a/hello-world/web/src/App.module.css b/hello-world/web/src/App.module.css index 960074c..712ad49 100644 --- a/hello-world/web/src/App.module.css +++ b/hello-world/web/src/App.module.css @@ -1,44 +1,46 @@ /* Styles by @sowg: https://codepen.io/sowg/pen/qBXexaZ */ .greetings { - width: 255px; - margin: 0 auto; + width: 255px; + margin: 0 auto; } .textInput { - margin: 1em; - appearance: none; - border: none; - outline: none; - border-bottom: .2em solid #E91E63; - background: rgba(#E91E63, .2); - border-radius: .2em .2em 0 0; - padding: .4em; - color: #E91E63; + margin: 1em; + appearance: none; + border: none; + outline: none; + border-bottom: 0.2em solid #e91e63; + background: rgba(#e91e63, 0.2); + border-radius: 0.2em 0.2em 0 0; + padding: 0.4em; + color: #e91e63; } .greeting { - appearance: none; - border: .2em solid #E91E63; - background: hsl(0 0 0/0); - padding: .85em 1.5em; - color: #E91E63; - border-radius: 2em; - transition: 1s; - margin: 1em 0; - font-family: sans-serif; + appearance: none; + border: 0.2em solid #e91e63; + background: hsl(0 0 0/0); + padding: 0.85em 1.5em; + color: #e91e63; + border-radius: 2em; + transition: 1s; + margin: 1em 0; + font-family: sans-serif; } .button { - appearance: none; - border: .2em solid #E91E63; - background: hsl(0 0 0/0); - padding: .85em 1.5em; - color: #E91E63; - border-radius: 2em; - transition: 1s; - &:hover, &:focus, &:active { - background: #E91E63; - color: #fff; - } + appearance: none; + border: 0.2em solid #e91e63; + background: hsl(0 0 0/0); + padding: 0.85em 1.5em; + color: #e91e63; + border-radius: 2em; + transition: 1s; + &:hover, + &:focus, + &:active { + background: #e91e63; + color: #fff; + } } diff --git a/hello-world/web/src/App.tsx b/hello-world/web/src/App.tsx index 9a9a1e7..63e3a5a 100644 --- a/hello-world/web/src/App.tsx +++ b/hello-world/web/src/App.tsx @@ -1,7 +1,6 @@ import { FC, useState } from "react"; +import { Greeter } from "../../../api/hello_world/v1/greeter_rsm"; import css from "./App.module.css"; -import { Greeter } from "./api/hello_world/v1/greeter_rsm"; - // We can choose any id we want because the state will be constructed when we // make the first .writer call. const GREETER_ID = "greeter-hello-world"; @@ -14,8 +13,11 @@ const App = () => { // State of the input component. const [greetingMessage, setGreetingMessage] = useState(""); - const { useGreetings } = Greeter({ actorId: GREETER_ID });; - const { response, mutations: { Greet } } = useGreetings(); + const { useGreetings } = Greeter({ actorId: GREETER_ID }); + const { + response, + mutations: { Greet }, + } = useGreetings(); return (
@@ -25,13 +27,16 @@ const App = () => { className={css.textInput} onChange={(e) => setGreetingMessage(e.target.value)} /> - {response !== undefined && response.greetings.length > 0 && response.greetings.map((greeting: string) => ( - + ))}
); diff --git a/hello-world/web/src/api/hello_world/v1/greeter_pb.d.ts b/hello-world/web/src/api/hello_world/v1/greeter_pb.d.ts deleted file mode 100644 index fdf8be4..0000000 --- a/hello-world/web/src/api/hello_world/v1/greeter_pb.d.ts +++ /dev/null @@ -1,125 +0,0 @@ -// @generated by protoc-gen-es v1.3.1 -// @generated from file hello_world/v1/greeter.proto (package hello_world.v1, syntax proto3) -/* eslint-disable */ -// @ts-nocheck - -import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; -import { Message, proto3 } from "@bufbuild/protobuf"; - -/** - * @generated from message hello_world.v1.GreeterState - */ -export declare class GreeterState extends Message { - /** - * @generated from field: repeated string greetings = 2; - */ - greetings: string[]; - - constructor(data?: PartialMessage); - - static readonly runtime: typeof proto3; - static readonly typeName = "hello_world.v1.GreeterState"; - static readonly fields: FieldList; - - static fromBinary(bytes: Uint8Array, options?: Partial): GreeterState; - - static fromJson(jsonValue: JsonValue, options?: Partial): GreeterState; - - static fromJsonString(jsonString: string, options?: Partial): GreeterState; - - static equals(a: GreeterState | PlainMessage | undefined, b: GreeterState | PlainMessage | undefined): boolean; -} - -/** - * @generated from message hello_world.v1.GreetingsRequest - */ -export declare class GreetingsRequest extends Message { - constructor(data?: PartialMessage); - - static readonly runtime: typeof proto3; - static readonly typeName = "hello_world.v1.GreetingsRequest"; - static readonly fields: FieldList; - - static fromBinary(bytes: Uint8Array, options?: Partial): GreetingsRequest; - - static fromJson(jsonValue: JsonValue, options?: Partial): GreetingsRequest; - - static fromJsonString(jsonString: string, options?: Partial): GreetingsRequest; - - static equals(a: GreetingsRequest | PlainMessage | undefined, b: GreetingsRequest | PlainMessage | undefined): boolean; -} - -/** - * @generated from message hello_world.v1.GreetingsResponse - */ -export declare class GreetingsResponse extends Message { - /** - * @generated from field: repeated string greetings = 1; - */ - greetings: string[]; - - constructor(data?: PartialMessage); - - static readonly runtime: typeof proto3; - static readonly typeName = "hello_world.v1.GreetingsResponse"; - static readonly fields: FieldList; - - static fromBinary(bytes: Uint8Array, options?: Partial): GreetingsResponse; - - static fromJson(jsonValue: JsonValue, options?: Partial): GreetingsResponse; - - static fromJsonString(jsonString: string, options?: Partial): GreetingsResponse; - - static equals(a: GreetingsResponse | PlainMessage | undefined, b: GreetingsResponse | PlainMessage | undefined): boolean; -} - -/** - * @generated from message hello_world.v1.GreetRequest - */ -export declare class GreetRequest extends Message { - /** - * E.g. "Hello, World". - * - * @generated from field: string greeting = 1; - */ - greeting: string; - - constructor(data?: PartialMessage); - - static readonly runtime: typeof proto3; - static readonly typeName = "hello_world.v1.GreetRequest"; - static readonly fields: FieldList; - - static fromBinary(bytes: Uint8Array, options?: Partial): GreetRequest; - - static fromJson(jsonValue: JsonValue, options?: Partial): GreetRequest; - - static fromJsonString(jsonString: string, options?: Partial): GreetRequest; - - static equals(a: GreetRequest | PlainMessage | undefined, b: GreetRequest | PlainMessage | undefined): boolean; -} - -/** - * @generated from message hello_world.v1.GreetResponse - */ -export declare class GreetResponse extends Message { - /** - * @generated from field: string greeting = 1; - */ - greeting: string; - - constructor(data?: PartialMessage); - - static readonly runtime: typeof proto3; - static readonly typeName = "hello_world.v1.GreetResponse"; - static readonly fields: FieldList; - - static fromBinary(bytes: Uint8Array, options?: Partial): GreetResponse; - - static fromJson(jsonValue: JsonValue, options?: Partial): GreetResponse; - - static fromJsonString(jsonString: string, options?: Partial): GreetResponse; - - static equals(a: GreetResponse | PlainMessage | undefined, b: GreetResponse | PlainMessage | undefined): boolean; -} - diff --git a/hello-world/web/src/api/hello_world/v1/greeter_pb.js b/hello-world/web/src/api/hello_world/v1/greeter_pb.js deleted file mode 100644 index 92da35d..0000000 --- a/hello-world/web/src/api/hello_world/v1/greeter_pb.js +++ /dev/null @@ -1,55 +0,0 @@ -// @generated by protoc-gen-es v1.3.1 -// @generated from file hello_world/v1/greeter.proto (package hello_world.v1, syntax proto3) -/* eslint-disable */ -// @ts-nocheck - -import { proto3 } from "@bufbuild/protobuf"; - -/** - * @generated from message hello_world.v1.GreeterState - */ -export const GreeterState = proto3.makeMessageType( - "hello_world.v1.GreeterState", - () => [ - { no: 2, name: "greetings", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - ], -); - -/** - * @generated from message hello_world.v1.GreetingsRequest - */ -export const GreetingsRequest = proto3.makeMessageType( - "hello_world.v1.GreetingsRequest", - [], -); - -/** - * @generated from message hello_world.v1.GreetingsResponse - */ -export const GreetingsResponse = proto3.makeMessageType( - "hello_world.v1.GreetingsResponse", - () => [ - { no: 1, name: "greetings", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - ], -); - -/** - * @generated from message hello_world.v1.GreetRequest - */ -export const GreetRequest = proto3.makeMessageType( - "hello_world.v1.GreetRequest", - () => [ - { no: 1, name: "greeting", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ], -); - -/** - * @generated from message hello_world.v1.GreetResponse - */ -export const GreetResponse = proto3.makeMessageType( - "hello_world.v1.GreetResponse", - () => [ - { no: 1, name: "greeting", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ], -); - diff --git a/hello-world/web/src/api/hello_world/v1/greeter_rsm.ts b/hello-world/web/src/api/hello_world/v1/greeter_rsm.ts deleted file mode 100644 index 17870e9..0000000 --- a/hello-world/web/src/api/hello_world/v1/greeter_rsm.ts +++ /dev/null @@ -1,671 +0,0 @@ -import type { - PartialMessage -} from "@bufbuild/protobuf"; -import { - Deferred, - Event, - IQueryRequest, - IQueryResponse, - Mutation, - QueryRequest, - QueryResponse, - ResponseOrError, - filterSet, - parseStreamedValue, - popMutationMaybeFromLocalStorage, - pushMutationMaybeToLocalStorage, - retryForever, - useResembleContext -} from "@reboot-dev/resemble-react"; -import { - Dispatch, - MutableRefObject, - SetStateAction, - useEffect, - useRef, - useState, -} from "react"; -import { v4 as uuidv4 } from "uuid"; - // There are known bugs with the following input / output message imports: - // 1. It doesn't correctly handle input / output messages that don't have a - // corresponding const definition in the generated _pb, - // e.g. google.protobuf.Empty. - // 2. It will try to import the same message twice if any input message is the - // same as any output message. - // TODO: correctly handle messages like google.protobuf.Empty. - // TODO: create a set of input messages AND output messages and |map ..|unique - // over those to ensure there are no duplicates. - import { - GreetRequest, - GreetResponse, - GreetingsRequest, - GreetingsResponse, -} from "./greeter_pb"; - - - // Start of service specific code. - /////////////////////////////////////////////////////////////////////////// - - export interface GreeterApi { - Greetings: (partialRequest?: PartialMessage) => - Promise; - Greet: (partialRequest?: PartialMessage) => - Promise; - useGreetings: (partialRequest?: PartialMessage) => { - response: GreetingsResponse | undefined; - isLoading: boolean; - error: unknown; - mutations: { - Greet: (request: PartialMessage, - optimistic_metadata?: any ) => - Promise>; - }; - pendingGreetMutations: { - request: GreetRequest; - idempotencyKey: string; - isLoading: boolean; - error?: unknown; - optimistic_metadata?: any; - }[]; - failedGreetMutations: { - request: GreetRequest; - idempotencyKey: string; - isLoading: boolean; - error?: unknown; - }[]; - recoveredGreetMutations: { - request: GreetRequest; - idempotencyKey: string; - run: () => void; - }[]; - }; - } - - - export interface SettingsParams { - actorId: string; - storeMutationsLocallyInNamespace?: string; - } - export const Greeter = ({ actorId, storeMutationsLocallyInNamespace}: SettingsParams): GreeterApi => { - const headers = new Headers(); - headers.set("Content-Type", "application/json"); - headers.append("x-resemble-service-name", "hello_world.v1.Greeter"); - headers.append("x-resemble-actor-id", actorId); - headers.append("Connection", "keep-alive"); - - const resembleContext = useResembleContext(); - - const newRequest = ( - requestBody: any, - path: string, - method: "GET" | "POST", - idempotencyKey?: string, - ) => { - if (idempotencyKey !== undefined) { - headers.set("x-resemble-idempotency-key", idempotencyKey); - } - return new Request(`${resembleContext.client?.endpoint}${path}`, { - method: method, - headers: headers, - body: - Object.keys(requestBody).length !== 0 - ? JSON.stringify(requestBody) - : null, - }); - }; - - const Greetings = async (partialRequest: PartialMessage = {}) => { - const request = partialRequest instanceof GreetingsRequest ? partialRequest : new GreetingsRequest(partialRequest); - const requestBody = request.toJson(); - // Invariant here is that we use the '/package.service.method' - // path and HTTP 'POST' method. - // - // See also 'resemble/helpers.py'. - const req = newRequest(requestBody, "/hello_world.v1.Greeter.Greetings", "POST"); - - const response = await fetch(req); - return await response.json(); - }; - - const useGreetings = (partialRequest: PartialMessage = {}) => { - const [response, setResponse] = useState(); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(); - - // NOTE: using "refs" here because we want to "remember" some - // state, but don't want setting that state to trigger new renders (see - // https://react.dev/learn/referencing-values-with-refs). - // Using a ref here so that we don't render every time we set it. - - const observedIdempotencyKeys = useRef(new Set()); - // NOTE: rather than starting with undefined for 'flushMutations' - // we start with an event so any mutations that may get created - // before we've started reading will get queued. - const flushMutations = useRef(new Event()); - - const abortController = useRef(); - // Helper function to get 'abortController' since it is immutable - // and this way we don't need to do 'new AbortController()' on - // every render. - function getAbortController() { - if (abortController.current === undefined) { - abortController.current = new AbortController(); - } - - return abortController.current; - } - - useEffect(() => { - const abortController = getAbortController(); - return () => { - abortController.abort(); - }; - }, []); - - const request = partialRequest instanceof GreetingsRequest - ? partialRequest - : new GreetingsRequest(partialRequest) - - // NOTE: using a ref for the 'request' and 'settings' (below) so - // that it doesn't get changed after the first time calling 'usePing'. - const requestRef = useRef(request); - - // We are using serialized string comparison here since we can't do value - // equality of anonymous objects. We must use the proto library's toBinary() - // since JavaScript's standard JSON library can't serialize every possible - // field type (notably BigInt). - const first_request_serialized = requestRef.current.toBinary().toString(); - const current_request_serialized = request.toBinary().toString(); - if (current_request_serialized !== first_request_serialized) { - throw new Error("Changing the request is not supported!"); - } - - const settingsRef = useRef({actorId, storeMutationsLocallyInNamespace}); - // We are using string comparison here since we can't do value - // equality of anonymous objects. - if (JSON.stringify(settingsRef.current) !== JSON.stringify({actorId, storeMutationsLocallyInNamespace})) { - throw new Error("Changing settings is not supported!"); - } - - const localStorageKeyRef = useRef(storeMutationsLocallyInNamespace); - - const queuedMutations = useRef void>>([]); - - function hasRunningMutations(): boolean { - if ( - runningGreetMutations.current.length > 0) { - return true; - } - return false; - } - - - const runningGreetMutations = useRef[]>([]); - const recoveredGreetMutations = useRef< - [Mutation, () => void][] - >([]); - const shouldClearFailedGreetMutations = useRef(false); - const [failedGreetMutations, setFailedGreetMutations] = useState< - Mutation[] - >([]); - const queuedGreetMutations = useRef<[Mutation, () => void][]>( - [] - ); - const recoverAndPurgeGreetMutations = (): [ - Mutation, - () => void - ][] => { - if (localStorageKeyRef.current === undefined) { - return []; - } - const suffix = Greet - const value = localStorage.getItem(localStorageKeyRef.current + suffix); - if (value === null) { - return []; - } - - localStorage.removeItem(localStorageKeyRef.current); - const mutations: Mutation[] = JSON.parse(value); - const recoveredGreetMutations: [ - Mutation, - () => void - ][] = []; - for (const mutation of mutations) { - recoveredGreetMutations.push([mutation, () => __Greet(mutation)]); - } - return recoveredGreetMutations; - } - const doOnceGreet = useRef(true) - if (doOnceGreet.current) { - doOnceGreet.current = false - recoveredGreetMutations.current = recoverAndPurgeGreetMutations() - } - - // User facing state that only includes the pending mutations that - // have not been observed. - const [unobservedPendingGreetMutations, setUnobservedPendingGreetMutations] = - useState[]>([]); - - useEffect(() => { - shouldClearFailedGreetMutations.current = true; - }, [failedGreetMutations]); - - async function __Greet( - mutation: Mutation - ): Promise> { - try { - // Invariant that we won't yield to event loop before pushing to - // runningGreetMutations - runningGreetMutations.current.push(mutation) - return _Mutation( - // Invariant here is that we use the '/package.service.method'. - // - // See also 'resemble/helpers.py'. - "/hello_world.v1.Greeter.Greet", - mutation, - mutation.request, - mutation.idempotencyKey, - setUnobservedPendingGreetMutations, - getAbortController(), - shouldClearFailedGreetMutations, - setFailedGreetMutations, - runningGreetMutations, - flushMutations, - queuedMutations, - GreetRequest, - GreetResponse.fromJson - ); - } finally { - runningGreetMutations.current = runningGreetMutations.current.filter( - ({ idempotencyKey }) => mutation.idempotencyKey !== idempotencyKey - ); - - popMutationMaybeFromLocalStorage( - localStorageKeyRef.current, - "Greet", - (mutationRequest: Mutation) => - mutationRequest.idempotencyKey !== mutation.idempotencyKey - ); - - - } - } - async function _Greet(mutation: Mutation) { - setUnobservedPendingGreetMutations( - (mutations) => [...mutations, mutation] - ) - - // NOTE: we only run one mutation at a time so that we provide a - // serializable experience for the end user but we will - // eventually support mutations in parallel when we have strong - // eventually consistent writers. - if ( - hasRunningMutations() || - queuedMutations.current.length > 0 || - flushMutations.current !== undefined - ) { - const deferred = new Deferred>(() => - __Greet(mutation) - ); - - // Add to localStorage here. - queuedGreetMutations.current.push([mutation, () => deferred.start()]); - queuedMutations.current.push(() => { - for (const [, run] of queuedGreetMutations.current) { - queuedGreetMutations.current.shift(); - run(); - break; - } - }); - // Maybe add to localStorage. - pushMutationMaybeToLocalStorage(localStorageKeyRef.current, "Greet", mutation); - - return deferred.promise; - } else { - // NOTE: we'll add this mutation to `runningGreetMutations` in `__Greet` - // without yielding to event loop so that we are guaranteed atomicity with checking `hasRunningMutations()`. - return await __Greet(mutation); - } - } - - async function Greet( - partialRequest: PartialMessage, optimistic_metadata?: any - ): Promise> { - const idempotencyKey = uuidv4(); - const request = partialRequest instanceof GreetRequest ? partialRequest : new GreetRequest(partialRequest); - - const mutation = { - request, - idempotencyKey, - optimistic_metadata, - isLoading: false, // Won't start loading if we're flushing mutations. - }; - - return _Greet(mutation); - } - - useEffect(() => { - const loop = async () => { - await retryForever(async () => { - try { - // Wait for any mutations to complete before starting to - // read so that we read the latest state including those - // mutations. - if (runningGreetMutations.current.length > 0) { - // TODO(benh): check invariant - // 'flushMutations.current !== undefined' but don't - // throw an error since that will just retry, instead - // add support for "bailing" from a 'retry' by calling a - // function passed into the lambda that 'retry' takes. - await flushMutations.current?.wait(); - } - - const responses = ReactQuery( - QueryRequest.create({ - method: "Greetings", - request: requestRef.current.toBinary(), - }), - getAbortController().signal - ); - - for await (const response of responses) { - setIsLoading(false); - - for (const idempotencyKey of response.idempotencyKeys) { - observedIdempotencyKeys.current.add(idempotencyKey); - } - - // Only keep around the idempotency keys that are - // still pending as the rest are not useful for us. - observedIdempotencyKeys.current = filterSet( - observedIdempotencyKeys.current, - (observedIdempotencyKey) => - [ - ...runningGreetMutations.current, - ].some( - (mutation) => - observedIdempotencyKey === mutation.idempotencyKey - ) - ); - - if (flushMutations.current !== undefined) { - // TODO(benh): check invariant - // 'pendingMutations.current.length === 0' but don't - // throw an error since that will just retry, instead - // add support for "bailing" from a 'retry' by calling a - // function passed into the lambda that 'retry' takes. - - flushMutations.current = undefined; - - // Dequeue 1 queue and run 1 mutation from it. - for (const run of queuedMutations.current) { - queuedMutations.current.shift(); - run(); - break; - } - } - - setUnobservedPendingGreetMutations( - (mutations) => - mutations - .filter( - (mutation) => - // Only keep mutations that are queued, pending or - // recovered. - queuedGreetMutations.current.some( - ([queuedGreetMutation]) => - mutation.idempotencyKey === - queuedGreetMutation.idempotencyKey - ) || - runningGreetMutations.current.some( - (runningGreetMutations) => - mutation.idempotencyKey === - runningGreetMutations.idempotencyKey - ) - ) - .filter( - (mutation) => - // Only keep mutations whose effects haven't been observed. - !observedIdempotencyKeys.current.has( - mutation.idempotencyKey - ) - ) - ) - - - setResponse(GreetingsResponse.fromBinary(response.response)); - } - } catch (e: unknown) { - if (getAbortController().signal.aborted) { - return; - } - - setError(e); - setIsLoading(true); - - // Run a mutation in the event that we are trying to read - // from an unconstructed actor and the mutation will peform - // the construction. - // - // TODO(benh): only do this if the reason we failed to - // read was because the actor does not exist. - for (const run of queuedMutations.current) { - queuedMutations.current.shift(); - run(); - break; - } - - // TODO(benh): check invariant - // 'flushMutations.current === undefined' but don't - // throw an error since that will just retry, instead - // add support for "bailing" from a 'retry' by calling a - // function passed into the lambda that 'retry' takes. - flushMutations.current = new Event(); - - throw e; - } - }); - }; - - loop(); - }, []); - - return { - response, - isLoading, - error, - mutations: { - Greet, - }, - pendingGreetMutations: unobservedPendingGreetMutations, - failedGreetMutations, - recoveredGreetMutations: recoveredGreetMutations.current.map( - ([mutation, run]) => ({ ...mutation, run: run }) - ), - }; - }; - - - const Greet = async (partialRequest: PartialMessage = {}) => { - const request = partialRequest instanceof GreetRequest ? partialRequest : new GreetRequest(partialRequest); - const requestBody = request.toJson(); - // Invariant here is that we use the '/package.service.method' - // path and HTTP 'POST' method. - // - // See also 'resemble/helpers.py'. - const req = newRequest(requestBody, "/hello_world.v1.Greeter.Greet", "POST"); - - const response = await fetch(req); - return await response.json(); - }; - - - async function _Mutation< - Request extends - GreetRequest, - Response extends GreetResponse >( - path: string, - mutation: Mutation, - request: Request, - idempotencyKey: string, - setUnobservedPendingMutations: Dispatch< - SetStateAction[]> - >, - abortController: AbortController, - shouldClearFailedMutations: MutableRefObject, - setFailedMutations: Dispatch[]>>, - runningMutations: MutableRefObject[]>, - flushMutations: MutableRefObject, - queuedMutations: MutableRefObject void>>, - requestType: { new (request: Request): Request }, - responseTypeFromJson: (json: any) => Response - ): Promise> { - - try { - return await retryForever( - async () => { - try { - setUnobservedPendingMutations( - (mutations) => { - return mutations.map((mutation) => { - if (mutation.idempotencyKey === idempotencyKey) { - return { ...mutation, isLoading: true }; - } - return mutation; - }); - } - ); - const req: Request = - request instanceof requestType - ? request - : new requestType(request); - - const response = await fetch( - newRequest(req.toJson(), path, "POST", idempotencyKey), - { signal: abortController.signal } - ); - - if (!response.ok && response.headers.has("grpc-status")) { - const grpcStatus = response.headers.get("grpc-status"); - let grpcMessage = response.headers.get("grpc-message"); - const error = new Error( - `'hello_world.v1.Greeter' for '${actorId}' responded ` + - `with status ${grpcStatus}` + - `${grpcMessage !== null ? ": " + grpcMessage : ""}` - ); - - if (shouldClearFailedMutations.current) { - shouldClearFailedMutations.current = false; - setFailedMutations([ - { request, idempotencyKey, isLoading: false, error }, - ]); - } else { - setFailedMutations((failedMutations) => [ - ...failedMutations, - { request, idempotencyKey, isLoading: false, error }, - ]); - } - setUnobservedPendingMutations( - (mutations) => - mutations.filter( - (mutation) => mutation.idempotencyKey !== idempotencyKey - ) - ); - - return { error } as ResponseOrError; - } - if (!response.ok) { - throw new Error("Failed to fetch"); - } - const jsonResponse = await response.json(); - return { - response: responseTypeFromJson(jsonResponse), - }; - } catch (e: unknown) { - setUnobservedPendingMutations( - (mutations) => - mutations.map((mutation) => { - if (mutation.idempotencyKey === idempotencyKey) { - return { ...mutation, error: e, isLoading: false }; - } else { - return mutation; - } - }) - ); - - if (abortController.signal.aborted) { - // TODO(benh): instead of returning 'undefined' as a - // means of knowing that we've aborted provide a way - // of "bailing" from a 'retry' by calling a function - // passed into the lambda that 'retry' takes. - return { error: new Error("Aborted") }; - } else { - throw e; - } - } - }, - { - maxBackoffSeconds: 3, - } - ); - } finally { - // NOTE: we deliberately DO NOT remove from - // 'unobservedPendingMutations' but instead wait - // for a response first so that we don't cause a render - // before getting the updated state from the server. - - if ( - flushMutations.current !== undefined && - runningMutations.current.length === 0 - ) { - flushMutations.current.set(); - } else { - // Dequeue 1 queue and run 1 mutation from it. - for (const run of queuedMutations.current) { - queuedMutations.current.shift(); - run(); - break; - } - } - } - } - - async function* ReactQuery( - request: IQueryRequest, - signal: AbortSignal - ): AsyncGenerator { - const response = await fetch( - newRequest(QueryRequest.toJson(request), "/query", "POST"), - { signal: signal } - ); - - if (response.body == null) { - throw new Error("Unable to read body of response"); - } - - const reader = response.body - .pipeThrough(new TextDecoderStream()) - .getReader(); - - if (reader === undefined) { - throw new Error("Not able to instantiate reader on response body"); - } - - while (true) { - const { value, done } = await reader.read(); - if (done) { - break; - } else { - yield QueryResponse.fromJson(JSON.parse(parseStreamedValue(value))); - } - } - } - - return { - Greetings, - useGreetings, - Greet, - }; - }; - - diff --git a/hello-world/web/src/index.tsx b/hello-world/web/src/index.tsx index 0d65f20..6e9a156 100644 --- a/hello-world/web/src/index.tsx +++ b/hello-world/web/src/index.tsx @@ -2,12 +2,12 @@ import { ResembleClient, ResembleClientProvider, } from "@reboot-dev/resemble-react"; -import ReactDOM from 'react-dom/client'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import reportWebVitals from "./reportWebVitals"; const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement + document.getElementById("root") as HTMLElement ); // Use TLS (via localhost.direct) so we get the advantage of HTTP/2 // multiplexing. diff --git a/hello-world/web/src/reportWebVitals.ts b/hello-world/web/src/reportWebVitals.ts index 49a2a16..5fa3583 100644 --- a/hello-world/web/src/reportWebVitals.ts +++ b/hello-world/web/src/reportWebVitals.ts @@ -1,8 +1,8 @@ -import { ReportHandler } from 'web-vitals'; +import { ReportHandler } from "web-vitals"; const reportWebVitals = (onPerfEntry?: ReportHandler) => { if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry);