From 7925b646d86122fe40177756eb9f9cf787723ce0 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 4 Apr 2024 13:27:23 -0600 Subject: [PATCH 01/25] fix(movex-react-improve-api): :bug: fix compiling issue by adding a quick hack --- libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx b/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx index 0de443f3..5fe080f8 100644 --- a/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx +++ b/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx @@ -82,7 +82,7 @@ export class MovexLocalProvider< movex: mockedMovex.movex, clientId: mockedMovex.movex.getClientId(), movexDefinition: this.props.movexDefinition, - + bindResource: () => () => {}, // TODO: Added on April 4th as a quick hack but needs to be fixed! } as const; this.setState({ From 85ff54ec6cf483c7760750c2a0bd52a8ff4560bd Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 4 Apr 2024 13:24:21 -0600 Subject: [PATCH 02/25] chore(lib versions): :hammer: bump to v0.1.3 for all packages --- libs/movex-core-util/package.json | 2 +- libs/movex-master/package.json | 2 +- libs/movex-react-local-master/package.json | 2 +- libs/movex-react/package.json | 2 +- libs/movex-server/package.json | 2 +- libs/movex-service/package.json | 2 +- libs/movex-store/package.json | 2 +- libs/movex/package.json | 2 +- package.json | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/movex-core-util/package.json b/libs/movex-core-util/package.json index 7a5044d5..99cebca2 100644 --- a/libs/movex-core-util/package.json +++ b/libs/movex-core-util/package.json @@ -1,6 +1,6 @@ { "name": "movex-core-util", - "version": "0.1.3-13", + "version": "0.1.3", "description": "Movex Core Util is the library of utilities for Movex", "license": "MIT", "author": { diff --git a/libs/movex-master/package.json b/libs/movex-master/package.json index 6edf99cc..16a4ec7f 100644 --- a/libs/movex-master/package.json +++ b/libs/movex-master/package.json @@ -1,6 +1,6 @@ { "name": "movex-master", - "version": "0.1.3-13", + "version": "0.1.3", "license": "MIT", "description": "Movex-master defines the master that wil be used on movex-server and movex-react-local-master", "author": { diff --git a/libs/movex-react-local-master/package.json b/libs/movex-react-local-master/package.json index cb5d27a3..57e8b5db 100644 --- a/libs/movex-react-local-master/package.json +++ b/libs/movex-react-local-master/package.json @@ -1,6 +1,6 @@ { "name": "movex-react-local-master", - "version": "0.1.3-13", + "version": "0.1.3", "license": "MIT", "description": "Run the movex-master locally with react", "author": { diff --git a/libs/movex-react/package.json b/libs/movex-react/package.json index d45800be..93e4003d 100644 --- a/libs/movex-react/package.json +++ b/libs/movex-react/package.json @@ -1,6 +1,6 @@ { "name": "movex-react", - "version": "0.1.3-16", + "version": "0.1.3", "license": "MIT", "description": "Movex React is the library of React components for Movex", "author": { diff --git a/libs/movex-server/package.json b/libs/movex-server/package.json index b65dad57..5eb2872d 100644 --- a/libs/movex-server/package.json +++ b/libs/movex-server/package.json @@ -1,6 +1,6 @@ { "name": "movex-server", - "version": "0.1.3-13", + "version": "0.1.3", "license": "MIT", "type": "commonjs", "description": "Movex Server is the backend runtime for Movex", diff --git a/libs/movex-service/package.json b/libs/movex-service/package.json index 9f334827..145e3253 100644 --- a/libs/movex-service/package.json +++ b/libs/movex-service/package.json @@ -1,6 +1,6 @@ { "name": "movex-service", - "version": "0.1.3-13", + "version": "0.1.3", "license": "MIT", "type": "commonjs", "description": "Movex Service is the CLI for Movex", diff --git a/libs/movex-store/package.json b/libs/movex-store/package.json index 1f775972..aa96a744 100644 --- a/libs/movex-store/package.json +++ b/libs/movex-store/package.json @@ -1,6 +1,6 @@ { "name": "movex-store", - "version": "0.1.3-13", + "version": "0.1.3", "license": "MIT", "description": "Movex-store defines the store interface and comes with a MemoryStore", "author": { diff --git a/libs/movex/package.json b/libs/movex/package.json index 25ba4c80..241d7ec7 100644 --- a/libs/movex/package.json +++ b/libs/movex/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.3-14", + "version": "0.1.3", "license": "MIT", "description": "Movex is a Multiplayer (Game) State Synchronization Library using Deterministic Action Propagation without the need to write Server Specific Code.", "author": { diff --git a/package.json b/package.json index 30d40f4d..1d9b58f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "author": { "name": "Gabriel C. Troia", From 200b2b269891389b75c628b05347d82f15710909 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 4 Apr 2024 13:37:37 -0600 Subject: [PATCH 03/25] chore(package version): :hammer: bump to 0.1.4 actually out of some stupid issue --- libs/movex-core-util/package.json | 2 +- libs/movex-master/package.json | 2 +- libs/movex-react-local-master/package.json | 2 +- libs/movex-react/package.json | 2 +- libs/movex-server/package.json | 2 +- libs/movex-service/package.json | 2 +- libs/movex-store/package.json | 2 +- libs/movex/package.json | 2 +- package.json | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/movex-core-util/package.json b/libs/movex-core-util/package.json index 99cebca2..35841cb8 100644 --- a/libs/movex-core-util/package.json +++ b/libs/movex-core-util/package.json @@ -1,6 +1,6 @@ { "name": "movex-core-util", - "version": "0.1.3", + "version": "0.1.4", "description": "Movex Core Util is the library of utilities for Movex", "license": "MIT", "author": { diff --git a/libs/movex-master/package.json b/libs/movex-master/package.json index 16a4ec7f..a3d3bb35 100644 --- a/libs/movex-master/package.json +++ b/libs/movex-master/package.json @@ -1,6 +1,6 @@ { "name": "movex-master", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "description": "Movex-master defines the master that wil be used on movex-server and movex-react-local-master", "author": { diff --git a/libs/movex-react-local-master/package.json b/libs/movex-react-local-master/package.json index 57e8b5db..d7d5f5a2 100644 --- a/libs/movex-react-local-master/package.json +++ b/libs/movex-react-local-master/package.json @@ -1,6 +1,6 @@ { "name": "movex-react-local-master", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "description": "Run the movex-master locally with react", "author": { diff --git a/libs/movex-react/package.json b/libs/movex-react/package.json index 93e4003d..ffc3a548 100644 --- a/libs/movex-react/package.json +++ b/libs/movex-react/package.json @@ -1,6 +1,6 @@ { "name": "movex-react", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "description": "Movex React is the library of React components for Movex", "author": { diff --git a/libs/movex-server/package.json b/libs/movex-server/package.json index 5eb2872d..874174f4 100644 --- a/libs/movex-server/package.json +++ b/libs/movex-server/package.json @@ -1,6 +1,6 @@ { "name": "movex-server", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "type": "commonjs", "description": "Movex Server is the backend runtime for Movex", diff --git a/libs/movex-service/package.json b/libs/movex-service/package.json index 145e3253..4f34983a 100644 --- a/libs/movex-service/package.json +++ b/libs/movex-service/package.json @@ -1,6 +1,6 @@ { "name": "movex-service", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "type": "commonjs", "description": "Movex Service is the CLI for Movex", diff --git a/libs/movex-store/package.json b/libs/movex-store/package.json index aa96a744..d4db68c7 100644 --- a/libs/movex-store/package.json +++ b/libs/movex-store/package.json @@ -1,6 +1,6 @@ { "name": "movex-store", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "description": "Movex-store defines the store interface and comes with a MemoryStore", "author": { diff --git a/libs/movex/package.json b/libs/movex/package.json index 241d7ec7..48afd9ff 100644 --- a/libs/movex/package.json +++ b/libs/movex/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "description": "Movex is a Multiplayer (Game) State Synchronization Library using Deterministic Action Propagation without the need to write Server Specific Code.", "author": { diff --git a/package.json b/package.json index 1d9b58f8..2f05dd49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "author": { "name": "Gabriel C. Troia", From e8088b98bf521936790b5e4de6c45ff8f4969464 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Mon, 1 Apr 2024 17:07:48 -0600 Subject: [PATCH 04/25] feat(movex-master): :sparkles: add onResourceSubscriberRemoved & onResourceSubscriberAdded handlers --- libs/movex-core-util/src/lib/Logsy.ts | 2 +- libs/movex-core-util/src/lib/core-types.ts | 1 + libs/movex-core-util/src/lib/io/IOEvents.ts | 50 ++++- .../movex-master/src/lib/MovexMasterServer.ts | 193 +++++++++++++----- .../src/lib/MovexLocalProvider.tsx | 2 +- .../src/lib/ConnectionToMasterResource.ts | 14 +- libs/movex/src/lib/MovexBoundResource.ts | 2 +- libs/movex/src/lib/MovexResource.ts | 4 +- package.json | 2 +- 9 files changed, 203 insertions(+), 67 deletions(-) diff --git a/libs/movex-core-util/src/lib/Logsy.ts b/libs/movex-core-util/src/lib/Logsy.ts index 9515ee0d..a5db5226 100644 --- a/libs/movex-core-util/src/lib/Logsy.ts +++ b/libs/movex-core-util/src/lib/Logsy.ts @@ -14,7 +14,7 @@ const globalDisabled = false; const globalLogsyConfigWrapper = { config: { disabled: false, - verbose: false, + verbose: true, // TODO: Change this back }, }; diff --git a/libs/movex-core-util/src/lib/core-types.ts b/libs/movex-core-util/src/lib/core-types.ts index acfd7d34..3bbb53a4 100644 --- a/libs/movex-core-util/src/lib/core-types.ts +++ b/libs/movex-core-util/src/lib/core-types.ts @@ -238,6 +238,7 @@ export type ResourceIdentifier = | ResourceIdentifierStr; export type AnyResourceIdentifier = ResourceIdentifier; +export type AnyStringResourceIdentifier = ResourceIdentifierStr; export type StringKeys = Extract< keyof TRecord, diff --git a/libs/movex-core-util/src/lib/io/IOEvents.ts b/libs/movex-core-util/src/lib/io/IOEvents.ts index f1b6ab7d..76d074e7 100644 --- a/libs/movex-core-util/src/lib/io/IOEvents.ts +++ b/libs/movex-core-util/src/lib/io/IOEvents.ts @@ -8,6 +8,7 @@ import type { CheckedState, Checksum, IOPayloadResult, + MovexClient, ResourceIdentifier, ResourceIdentifierStr, } from '../core-types'; @@ -17,8 +18,11 @@ export type IOEvents< A extends AnyAction = AnyAction, TResourceType extends string = string > = { - ping: () => IOPayloadResult; - pong: () => IOPayloadResult; + + /** + * The following events are directed from Client to Master + * */ + createResource: (p: { resourceType: TResourceType; resourceState: TState; @@ -40,12 +44,13 @@ export type IOEvents< void, unknown // Type this >; - removeResourceSubscriber: (p: { - rid: ResourceIdentifier; - }) => IOPayloadResult< - void, - unknown // Type this - >; + + // removeResourceSubscriber: (p: { + // rid: ResourceIdentifier; + // }) => IOPayloadResult< + // void, + // unknown // Type this + // >; getResourceState: (p: { rid: ResourceIdentifier; @@ -68,14 +73,39 @@ export type IOEvents< 'MasterResourceInexistent' | string >; // Type the other errors - fwdAction: ( + /** + * The following events are directed from Master to Client + * */ + + onFwdAction: ( payload: { rid: ResourceIdentifier; } & ToCheckedAction ) => IOPayloadResult; - reconciliateActions: ( + onReconciliateActions: ( payload: { rid: ResourceIdentifier; } & CheckedReconciliatoryActions ) => IOPayloadResult; + onResourceSubscriberAdded: (p: { + rid: ResourceIdentifier; + clientId: MovexClient['id']; + }) => IOPayloadResult< + void, + unknown // Type this + >; + onResourceSubscriberRemoved: (p: { + rid: ResourceIdentifier; + clientId: MovexClient['id']; + }) => IOPayloadResult< + void, + unknown // Type this + >; + + /** + * The following events are by-directional (from Client to Master and vice-versa) + * */ + + ping: () => IOPayloadResult; + pong: () => IOPayloadResult; }; diff --git a/libs/movex-master/src/lib/MovexMasterServer.ts b/libs/movex-master/src/lib/MovexMasterServer.ts index 63dc3495..ed0b8b17 100644 --- a/libs/movex-master/src/lib/MovexMasterServer.ts +++ b/libs/movex-master/src/lib/MovexMasterServer.ts @@ -6,7 +6,9 @@ import { type AnyAction, type IOEvents, type ConnectionToClient, -} from 'movex-core-util'; + AnyResourceIdentifier, + AnyStringResourceIdentifier, +} from 'movex-core-util'; import { AsyncResult } from 'ts-async-results'; import { Err, Ok } from 'ts-results'; import { MovexMasterResource } from './MovexMasterResource'; @@ -19,11 +21,6 @@ const logsy = globalLogsy.withNamespace('[MovexMasterServer]'); * This is also very generic with an API to just work when run */ export class MovexMasterServer { - // needs a store (redis, api, etc) - // needs a way to send messages to the clients - // so a connection. when an incoming message comes, process it and send further - // but of course without knowing it's socket or local, just async so it can be tested - // TODO: This works only per one instance/machine // If there are multiple server instances running then we need to use redis/socket-io distribution etc.. @@ -32,12 +29,22 @@ export class MovexMasterServer { ConnectionToClient > = {}; + // A Map of subscribers to their subsxribed resources + private subscribersToRidsMap: Record< + MovexClient['id'], + Record + > = {}; + constructor( private masterResourcesByType: Record> ) {} - // This needs to respond back to the client - // it receives emitActions and responds with Ack, fwd or reconcilitary + /** + * Adds the Client Connection and subscribers to all the client events + * + * @param clientConnection + * @returns + */ addClientConnection( clientConnection: ConnectionToClient ) { @@ -72,7 +79,7 @@ export class MovexMasterServer { const peerConnection = this.clientConnectionsByClientId[peerId]; - peerConnection.emitter.emit('reconciliateActions', { + peerConnection.emitter.emit('onReconciliateActions', { rid, ...peerActions.byClientId[peerId], }); @@ -103,7 +110,7 @@ export class MovexMasterServer { return; } - peerConnection.emitter.emit('fwdAction', { + peerConnection.emitter.emit('onFwdAction', { rid, ...peerActions.byClientId[peerId], }); @@ -194,32 +201,40 @@ export class MovexMasterServer { masterResource .addResourceSubscriber(payload.rid, clientConnection.clientId) - .map(() => { + .map((s) => { + // Keep a record of the rid it just subscribed to so it can also be unsubscribed + this.subscribersToRidsMap = { + ...this.subscribersToRidsMap, + [clientConnection.clientId]: { + ...this.subscribersToRidsMap[clientConnection.clientId], + [s.rid]: undefined, + }, + }; + + // Send the ack to the just-added-client acknowledge?.(Ok.EMPTY); - }) - .mapErr((e) => acknowledge?.(new Err('UnknownError'))); // TODO: Type this using the ResultError from Matterio - }; - - const onRemoveResourceSubscriber = ( - payload: Parameters< - IOEvents['removeResourceSubscriber'] - >[0], - acknowledge?: ( - p: ReturnType['removeResourceSubscriber']> - ) => void - ) => { - const { resourceType } = toResourceIdentifierObj(payload.rid); - - const masterResource = this.masterResourcesByType[resourceType]; - - if (!masterResource) { - return acknowledge?.(new Err('MasterResourceInexistent')); - } - masterResource - .removeResourceSubscriber(payload.rid, clientConnection.clientId) - .map(() => { - acknowledge?.(Ok.EMPTY); + // Let the rest of the peer-clients know as well + objectKeys(s.subscribers) + // Take out just-added-client + .filter((clientId) => clientId !== clientConnection.clientId) + .forEach((peerId) => { + const peerConnection = this.clientConnectionsByClientId[peerId]; + + if (!peerConnection) { + logsy.error( + 'onAddResourceSubscriber: Peer Connection not found for', + peerId, + this.clientConnectionsByClientId + ); + return; + } + + peerConnection.emitter.emit('onResourceSubscriberAdded', { + rid: payload.rid, + clientId: clientConnection.clientId, + }); + }); }) .mapErr((e) => acknowledge?.(new Err('UnknownError'))); // TODO: Type this using the ResultError from Matterio }; @@ -243,10 +258,12 @@ export class MovexMasterServer { 'addResourceSubscriber', onAddResourceSubscriber ); - clientConnection.emitter.on( - 'removeResourceSubscriber', - onRemoveResourceSubscriber - ); + + // This doesn't come from the client, but it is emitted to the clent from the server + // clientConnection.emitter.on( + // 'removeResourceSubscriber', + // onRemoveResourceSubscriber + // ); this.clientConnectionsByClientId = { ...this.clientConnectionsByClientId, @@ -277,24 +294,106 @@ export class MovexMasterServer { 'addResourceSubscriber', onAddResourceSubscriber ); - clientConnection.emitter.off( - 'removeResourceSubscriber', - onRemoveResourceSubscriber - ); + // clientConnection.emitter.off( + // 'removeResourceSubscriber', + // onRemoveResourceSubscriber + // ); }; } - removeConnection(clienId: MovexClient['id']) { - const { [clienId]: removed, ...restOfConnections } = + removeConnection(clientId: MovexClient['id']) { + this.unsubscribeClientFromResources(clientId); + + const { [clientId]: removed, ...restOfConnections } = this.clientConnectionsByClientId; this.clientConnectionsByClientId = restOfConnections; logsy.log( - '[MovexMasterServer] Removed Connection Succesfully', - this.clientConnectionsByClientId, - '| Connections:', + '[MovexMasterServer] Removed Connection Succesfully for Client:', + clientId, + '| Connections Left:', Object.keys(this.clientConnectionsByClientId).length ); } + + private unsubscribeClientFromResources(clientId: MovexClient['id']) { + const clientSubscricptions = this.subscribersToRidsMap[clientId]; + + if (!clientSubscricptions) { + logsy.log('No Resource Subscription'); + return; + } + + const subscribedRidsList = objectKeys(clientSubscricptions); + + subscribedRidsList.forEach(async (rid) => { + await this.removeResourceSubscriberAndNotifyPeersOfClientUnsubscription( + rid, + clientId + ); + + // TODO: should this wait for the client to ack or smtg? probably not needed + + // Remove the rid from the client's record + const { [rid]: removed, ...rest } = this.subscribersToRidsMap[clientId]; + + this.subscribersToRidsMap[clientId] = rest; + }); + + logsy.log('Removed client subscriptions ok'); + } + + private async removeResourceSubscriberAndNotifyPeersOfClientUnsubscription( + rid: AnyStringResourceIdentifier, + subscriberClientId: MovexClient['id'] + ) { + const { resourceType } = toResourceIdentifierObj(rid); + + const masterResource = this.masterResourcesByType[resourceType]; + + if (!masterResource) { + logsy.error('MasterResourceInexistent', resourceType); + + return; + } + + await masterResource + .removeResourceSubscriber(rid, subscriberClientId) + .resolve(); + + await masterResource + .getSubscribers(rid) + .map((clientIds) => { + console.log( + 'notifyPeersOfClientUnsubscription resource', + rid, + 'subscribers', + clientIds + ); + + objectKeys(clientIds) + .filter((clientId) => clientId !== subscriberClientId) + .forEach((peerId) => { + const peerConnection = this.clientConnectionsByClientId[peerId]; + + if (!peerConnection) { + logsy.error( + 'notifyPeersOfClientUnsubscription Peer Connection not found for', + peerId, + this.clientConnectionsByClientId + ); + return; + } + + peerConnection.emitter.emit('onResourceSubscriberRemoved', { + rid, + clientId: subscriberClientId, + }); + }); + }) + .resolve(); + + // TODO: Should this wait for the ack form each subscriber before removing it?? Probably not, right? + } } diff --git a/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx b/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx index 5fe080f8..720bc0f4 100644 --- a/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx +++ b/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx @@ -82,7 +82,7 @@ export class MovexLocalProvider< movex: mockedMovex.movex, clientId: mockedMovex.movex.getClientId(), movexDefinition: this.props.movexDefinition, - bindResource: () => () => {}, // TODO: Added on April 4th as a quick hack but needs to be fixed! + bindResource: () => () => {}, // Added this on Apr 01 2024, w/o testing - not sure it doesn't create more issues but needed it for tsc } as const; this.setState({ diff --git a/libs/movex/src/lib/ConnectionToMasterResource.ts b/libs/movex/src/lib/ConnectionToMasterResource.ts index e1889e4d..9a46ac32 100644 --- a/libs/movex/src/lib/ConnectionToMasterResource.ts +++ b/libs/movex/src/lib/ConnectionToMasterResource.ts @@ -73,19 +73,25 @@ export class ConnectionToMasterResource< ); }; + const onRemoveResourceSubscriber = () => { + + } + connectionToMaster.emitter.on( - 'reconciliateActions', + 'onReconciliateActions', onReconciliateActionsHandler ); - connectionToMaster.emitter.on('fwdAction', onFwdActionHandler); + connectionToMaster.emitter.on('onFwdAction', onFwdActionHandler); + + connectionToMaster.emitter.on('onResourceSubscriberRemoved', onRemoveResourceSubscriber); // Unsubscribe from the events too this.unsubscribers = [ - () => connectionToMaster.emitter.off('fwdAction', onFwdActionHandler), + () => connectionToMaster.emitter.off('onFwdAction', onFwdActionHandler), () => connectionToMaster.emitter.off( - 'reconciliateActions', + 'onReconciliateActions', onReconciliateActionsHandler ), ]; diff --git a/libs/movex/src/lib/MovexBoundResource.ts b/libs/movex/src/lib/MovexBoundResource.ts index 2e4f40da..983a7dd7 100644 --- a/libs/movex/src/lib/MovexBoundResource.ts +++ b/libs/movex/src/lib/MovexBoundResource.ts @@ -8,7 +8,7 @@ import type { MovexResourceObservable } from './MovexResourceObservable'; /** * This is the MovexResource running on the Client * It could be used extensively in the UI hence the need to "bind" the methods - * so "this" statys correct + * so "this" stays correct */ export class MovexBoundResource< TState = any, diff --git a/libs/movex/src/lib/MovexResource.ts b/libs/movex/src/lib/MovexResource.ts index c12f9e53..ac3b5f99 100644 --- a/libs/movex/src/lib/MovexResource.ts +++ b/libs/movex/src/lib/MovexResource.ts @@ -28,7 +28,7 @@ const logOpenConnectionStyle = 'color: #EF5FA0; font-weight: bold'; const logClosedConnectionStyle = 'color: #DF9D04; font-weight: bold'; const logErrorStyle = 'color: red; font-weight: bold;'; -const logsy = rawLogsy.withNamespace('[MovexResource]'); +const logsy = rawLogsy.withNamespace('[Movex][MovexResource]'); // const logsy = rawLogsy; export class MovexResource< @@ -135,7 +135,7 @@ export class MovexResource< logUnimportantStyle, prevCheckedState ); - logsy.log('%cNext (Master) Staet', logIncomingStyle, masterCheckState); + logsy.log('%cNext (Master) State', logIncomingStyle, masterCheckState); logsy.debug( 'Diff', deepObject.detailedDiff(prevCheckedState, masterCheckState) diff --git a/package.json b/package.json index 2f05dd49..c640f030 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "publish-movex-service": "cd dist/libs/movex-service && npm publish && cd -", "publish-movex-store": "cd dist/libs/movex-store && npm publish && cd -", "publish-all-libs": "npm run publish-movex && npm run publish-movex-server && npm run publish-movex-core-util && npm run publish-movex-react && npm run publish-movex-service && npm run publish-movex-store && npm run publish-movex-master", - "tsc-all-libs": "npx nx run-many --parallel --target=tsc --projects=movex-core-util,movex,movex-server,movex-service,movex-react,movex-vue,movex-store,movex-master,movex-specs-util", + "tsc-all-libs": "npx nx run-many --parallel --target=tsc --projects=movex-core-util,movex,movex-server,movex-service,movex-react,movex-react-local-master,movex-vue,movex-store,movex-master,movex-specs-util", "test-all-libs": "npx nx run-many --parallel --target=test --projects=movex,movex-core-util,movex-master,movex-react,movex-react-local-master,movex-server,movex-service,movex-store,movex-vue", "build-all-libs": "npx nx run-many --parallel --target=build --projects=movex,movex-core-util,movex-master,movex-react,movex-react-local-master,movex-server,movex-service,movex-store,movex-vue", "deploy": "git diff --exit-code HEAD && flyctl deploy -c apps/movex-demo-api/fly.toml --dockerfile apps/movex-demo-api/Dockerfile -e APP_REVISION=$(git rev-parse --short HEAD) --remote-only" From 556af2877c1165f5c001b9c68bf10f072f5f81ff Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Wed, 3 Apr 2024 13:51:00 -0600 Subject: [PATCH 05/25] Refactor the MovexResourceObseravble State to support subscribers as well. Add Observable.map functionality --- libs/movex-core-util/src/index.ts | 2 +- .../src/lib/Observable/Observable.spec.ts | 63 ++++++ .../src/lib/{ => Observable}/Observable.ts | 29 ++- .../src/lib/Observable/index.ts | 1 + libs/movex-core-util/src/lib/core-types.ts | 51 +++-- libs/movex-core-util/src/lib/domain.ts | 15 +- libs/movex-core-util/src/lib/io/IOEvents.ts | 36 +-- .../src/lib/MovexMasterResource.spec.ts | 20 +- .../src/lib/MovexMasterResource.ts | 45 ++-- .../movex-master/src/lib/MovexMasterServer.ts | 62 +++++- libs/movex-master/src/lib/util.ts | 31 ++- libs/movex-react/src/index.ts | 2 +- ...ce.tsx => MovexBoundResourceComponent.tsx} | 37 ++-- libs/movex-react/src/lib/MovexProvider.tsx | 55 +---- .../src/lib/ResourceObservableRegistry.ts | 56 +++++ .../src/lib/ConnectionToMasterResource.ts | 152 +++++++++++-- libs/movex/src/lib/MovexBoundResource.ts | 15 +- libs/movex/src/lib/MovexResource.ts | 208 ++++++++++-------- .../src/lib/MovexResourceObservable.spec.ts | 203 +++++++++-------- libs/movex/src/lib/MovexResourceObservable.ts | 128 +++++++++-- libs/movex/src/lib/dispatch.ts | 4 +- 21 files changed, 818 insertions(+), 397 deletions(-) create mode 100644 libs/movex-core-util/src/lib/Observable/Observable.spec.ts rename libs/movex-core-util/src/lib/{ => Observable}/Observable.ts (59%) create mode 100644 libs/movex-core-util/src/lib/Observable/index.ts rename libs/movex-react/src/lib/{MovexBoundResource.tsx => MovexBoundResourceComponent.tsx} (80%) create mode 100644 libs/movex-react/src/lib/ResourceObservableRegistry.ts diff --git a/libs/movex-core-util/src/index.ts b/libs/movex-core-util/src/index.ts index 7519a5df..7b21ae9a 100644 --- a/libs/movex-core-util/src/index.ts +++ b/libs/movex-core-util/src/index.ts @@ -1,6 +1,6 @@ export * from './lib/misc'; export * from './lib/domain'; -export * from './lib/Observable'; +export * from './lib/Observable/Observable'; export * from './lib/EventEmitter'; export * from './lib/ScketIOEmitter'; export * from './lib/Logsy'; diff --git a/libs/movex-core-util/src/lib/Observable/Observable.spec.ts b/libs/movex-core-util/src/lib/Observable/Observable.spec.ts new file mode 100644 index 00000000..9131ce85 --- /dev/null +++ b/libs/movex-core-util/src/lib/Observable/Observable.spec.ts @@ -0,0 +1,63 @@ +import { Observable } from './Observable'; + +test('Get', () => { + const $model = new Observable(1); + + expect($model.get()).toEqual(1); +}); + +test('Update', () => { + const $model = new Observable(1); + + $model.update(2); + + expect($model.get()).toEqual(2); +}); + +test('On Update', () => { + const $model = new Observable(1); + + const spy = jest.fn(); + + $model.onUpdate(spy); + + $model.update(2); + + expect(spy).toHaveBeenCalledWith(2); +}); + +describe('Map', () => { + test('Update on $root triggers an update on the $mapped', () => { + const $root = new Observable(1); + const $mappedModel = $root.map((x) => x + 10); + + const spy = jest.fn(); + $mappedModel.onUpdate(spy); + + $root.update(2); + + expect($root.get()).toEqual(2); + + expect(spy).toHaveBeenCalledWith(12); + expect($mappedModel.get()).toEqual(12); + }); + + test('Update on $mapped does NOT trigger an update on the $root', () => { + const $root = new Observable(0); + const $mappedModel = $root.map((x) => x + 10); + + const spyOnMapped = jest.fn(); + $mappedModel.onUpdate(spyOnMapped); + + const spyOnRoot = jest.fn(); + $root.onUpdate(spyOnRoot); + + $mappedModel.update(2); + + expect($mappedModel.get()).toEqual(2); + expect(spyOnMapped).toHaveBeenCalledWith(2); + + expect($root.get()).toEqual(0); // the initial val + expect(spyOnRoot).not.toHaveBeenCalled(); + }); +}); diff --git a/libs/movex-core-util/src/lib/Observable.ts b/libs/movex-core-util/src/lib/Observable/Observable.ts similarity index 59% rename from libs/movex-core-util/src/lib/Observable.ts rename to libs/movex-core-util/src/lib/Observable/Observable.ts index 45fc94f9..1cb0b750 100644 --- a/libs/movex-core-util/src/lib/Observable.ts +++ b/libs/movex-core-util/src/lib/Observable/Observable.ts @@ -1,11 +1,10 @@ import { Pubsy } from 'ts-pubsy'; -import { isFunction } from './misc'; +import { isFunction } from '../misc'; export type NextStateGetter = T | ((prev: T) => T); -export const getNextStateFrom = (prev: T, a: NextStateGetter) => { - return isFunction(a) ? a(prev) : a; -}; +// export const getNextStateFrom = (prev: T, a: NextStateGetter) => +// isFunction(a) ? a(prev) : a; export interface IObservable { get: () => T; @@ -13,7 +12,7 @@ export interface IObservable { update: (getNextState: T | ((prev: T) => T)) => void; } -export class Observable implements IObservable{ +export class Observable implements IObservable { private pubsy = new Pubsy<{ onUpdate: T; }>(); @@ -29,7 +28,7 @@ export class Observable implements IObservable{ } update(nextStateGetter: NextStateGetter) { - this._state = getNextStateFrom(this._state, nextStateGetter); + this._state = Observable.getNextStateFrom(this._state, nextStateGetter); // TODO: Should it only call onUpdate when there actually is an update?? // Or should I leave that to the implementation @@ -39,15 +38,23 @@ export class Observable implements IObservable{ } // TODO: Add map for transformation + // TODO: TEST THIS AND ENSURE IT DOESNT CREATE SOME WEIRD LOOPING OR OTHER ISSUES + /** + * + * + * @param mapFn + * @returns + */ map(mapFn: (state: T) => T1) { - // Is this good enough? Or is it creating some weird issues? const $next = new Observable(mapFn(this.get())); - // Hook up the update - $next.onUpdate = (updateFn: (state: T1) => void) => { - return this.onUpdate((t) => updateFn(mapFn(t))); - }; + this.onUpdate((nextRootState) => { + $next.update(mapFn(nextRootState)); + }); return $next; } + + static getNextStateFrom = (prev: T, a: NextStateGetter) => + isFunction(a) ? a(prev) : a; } diff --git a/libs/movex-core-util/src/lib/Observable/index.ts b/libs/movex-core-util/src/lib/Observable/index.ts new file mode 100644 index 00000000..42099f22 --- /dev/null +++ b/libs/movex-core-util/src/lib/Observable/index.ts @@ -0,0 +1 @@ +export * from './Observable'; diff --git a/libs/movex-core-util/src/lib/core-types.ts b/libs/movex-core-util/src/lib/core-types.ts index 3bbb53a4..3d55fb96 100644 --- a/libs/movex-core-util/src/lib/core-types.ts +++ b/libs/movex-core-util/src/lib/core-types.ts @@ -152,6 +152,15 @@ export type AnyIdentifiableRecord = { id: string } & Record; export type UnidentifiableModel = Omit; +export type MovexClientResourceShape< + TResourceType extends GenericResourceType, + TState +> = { + state: CheckedState; + rid: ResourceIdentifierStr; + subscribers: Record; +}; + // TODO: Remove all of these if not used export type ResourceShape< @@ -169,10 +178,10 @@ export type ResourceShape< >; }; -export type ClientResourceShape< - TResourceType extends PropertyKey, - TData extends UnknownRecord -> = Pick, 'type' | 'id' | 'item'>; +// export type ClientResourceShape< +// TResourceType extends PropertyKey, +// TData extends UnknownRecord +// > = Pick, 'type' | 'id' | 'item'>; export type Resource< ResourceCollectionMap extends Record, @@ -184,22 +193,22 @@ export type ServerResource< TResourceType extends keyof ResourceCollectionMap > = Resource; -export type ClientResource< - ResourceCollectionMap extends Record, - TResourceType extends keyof ResourceCollectionMap -> = Pick< - ServerResource, - 'item' | 'type' | 'id' ->; +// export type ClientResource< +// ResourceCollectionMap extends Record, +// TResourceType extends keyof ResourceCollectionMap +// > = Pick< +// ServerResource, +// 'item' | 'type' | 'id' +// >; -export type GenericClientResource = ClientResource< - Record, - keyof UnknownRecord ->; +// export type GenericClientResource = ClientResource< +// Record, +// keyof UnknownRecord +// >; export type GenericClientResourceShapeOfType< - TResourceType extends PropertyKey -> = ClientResourceShape; + TResourceType extends GenericResourceType +> = MovexClientResourceShape; export type GenericResource = Resource< Record, @@ -385,3 +394,11 @@ export type IsOfType = T extends U ? K : never; export type OnlyKeysOfType> = { [K in keyof O]: IsOfType; }[keyof O]; + +/** + * Taken from https://stackoverflow.com/a/67794430/2093626 + * This ensures the omit doesn't break distributin uniions! + */ +export type DistributiveOmit = T extends any + ? Omit + : never; diff --git a/libs/movex-core-util/src/lib/domain.ts b/libs/movex-core-util/src/lib/domain.ts index e2beed42..0f7e677d 100644 --- a/libs/movex-core-util/src/lib/domain.ts +++ b/libs/movex-core-util/src/lib/domain.ts @@ -44,13 +44,14 @@ export const toWsResponseResultPayloadErr = ( val, }); -export const getResourceRId = ( - r: GenericClientResourceShapeOfType -) => - toResourceIdentifierStr({ - resourceType: r.type, - resourceId: r.id, - }); +// This was removed on Apirl 1st 2024, as I'm not sure it's used anywhere +// export const getResourceRid = ( +// r: GenericClientResourceShapeOfType +// ) => +// toResourceIdentifierStr({ +// resourceType: r.type, +// resourceId: r.id, +// }); export const isResourceIdentifier = ( s: unknown diff --git a/libs/movex-core-util/src/lib/io/IOEvents.ts b/libs/movex-core-util/src/lib/io/IOEvents.ts index 76d074e7..f79e9a92 100644 --- a/libs/movex-core-util/src/lib/io/IOEvents.ts +++ b/libs/movex-core-util/src/lib/io/IOEvents.ts @@ -7,6 +7,7 @@ import type { import type { CheckedState, Checksum, + MovexClientResourceShape, IOPayloadResult, MovexClient, ResourceIdentifier, @@ -18,9 +19,8 @@ export type IOEvents< A extends AnyAction = AnyAction, TResourceType extends string = string > = { - /** - * The following events are directed from Client to Master + * The following events are directed from Client to Master * */ createResource: (p: { @@ -29,10 +29,11 @@ export type IOEvents< resourceId?: string; // clientId: MovexClient['id']; // Needed? }) => IOPayloadResult< - { - rid: ResourceIdentifierStr; - state: CheckedState; - }, + MovexClientResourceShape, + // { + // rid: ResourceIdentifierStr; + // state: CheckedState; + // }, unknown // Type this >; @@ -41,17 +42,10 @@ export type IOEvents< addResourceSubscriber: (p: { rid: ResourceIdentifier; }) => IOPayloadResult< - void, + MovexClientResourceShape, unknown // Type this >; - // removeResourceSubscriber: (p: { - // rid: ResourceIdentifier; - // }) => IOPayloadResult< - // void, - // unknown // Type this - // >; - getResourceState: (p: { rid: ResourceIdentifier; }) => IOPayloadResult< @@ -59,6 +53,20 @@ export type IOEvents< unknown // Type this >; + getResourceSubscribers: (p: { + rid: ResourceIdentifier; + }) => IOPayloadResult< + MovexClientResourceShape['subscribers'], + unknown // Type this + >; + + getResource: (p: { + rid: ResourceIdentifier; + }) => IOPayloadResult< + MovexClientResourceShape, + unknown // Type this + >; + emitActionDispatch: (payload: { rid: ResourceIdentifier; action: ActionOrActionTupleFromAction; diff --git a/libs/movex-master/src/lib/MovexMasterResource.spec.ts b/libs/movex-master/src/lib/MovexMasterResource.spec.ts index da32ed27..36856aaa 100644 --- a/libs/movex-master/src/lib/MovexMasterResource.spec.ts +++ b/libs/movex-master/src/lib/MovexMasterResource.spec.ts @@ -8,7 +8,7 @@ import { GetReducerAction, computeCheckedState, toResourceIdentifierStr, -} from 'movex-core-util'; +} from 'movex-core-util'; import { MovexMasterResource } from './MovexMasterResource'; import { MemoryMovexStore } from 'movex-store'; @@ -26,7 +26,7 @@ test('gets initial state', async () => { const actualPublic = await master.getPublicState(rid).resolveUnwrap(); const actualByClient = await master - .getState(rid, 'testClient') + .getClientSpecificState(rid, 'testClient') .resolveUnwrap(); const expectedPublic = computeCheckedState(initialCounterState); @@ -56,7 +56,9 @@ test('applies public action', async () => { .resolveUnwrap(); const actualPublic = await master.getPublicState(rid).resolveUnwrap(); - const actualByClient = await master.getState(rid, clientAId).resolveUnwrap(); + const actualByClient = await master + .getClientSpecificState(rid, clientAId) + .resolveUnwrap(); const expectedPublic = computeCheckedState({ ...initialCounterState, @@ -106,11 +108,11 @@ test('applies only one private action w/o getting to reconciliation', async () = const actualPublicState = await master.getPublicState(rid).resolveUnwrap(); const actualSenderState = await master - .getState(rid, senderClientId) + .getClientSpecificState(rid, senderClientId) .resolveUnwrap(); const actualReceiverState = await master - .getState(rid, 'otherClient') + .getClientSpecificState(rid, 'otherClient') .resolveUnwrap(); const expectedPublic = computeCheckedState({ @@ -183,11 +185,11 @@ test('applies private action UNTIL Reconciliation', async () => { .resolveUnwrap(); const actualSenderStateBeforeReconciliation = await master - .getState(rid, whitePlayer) + .getClientSpecificState(rid, whitePlayer) .resolveUnwrap(); const actualReceiverStateBeforeReconciliation = await master - .getState(rid, blackPlayer) + .getClientSpecificState(rid, blackPlayer) .resolveUnwrap(); const expectedPublicStateBeforeReconciliation = computeCheckedState({ @@ -266,11 +268,11 @@ test('applies private action UNTIL Reconciliation', async () => { .resolveUnwrap(); const actualSenderStateAfterReconciliation = await master - .getState(rid, blackPlayer) + .getClientSpecificState(rid, blackPlayer) .resolveUnwrap(); const actualReceiverStateAfterReconciliation = await master - .getState(rid, whitePlayer) + .getClientSpecificState(rid, whitePlayer) .resolveUnwrap(); const expectedPublicStateAfterReconciliation = computeCheckedState({ diff --git a/libs/movex-master/src/lib/MovexMasterResource.ts b/libs/movex-master/src/lib/MovexMasterResource.ts index b9fee874..e045e37d 100644 --- a/libs/movex-master/src/lib/MovexMasterResource.ts +++ b/libs/movex-master/src/lib/MovexMasterResource.ts @@ -10,6 +10,7 @@ import type { CheckedReconciliatoryActions, ToPublicAction, MovexReducer, + MovexClientResourceShape, } from 'movex-core-util'; import { isAction, @@ -18,11 +19,7 @@ import { objectKeys, } from 'movex-core-util'; import { AsyncOk, AsyncResult } from 'ts-async-results'; -import type { - MovexStatePatch, - MovexStore, - MovexStoreItem, -} from 'movex-store'; +import type { MovexStatePatch, MovexStore, MovexStoreItem } from 'movex-store'; import { applyMovexStatePatches, getMovexStatePatch, getUuid } from './util'; /** @@ -74,7 +71,13 @@ export class MovexMasterResource< ); } - getItem( + /** + * Gets the Raw Item as saved in the store - NOT TO BE SENT TO THE CLIENT + * + * @param rid + * @returns + */ + private getStoreItem( rid: ResourceIdentifier ) { return this.store.get(rid); @@ -92,7 +95,7 @@ export class MovexMasterResource< // TODO: Here probably should include the id! // at this level or at the store level? // Sometime the state could have it's own id but not always and it should be given or not? :/ - return this.store.get(rid).map((r) => r.state); + return this.getStoreItem(rid).map((r) => r.state); } /** @@ -103,7 +106,7 @@ export class MovexMasterResource< * @param clientId * @returns */ - getState( + getClientSpecificState( rid: ResourceIdentifier, clientId: MovexClient['id'] ) { @@ -112,10 +115,26 @@ export class MovexMasterResource< .map((item) => this.computeClientState(clientId, item)); } - getStateBySubscriberId( + /** + * This gets the resource with the computed state for the current connected client + * + * @param rid + * @param clientId + */ + getClientSpecificResource( + rid: ResourceIdentifier, + clientId: MovexClient['id'] + ) { + return this.store.get(rid, clientId).map((item) => ({ + ...item, + state: this.computeClientState(clientId, item), + })); + } + + private getStateBySubscriberId( rid: ResourceIdentifier ) { - return this.getItem(rid).map((item) => + return this.getStoreItem(rid).map((item) => this.computeStateForItemSubscribers(item) ); } @@ -179,7 +198,7 @@ export class MovexMasterResource< type PeerActions = ForwardablePeerActions | ReconcilablePeerActions; - return this.getItem(rid).flatMap< + return this.getStoreItem(rid).flatMap< { nextPublic: ToCheckedAction; nextPrivate?: ToCheckedAction; @@ -237,7 +256,7 @@ export class MovexMasterResource< AsyncResult.all( new AsyncOk(itemWithLatestPatch), - this.getState(rid, clientId), + this.getClientSpecificState(rid, clientId), // Apply the Public Action // *Note The Public Action needs to get applied after the private one! @@ -393,7 +412,7 @@ export class MovexMasterResource< // return this.store.get(rid).map((s) => s.state[0]); // } - update( + private update( rid: ResourceIdentifier, nextStateGetter: NextStateGetter ) { diff --git a/libs/movex-master/src/lib/MovexMasterServer.ts b/libs/movex-master/src/lib/MovexMasterServer.ts index ed0b8b17..1cb1cbcd 100644 --- a/libs/movex-master/src/lib/MovexMasterServer.ts +++ b/libs/movex-master/src/lib/MovexMasterServer.ts @@ -8,10 +8,12 @@ import { type ConnectionToClient, AnyResourceIdentifier, AnyStringResourceIdentifier, + MovexClientResourceShape, } from 'movex-core-util'; import { AsyncResult } from 'ts-async-results'; import { Err, Ok } from 'ts-results'; import { MovexMasterResource } from './MovexMasterResource'; +import { storeItemToSanitizedClientResource } from './util'; const logsy = globalLogsy.withNamespace('[MovexMasterServer]'); /** @@ -126,6 +128,34 @@ export class MovexMasterServer { .mapErr(() => acknowledge?.(new Err('UnknownError'))); // TODO: Type this using the ResultError from Matterio }; + const onGetResourceHandler = ( + payload: Parameters['getResourceState']>[0], + acknowledge?: ( + p: ReturnType['getResource']> + ) => void + ) => { + const { rid } = payload; + + const masterResource = + this.masterResourcesByType[toResourceIdentifierObj(rid).resourceType]; + + if (!masterResource) { + return acknowledge?.(new Err('MasterResourceInexistent')); + } + + masterResource + .getClientSpecificResource(rid, clientConnection.clientId) + .map((r) => + acknowledge?.(new Ok(storeItemToSanitizedClientResource(r))) + ) + .mapErr( + AsyncResult.passThrough((e) => { + logsy.error('Get Resource Error', e); + }) + ) + .mapErr((e) => acknowledge?.(new Err(e))); + }; + const onGetResourceStateHandler = ( payload: Parameters['getResourceState']>[0], acknowledge?: ( @@ -142,7 +172,7 @@ export class MovexMasterServer { } masterResource - .getState(rid, clientConnection.clientId) + .getClientSpecificState(rid, clientConnection.clientId) .map((checkedState) => acknowledge?.(new Ok(checkedState))) .mapErr( AsyncResult.passThrough((e) => { @@ -152,6 +182,22 @@ export class MovexMasterServer { .mapErr((e) => acknowledge?.(new Err(e))); }; + const onGetResourceSubscribersHandler = ( + payload: Parameters< + IOEvents['getResourceSubscribers'] + >[0], + acknowledge?: ( + p: ReturnType['getResourceSubscribers']> + ) => void + ) => + onGetResourceHandler(payload, (r) => { + if (r.ok) { + acknowledge?.(new Ok(r.val.subscribers)); + } else { + acknowledge?.(r); + } + }); + const onCreateResourceHandler = ( payload: Parameters['createResource']>[0], acknowledge?: ( @@ -168,12 +214,7 @@ export class MovexMasterServer { masterResource .create(resourceType, resourceState, resourceId) .map((r) => - acknowledge?.( - new Ok({ - rid: r.rid, - state: r.state, - }) - ) + acknowledge?.(new Ok(storeItemToSanitizedClientResource(r))) ) .mapErr( AsyncResult.passThrough((e) => { @@ -212,7 +253,7 @@ export class MovexMasterServer { }; // Send the ack to the just-added-client - acknowledge?.(Ok.EMPTY); + acknowledge?.(new Ok(storeItemToSanitizedClientResource(s))); // Let the rest of the peer-clients know as well objectKeys(s.subscribers) @@ -252,6 +293,11 @@ export class MovexMasterServer { clientConnection.emitter.on('ping', onPingHandler); clientConnection.emitter.on('emitActionDispatch', onEmitActionHandler); + clientConnection.emitter.on('getResource', onGetResourceHandler); + clientConnection.emitter.on( + 'getResourceSubscribers', + onGetResourceSubscribersHandler + ); clientConnection.emitter.on('getResourceState', onGetResourceStateHandler); clientConnection.emitter.on('createResource', onCreateResourceHandler); clientConnection.emitter.on( diff --git a/libs/movex-master/src/lib/util.ts b/libs/movex-master/src/lib/util.ts index 57906bf5..e06a6f55 100644 --- a/libs/movex-master/src/lib/util.ts +++ b/libs/movex-master/src/lib/util.ts @@ -1,6 +1,14 @@ export { v4 as getUuid } from 'uuid'; import { applyReducer, compare, deepClone } from 'fast-json-patch'; -import { JsonPatch, isObject } from 'movex-core-util'; +import { + JsonPatch, + isObject, + GenericResourceType, + MovexClientResourceShape, + toResourceIdentifierStr, + objectKeys, +} from 'movex-core-util'; +import { MovexStoreItem } from 'movex-store'; export const applyMovexStatePatches = ( state: TState, @@ -59,4 +67,23 @@ export function getRandomInt(givenMin: number, givenMax: number) { const max = Math.floor(givenMax); return Math.floor(Math.random() * (max - min + 1)) + min; -} \ No newline at end of file +} + +export const storeItemToSanitizedClientResource = < + TResourceType extends GenericResourceType, + TState +>( + storeItem: MovexStoreItem +): MovexClientResourceShape => { + return { + rid: toResourceIdentifierStr(storeItem.rid), + state: storeItem.state, + subscribers: objectKeys(storeItem.subscribers).reduce( + (prev, next) => ({ + ...prev, + [next]: null, + }), + {} as MovexClientResourceShape['subscribers'] + ), + }; +}; diff --git a/libs/movex-react/src/index.ts b/libs/movex-react/src/index.ts index 1349d692..d4507102 100644 --- a/libs/movex-react/src/index.ts +++ b/libs/movex-react/src/index.ts @@ -7,6 +7,6 @@ export { type MovexContextProps as MovexReactContextProps, } from './lib/MovexContext'; -export * from './lib/MovexBoundResource'; +export { MovexBoundResourceComponent as MovexBoundResource } from './lib/MovexBoundResourceComponent'; export * from './lib/MovexConnection'; export * from './lib/hooks'; diff --git a/libs/movex-react/src/lib/MovexBoundResource.tsx b/libs/movex-react/src/lib/MovexBoundResourceComponent.tsx similarity index 80% rename from libs/movex-react/src/lib/MovexBoundResource.tsx rename to libs/movex-react/src/lib/MovexBoundResourceComponent.tsx index 98390bc9..381a483f 100644 --- a/libs/movex-react/src/lib/MovexBoundResource.tsx +++ b/libs/movex-react/src/lib/MovexBoundResourceComponent.tsx @@ -9,10 +9,19 @@ import type { BaseMovexDefinitionResourcesMap, MovexDefinition, UnsubscribeFn, + DistributiveOmit, } from 'movex-core-util'; import { invoke, isSameResourceIdentifier } from 'movex-core-util'; import { MovexContextStateChange } from './MovexContextStateChange'; +type ReactMovexBoundResource< + TResourcesMap extends BaseMovexDefinitionResourcesMap, + TResourceType extends StringKeys +> = MovexClient.MovexBoundResource< + GetReducerState, + GetReducerAction +>; + type Props< TResourcesMap extends BaseMovexDefinitionResourcesMap, TResourceType extends StringKeys @@ -20,24 +29,15 @@ type Props< movexDefinition: MovexDefinition; rid: ResourceIdentifier; onReady?: (p: { - boundResource: MovexClient.MovexBoundResource< - GetReducerState, - GetReducerAction - >; + boundResource: ReactMovexBoundResource; clientId: MovexClientUser['id']; }) => void; onComponentWillUnmount?: (p: State) => void; onResourceStateUpdated?: ( - p: MovexClient.MovexBoundResource< - GetReducerState, - GetReducerAction - >['state'] + p: ReactMovexBoundResource['state'] ) => void; render: (p: { - boundResource: MovexClient.MovexBoundResource< - GetReducerState, - GetReducerAction - >; + boundResource: ReactMovexBoundResource; clientId: MovexClientUser['id']; }) => React.ReactNode; @@ -54,14 +54,11 @@ type State< } | { init: true; - boundResource: MovexClient.MovexBoundResource< - GetReducerState, - GetReducerAction - >; + boundResource: ReactMovexBoundResource; clientId: MovexClientUser['id']; }; -export class MovexBoundResource< +export class MovexBoundResourceComponent< TResourcesMap extends BaseMovexDefinitionResourcesMap, TResourceType extends StringKeys > extends React.Component< @@ -147,9 +144,9 @@ export class MovexBoundResource< {this.state.init ? this.props.render( this.state as { - boundResource: MovexClient.MovexBoundResource< - GetReducerState, - GetReducerAction + boundResource: ReactMovexBoundResource< + TResourcesMap, + TResourceType >; clientId: MovexClientUser['id']; } diff --git a/libs/movex-react/src/lib/MovexProvider.tsx b/libs/movex-react/src/lib/MovexProvider.tsx index 4eaeda38..5a8fc521 100644 --- a/libs/movex-react/src/lib/MovexProvider.tsx +++ b/libs/movex-react/src/lib/MovexProvider.tsx @@ -7,16 +7,10 @@ import { type BaseMovexDefinitionResourcesMap, type MovexDefinition, ResourceIdentifier, - toResourceIdentifierStr, - ResourceIdentifierStr, - toResourceIdentifierObj, StringKeys, } from 'movex-core-util'; -import { - MovexClient, - MovexFromDefintion, - MovexResourceObservable, -} from 'movex'; +import { MovexClient } from 'movex'; +import { ResourceObservablesRegistry } from './ResourceObservableRegistry'; type Props = React.PropsWithChildren<{ @@ -37,49 +31,6 @@ type Props = ) => void; }>; -class ResourceObservablesRegistry< - TResourcesMap extends BaseMovexDefinitionResourcesMap -> { - private resourceObservablesByRid: { - [rid in ResourceIdentifierStr]: { - _movexResource: MovexClient.MovexResource; - $resource: MovexResourceObservable; - }; - } = {}; - - constructor(private movex: MovexFromDefintion) {} - - private get>( - rid: ResourceIdentifier - ) { - return this.resourceObservablesByRid[toResourceIdentifierStr(rid)]; - } - - register>( - rid: ResourceIdentifier - ) { - const existent = this.get(rid); - - if (existent) { - return existent.$resource; - } - - const ridAsStr = toResourceIdentifierStr(rid); - const { resourceType } = toResourceIdentifierObj(rid); - - const movexResource = this.movex.register(resourceType); - - const $resource = movexResource.bind(rid); - - this.resourceObservablesByRid[ridAsStr] = { - _movexResource: movexResource, - $resource, - }; - - return $resource; - } -} - export const MovexProvider: React.FC< Props > = ({ onConnected = noop, onDisconnected = noop, ...props }) => { @@ -92,6 +43,8 @@ export const MovexProvider: React.FC< clientId: undefined, }); + // TODO: This can all ne moved into the MovexProviderClass and rename it to MovexProviderImplementation + // or this to MovexProviderContainer useEffect(() => { if (contextState.connected) { return; diff --git a/libs/movex-react/src/lib/ResourceObservableRegistry.ts b/libs/movex-react/src/lib/ResourceObservableRegistry.ts new file mode 100644 index 00000000..1bbdfa0c --- /dev/null +++ b/libs/movex-react/src/lib/ResourceObservableRegistry.ts @@ -0,0 +1,56 @@ +import { + type BaseMovexDefinitionResourcesMap, + ResourceIdentifier, + toResourceIdentifierStr, + ResourceIdentifierStr, + toResourceIdentifierObj, + StringKeys, +} from 'movex-core-util'; +import { + MovexClient, + MovexFromDefintion, + MovexResourceObservable, +} from 'movex'; + +export class ResourceObservablesRegistry< + TResourcesMap extends BaseMovexDefinitionResourcesMap +> { + private resourceObservablesByRid: { + [rid in ResourceIdentifierStr]: { + _movexResource: MovexClient.MovexResource; + $resource: MovexResourceObservable; + }; + } = {}; + + constructor(private movex: MovexFromDefintion) {} + + private get>( + rid: ResourceIdentifier + ) { + return this.resourceObservablesByRid[toResourceIdentifierStr(rid)]; + } + + register>( + rid: ResourceIdentifier + ) { + const existent = this.get(rid); + + if (existent) { + return existent.$resource; + } + + const ridAsStr = toResourceIdentifierStr(rid); + const { resourceType } = toResourceIdentifierObj(rid); + + const movexResource = this.movex.register(resourceType); + + const $resource = movexResource.bind(rid); + + this.resourceObservablesByRid[ridAsStr] = { + _movexResource: movexResource, + $resource, + }; + + return $resource; + } +} diff --git a/libs/movex/src/lib/ConnectionToMasterResource.ts b/libs/movex/src/lib/ConnectionToMasterResource.ts index 9a46ac32..6c831506 100644 --- a/libs/movex/src/lib/ConnectionToMasterResource.ts +++ b/libs/movex/src/lib/ConnectionToMasterResource.ts @@ -10,12 +10,13 @@ import type { UnsubscribeFn, IOEvents, ConnectionToMaster, + MovexClient, } from 'movex-core-util'; import { invoke, toResourceIdentifierObj, toResourceIdentifierStr, -} from 'movex-core-util'; +} from 'movex-core-util'; import { Pubsy } from 'ts-pubsy'; import { AsyncResult } from 'ts-async-results'; import { Err, Ok } from 'ts-results'; @@ -28,14 +29,22 @@ export class ConnectionToMasterResource< TAction extends AnyAction, TResourceType extends string > { - private fwdActionPubsy = new Pubsy<{ + private fwdActionEventPubsy = new Pubsy<{ [key in `rid:${ResourceIdentifierStr}`]: ToCheckedAction; }>(); - private reconciliatoryActionPubsy = new Pubsy<{ + private reconciliatoryActionEventPubsy = new Pubsy<{ [key in `rid:${ResourceIdentifierStr}`]: CheckedReconciliatoryActions; }>(); + private subscriberAddedEventPubsy = new Pubsy<{ + [key in `rid:${ResourceIdentifierStr}`]: MovexClient['id']; + }>(); + + private subscriberRemovedEventPubsy = new Pubsy<{ + [key in `rid:${ResourceIdentifierStr}`]: MovexClient['id']; + }>(); + private unsubscribers: UnsubscribeFn[] = []; constructor( @@ -55,7 +64,10 @@ export class ConnectionToMasterResource< return; } - this.fwdActionPubsy.publish(`rid:${toResourceIdentifierStr(p.rid)}`, p); + this.fwdActionEventPubsy.publish( + `rid:${toResourceIdentifierStr(p.rid)}`, + p + ); }; const onReconciliateActionsHandler = ( @@ -67,15 +79,43 @@ export class ConnectionToMasterResource< return; } - this.reconciliatoryActionPubsy.publish( + this.reconciliatoryActionEventPubsy.publish( `rid:${toResourceIdentifierStr(p.rid)}`, p ); }; - const onRemoveResourceSubscriber = () => { + const onRemoveResourceSubscriberHandler = (p: { + rid: ResourceIdentifier; + clientId: MovexClient['id']; + }) => { + console.log('ConnectionToMaster onRemoveResourceSubscriberHandler', p); + if (toResourceIdentifierObj(p.rid).resourceType !== resourceType) { + console.log('ConnectionToMaster onRemoveResourceSubscriberHandler', 'nooooope', p); + return; + } - } + this.subscriberRemovedEventPubsy.publish( + `rid:${toResourceIdentifierStr(p.rid)}`, + p.clientId + ); + }; + const onAddResourceSubscriberHandler = (p: { + rid: ResourceIdentifier; + clientId: MovexClient['id']; + }) => { + console.log('ConnectionToMaster onAddResourceSubscriberHandler', p); + + if (toResourceIdentifierObj(p.rid).resourceType !== resourceType) { + console.log('ConnectionToMaster onAddResourceSubscriberHandler', p, 'noooope'); + return; + } + + this.subscriberAddedEventPubsy.publish( + `rid:${toResourceIdentifierStr(p.rid)}`, + p.clientId + ); + }; connectionToMaster.emitter.on( 'onReconciliateActions', @@ -84,7 +124,15 @@ export class ConnectionToMasterResource< connectionToMaster.emitter.on('onFwdAction', onFwdActionHandler); - connectionToMaster.emitter.on('onResourceSubscriberRemoved', onRemoveResourceSubscriber); + connectionToMaster.emitter.on( + 'onResourceSubscriberRemoved', + onRemoveResourceSubscriberHandler + ); + + connectionToMaster.emitter.on( + 'onResourceSubscriberAdded', + onAddResourceSubscriberHandler + ); // Unsubscribe from the events too this.unsubscribers = [ @@ -94,6 +142,16 @@ export class ConnectionToMasterResource< 'onReconciliateActions', onReconciliateActionsHandler ), + () => + connectionToMaster.emitter.off( + 'onResourceSubscriberRemoved', + onRemoveResourceSubscriberHandler + ), + () => + connectionToMaster.emitter.off( + 'onResourceSubscriberAdded', + onAddResourceSubscriberHandler + ), ]; } @@ -118,13 +176,15 @@ export class ConnectionToMasterResource< }) .then((res) => res.ok - ? new Ok({ - // This sanitizes the data only allowing specific fields to get to the client - // TODO: This actually should come sanitized from the server, since this is on the client - // Probably best to where the ack is called - rid: res.val.rid, - state: res.val.state, - }) + ? // ? new Ok({ + // // This sanitizes the data only allowing specific fields to get to the client + // // TODO: This actually should come sanitized from the server, since this is on the client + // // Probably best to where the ack is called + // // rid: res.val.rid, + // // state: res.val.state, + // ...res.val, + // }) + new Ok(res.val) // TODO: Ensure the data is sanitized from the server : new Err(res.val) ) ); @@ -146,14 +206,14 @@ export class ConnectionToMasterResource< ); } - get(rid: ResourceIdentifier) { - type GetEvent = ReturnType< + getState(rid: ResourceIdentifier) { + type GetStateEvent = ReturnType< IOEvents['getResourceState'] >; return AsyncResult.toAsyncResult< - GetIOPayloadOKTypeFrom, - GetIOPayloadErrTypeFrom + GetIOPayloadOKTypeFrom, + GetIOPayloadErrTypeFrom >( this.connectionToMaster.emitter .emitAndAcknowledge('getResourceState', { rid }) @@ -161,6 +221,36 @@ export class ConnectionToMasterResource< ); } + getSubscribers(rid: ResourceIdentifier) { + type GetSubscribersEvent = ReturnType< + IOEvents['getResourceSubscribers'] + >; + + return AsyncResult.toAsyncResult< + GetIOPayloadOKTypeFrom, + GetIOPayloadErrTypeFrom + >( + this.connectionToMaster.emitter + .emitAndAcknowledge('getResourceSubscribers', { rid }) + .then((res) => (res.ok ? new Ok(res.val) : new Err(res.val))) + ); + } + + getResource(rid: ResourceIdentifier) { + type GetResourceEvent = ReturnType< + IOEvents['getResource'] + >; + + return AsyncResult.toAsyncResult< + GetIOPayloadOKTypeFrom, + GetIOPayloadErrTypeFrom + >( + this.connectionToMaster.emitter + .emitAndAcknowledge('getResource', { rid }) + .then((res) => (res.ok ? new Ok(res.val) : new Err(res.val))) + ); + } + emitAction( rid: ResourceIdentifier, actionOrActionTuple: ActionOrActionTupleFromAction @@ -186,7 +276,7 @@ export class ConnectionToMasterResource< rid: ResourceIdentifier, fn: (p: ToCheckedAction) => void ) { - return this.fwdActionPubsy.subscribe( + return this.fwdActionEventPubsy.subscribe( `rid:${toResourceIdentifierStr(rid)}`, fn ); @@ -196,7 +286,27 @@ export class ConnectionToMasterResource< rid: ResourceIdentifier, fn: (p: CheckedReconciliatoryActions) => void ) { - return this.reconciliatoryActionPubsy.subscribe( + return this.reconciliatoryActionEventPubsy.subscribe( + `rid:${toResourceIdentifierStr(rid)}`, + fn + ); + } + + onSubscriberAdded( + rid: ResourceIdentifier, + fn: (clientId: MovexClient['id']) => void + ) { + return this.subscriberAddedEventPubsy.subscribe( + `rid:${toResourceIdentifierStr(rid)}`, + fn + ); + } + + onSubscriberRemoved( + rid: ResourceIdentifier, + fn: (clientId: MovexClient['id']) => void + ) { + return this.subscriberRemovedEventPubsy.subscribe( `rid:${toResourceIdentifierStr(rid)}`, fn ); diff --git a/libs/movex/src/lib/MovexBoundResource.ts b/libs/movex/src/lib/MovexBoundResource.ts index 983a7dd7..90dea905 100644 --- a/libs/movex/src/lib/MovexBoundResource.ts +++ b/libs/movex/src/lib/MovexBoundResource.ts @@ -31,17 +31,16 @@ export class MovexBoundResource< }; get state() { - return this.getState(); + return this.observable.getUncheckedState(); } - private getState = () => { - return this.observable.getUncheckedState(); - }; + get subscribers() { + return this.observable.get().subscribers; + } - // TODO: Should this give access to the all the subscribers to the client movex?? - // get subscribers() { - // // return - // } + get item() { + return this.observable.get(); + } // This to be called when not used anymore in order to clean the update subscriptions destroy = () => { diff --git a/libs/movex/src/lib/MovexResource.ts b/libs/movex/src/lib/MovexResource.ts index ac3b5f99..decd029e 100644 --- a/libs/movex/src/lib/MovexResource.ts +++ b/libs/movex/src/lib/MovexResource.ts @@ -7,14 +7,14 @@ import type { CheckedReconciliatoryActions, MovexReducer, IOConnection, -} from 'movex-core-util'; +} from 'movex-core-util'; import { logsy as rawLogsy, toResourceIdentifierObj, toResourceIdentifierStr, isAction, invoke, -} from 'movex-core-util'; +} from 'movex-core-util'; import { ConnectionToMasterResource } from './ConnectionToMasterResource'; import { MovexResourceObservable } from './MovexResourceObservable'; import * as deepObject from 'deep-object-diff'; @@ -62,17 +62,20 @@ export class MovexResource< return this.connectionToMasterResource .create(this.resourceType, state, resourceId) .map((item) => ({ + ...item, rid: toResourceIdentifierObj(item.rid), state: item.state[0], })); } get(rid: ResourceIdentifier) { - return this.connectionToMasterResource.get(rid).map(([state]) => ({ - // This is rempaed in this way in order to return the same payload as "create" - rid, - state, - })); + return this.connectionToMasterResource.getResource(rid); + // .map(({ rid, subscribers, }) => ({ + // ...resource, + // // This is rempaed in this way in order to return the same payload as "create" + // // rid, + // // state, + // })); // TODO: Once a client can bind multiple times this could also sync with the obsservable // .map((s) => { @@ -82,12 +85,10 @@ export class MovexResource< } /** - * This returns the actual MovexClientResource. The name "use" doesn't seem to be perfect yet - * but not sure what to use yet. "observe", "listenTo", "attach", "follow" ?:) - * I think "bind" works pretty well! :D + * Connect the Master to the Client resource * * @param rid - * @returns + * @returns MovexResourceObservable */ bind(rid: ResourceIdentifier) { // TODO: @@ -101,14 +102,16 @@ export class MovexResource< this.reducer ); + // resourceObservable.$subscribers.get() + // TODO: Fix this!!! // resourceObservable.setMasterSyncing(false); const syncLocalState = () => { return this.connectionToMasterResource - .get(rid) + .getState(rid) .map((masterCheckState) => { - resourceObservable.sync(masterCheckState); + resourceObservable.syncState(masterCheckState); return masterCheckState; }); @@ -151,11 +154,14 @@ export class MovexResource< this.connectionToMasterResource .addResourceSubscriber(rid) - .map(() => { - // TODO: This is where the issue is. the master never responds - + .map((res) => { // TODO: This could be optimized to be returned from the "addResourceSubscriber" directly - syncLocalState(); + // syncLocalState(); + + // Added on April 1st + // TODO: This can be improved to update the whole resource or smtg like that, also to look at the sync and think should the subscribers also sync + resourceObservable.syncState(res.state); + resourceObservable.updateSubscribers(res.subscribers); }) .mapErr((e) => { logsy.error('Add Resource Subscriber Error', e); @@ -164,7 +170,7 @@ export class MovexResource< const onReconciliateActionsHandler = ( p: CheckedReconciliatoryActions ) => { - const prevState = resourceObservable.get(); + const prevState = resourceObservable.getCheckedState(); logsy.group( `%c\u{25BC} %cReconciliatory Actions Received (${p.actions.length}). FinalCheckum ${p.finalChecksum}`, @@ -175,7 +181,9 @@ export class MovexResource< ); logsy.log('%cPrev state', logUnimportantStyle, prevState[0]); - const nextState = resourceObservable.applyMultipleActions(p.actions); + const nextState = resourceObservable.applyMultipleActions( + p.actions + ).checkedState; p.actions.forEach((action, i) => { logsy.log( @@ -227,72 +235,9 @@ export class MovexResource< this.unsubscribersByRid[toResourceIdentifierStr(rid)] = [ resourceObservable.onDispatched( - ({ - action, - next: nextLocalCheckedState, - prev: prevLocalCheckedState, - }) => { + ({ action, next: nextLocalCheckedState }) => { const [nextLocalState, nextLocalChecksum] = nextLocalCheckedState; - logsy.group( - `%c\u{25B2} %cAction Dispatched: %c${ - isAction(action) - ? action.type - : action[0].type + ' + ' + action[1].type - }`, - logOutgoingStyle, - logUnimportantStyle, - logImportantStyle, - 'Client:', - this.connectionToMaster.clientId - ); - logsy.log( - '%cPrev state', - logUnimportantStyle, - prevLocalCheckedState[0] - ); - if (isAction(action)) { - logsy.log( - `%cPublic Action: %c${action.type}`, - logOutgoingStyle, - logImportantStyle, - (action as ActionWithAnyPayload).payload - ); - } else { - const [privateAction, publicAction] = action; - logsy.log( - `%cPrivate Action: %c${privateAction.type}`, - logOpenConnectionStyle, - logImportantStyle, - (privateAction as ActionWithAnyPayload).payload - ); - logsy.log( - `%cPublic Action payload: %c${publicAction.type}`, - logOutgoingStyle, - logImportantStyle, - (publicAction as ActionWithAnyPayload).payload - ); - } - logsy.log('%cNext State', logIncomingStyle, nextLocalState); - - if (prevLocalCheckedState[1] !== nextLocalChecksum) { - logsy.log( - '%cChecksums', - logUnimportantStyle, - prevLocalCheckedState[1], - '>', - nextLocalChecksum - ); - } else { - logsy.log( - '%cNo Diff', - logUnimportantStyle, - prevLocalCheckedState[1] - ); - } - - logsy.groupEnd(); - this.connectionToMasterResource .emitAction(rid, action) .map(async (master) => { @@ -343,11 +288,11 @@ export class MovexResource< } ), this.connectionToMasterResource.onFwdAction(rid, (p) => { - const prevState = resourceObservable.get(); + const prevState = resourceObservable.getCheckedState(); resourceObservable.reconciliateAction(p); - const nextState = resourceObservable.get(); + const nextState = resourceObservable.getCheckedState(); logsy.group( `%c\u{25BC} %cForwarded Action Received: %c${p.action.type}`, @@ -383,14 +328,105 @@ export class MovexResource< rid, onReconciliateActionsHandler ), + + // Subscribers + this.connectionToMasterResource.onSubscriberAdded(rid, (clientId) => { + logsy.log('Subscriber Added', clientId); + + resourceObservable.updateSubscribers((prev) => ({ + ...prev, + [clientId]: null, + })); + }), + this.connectionToMasterResource.onSubscriberRemoved(rid, (clientId) => { + logsy.log('Subscriber Removed', clientId); + + resourceObservable.updateSubscribers((prev) => { + const { [clientId]: removed, ...rest } = prev; + + return rest; + }); + }), + + // Destroyers + // Add the client resource destroy to the list of unsubscribers () => resourceObservable.destroy(), // Add the master Resource Destroy as well () => this.connectionToMasterResource.destroy(), + + // Logger + resourceObservable.onDispatched( + ({ + action, + next: nextLocalCheckedState, + prev: prevLocalCheckedState, + }) => { + const [nextLocalState, nextLocalChecksum] = nextLocalCheckedState; + + logsy.group( + `%c\u{25B2} %cAction Dispatched: %c${ + isAction(action) + ? action.type + : action[0].type + ' + ' + action[1].type + }`, + logOutgoingStyle, + logUnimportantStyle, + logImportantStyle, + 'Client:', + this.connectionToMaster.clientId + ); + logsy.log( + '%cPrev state', + logUnimportantStyle, + prevLocalCheckedState[0] + ); + if (isAction(action)) { + logsy.log( + `%cPublic Action: %c${action.type}`, + logOutgoingStyle, + logImportantStyle, + (action as ActionWithAnyPayload).payload + ); + } else { + const [privateAction, publicAction] = action; + logsy.log( + `%cPrivate Action: %c${privateAction.type}`, + logOpenConnectionStyle, + logImportantStyle, + (privateAction as ActionWithAnyPayload).payload + ); + logsy.log( + `%cPublic Action payload: %c${publicAction.type}`, + logOutgoingStyle, + logImportantStyle, + (publicAction as ActionWithAnyPayload).payload + ); + } + logsy.log('%cNext State', logIncomingStyle, nextLocalState); + + if (prevLocalCheckedState[1] !== nextLocalChecksum) { + logsy.log( + '%cChecksums', + logUnimportantStyle, + prevLocalCheckedState[1], + '>', + nextLocalChecksum + ); + } else { + logsy.log( + '%cNo Diff', + logUnimportantStyle, + prevLocalCheckedState[1] + ); + } + + logsy.groupEnd(); + } + ), ]; - // return new MovexBoundResource(resourceObservable); return resourceObservable; } diff --git a/libs/movex/src/lib/MovexResourceObservable.spec.ts b/libs/movex/src/lib/MovexResourceObservable.spec.ts index 6edfc8ca..98de4d36 100644 --- a/libs/movex/src/lib/MovexResourceObservable.spec.ts +++ b/libs/movex/src/lib/MovexResourceObservable.spec.ts @@ -4,7 +4,7 @@ import { globalLogsy, ResourceIdentifier, computeCheckedState, -} from 'movex-core-util'; +} from 'movex-core-util'; import { tillNextTick, counterReducer, @@ -21,149 +21,148 @@ afterAll(() => { globalLogsy.enable(); }); -describe('Observable', () => { - test('Dispatch Local Actions', async () => { - const xResource = new MovexResourceObservable( +test('Dispatch Local Actions', async () => { + const $resource = new MovexResourceObservable( + 'test-client', + rid, + counterReducer + ); + $resource.setMasterSyncing(false); + + $resource.dispatch({ + type: 'increment', + }); + + await tillNextTick(); + + expect($resource.getUncheckedState()).toEqual({ count: 1 }); + + $resource.onUpdate((next) => { + expect(next.checkedState).toEqual( + computeCheckedState({ + count: 4, + }) + ); + }); + + $resource.dispatch({ + type: 'incrementBy', + payload: 3, + }); + + await tillNextTick(); + + expect($resource.getUncheckedState()).toEqual({ count: 4 }); +}); + +describe('External Updates', () => { + test('updates the unchecked state', async () => { + const $resource = new MovexResourceObservable( 'test-client', rid, counterReducer ); - xResource.setMasterSyncing(false); + $resource.setMasterSyncing(false); - xResource.dispatch({ + $resource.dispatch({ type: 'increment', }); await tillNextTick(); - expect(xResource.getUncheckedState()).toEqual({ count: 1 }); + expect($resource.getUncheckedState()).toEqual({ count: 1 }); - xResource.onUpdate((nextCheckedState) => { - expect(nextCheckedState).toEqual( - computeCheckedState({ - count: 4, - }) - ); + $resource.updateUncheckedState({ + count: 40, }); - xResource.dispatch({ - type: 'incrementBy', - payload: 3, + expect($resource.getUncheckedState()).toEqual({ count: 40 }); + + $resource.dispatch({ + type: 'decrement', }); await tillNextTick(); - expect(xResource.getUncheckedState()).toEqual({ count: 4 }); + expect($resource.getUncheckedState()).toEqual({ count: 39 }); }); - describe('External Updates', () => { - test('updates the unchecked state', async () => { - const xResource = new MovexResourceObservable( + describe('Reconciliate Action', () => { + test('Updates when matching', () => { + const $resource = new MovexResourceObservable( 'test-client', rid, counterReducer ); - xResource.setMasterSyncing(false); - - xResource.dispatch({ - type: 'increment', - }); - - await tillNextTick(); - - expect(xResource.getUncheckedState()).toEqual({ count: 1 }); - - xResource.updateUncheckedState({ - count: 40, - }); - expect(xResource.getUncheckedState()).toEqual({ count: 40 }); + const updateSpy = jest.fn(); + $resource.onUpdate(updateSpy); - xResource.dispatch({ - type: 'decrement', + const [incrementedState, incrementedStateChecksum] = computeCheckedState({ + ...initialCounterState, + count: initialCounterState.count + 1, }); - await tillNextTick(); - - expect(xResource.getUncheckedState()).toEqual({ count: 39 }); - }); - - describe('Reconciliate Action', () => { - test('Updates when matching', () => { - const xResource = new MovexResourceObservable( - 'test-client', - rid, - counterReducer - ); - - const updateSpy = jest.fn(); - xResource.onUpdate(updateSpy); - - const [incrementedState, incrementedStateChecksum] = - computeCheckedState({ - ...initialCounterState, - count: initialCounterState.count + 1, - }); - - const actual = xResource.reconciliateAction({ - action: { - type: 'increment', - }, - checksum: incrementedStateChecksum, - }); - - expect(actual).toEqual( - new Ok([incrementedState, incrementedStateChecksum]) - ); - - expect(updateSpy).toHaveBeenCalledWith([ - incrementedState, - incrementedStateChecksum, - ]); + const actual = $resource.reconciliateAction({ + action: { + type: 'increment', + }, + checksum: incrementedStateChecksum, }); - test('Fails when NOT matching and does not update', () => { - const xResource = new MovexResourceObservable( - 'test-client', - rid, - counterReducer - ); - - const updateSpy = jest.fn(); - xResource.onUpdate(updateSpy); - - const actual = xResource.reconciliateAction({ - action: { - type: 'increment', - }, - checksum: 'wrong_checksum', - }); + expect(actual).toEqual( + new Ok([incrementedState, incrementedStateChecksum]) + ); - expect(actual.val).toEqual('ChecksumMismatch'); + expect(updateSpy).toHaveBeenCalledWith({ + subscribers: {}, + checkedState: [incrementedState, incrementedStateChecksum], }); }); - test('Destroys the observable', async () => { - const xResource = new MovexResourceObservable( + test('Fails when NOT matching and does not update', () => { + const $resource = new MovexResourceObservable( 'test-client', rid, counterReducer ); - xResource.setMasterSyncing(false); - const updateListener = jest.fn(); - xResource.onUpdate(updateListener); - expect(updateListener).not.toHaveBeenCalled(); + const updateSpy = jest.fn(); + $resource.onUpdate(updateSpy); - xResource.destroy(); + const actual = $resource.reconciliateAction({ + action: { + type: 'increment', + }, + checksum: 'wrong_checksum', + }); - await tillNextTick(); + expect(actual.val).toEqual('ChecksumMismatch'); + }); + }); - xResource.dispatch({ - type: 'increment', - }); + test('Destroys the observable', async () => { + const $resource = new MovexResourceObservable( + 'test-client', + rid, + counterReducer + ); + $resource.setMasterSyncing(false); + const updateListener = jest.fn(); + $resource.onUpdate(updateListener); + + expect(updateListener).not.toHaveBeenCalled(); + + $resource.destroy(); - expect(updateListener).not.toHaveBeenCalled(); + await tillNextTick(); + + $resource.dispatch({ + type: 'increment', }); + + expect(updateListener).not.toHaveBeenCalled(); }); }); + +// TODO: Add some tests with the subscibers diff --git a/libs/movex/src/lib/MovexResourceObservable.ts b/libs/movex/src/lib/MovexResourceObservable.ts index 3456cda1..7bf394a8 100644 --- a/libs/movex/src/lib/MovexResourceObservable.ts +++ b/libs/movex/src/lib/MovexResourceObservable.ts @@ -1,7 +1,6 @@ import { Pubsy } from 'ts-pubsy'; import { Err, Ok, Result } from 'ts-results'; import { - getNextStateFrom, invoke, logsy, computeCheckedState, @@ -25,23 +24,32 @@ import type { import { createDispatcher, DispatchedEvent } from './dispatch'; import { PromiseDelegate } from 'promise-delegate'; +type ObservedItem = { + subscribers: Record; + checkedState: CheckedState; +}; + /** * This is the MovexResource running on the Client */ export class MovexResourceObservable< TState = any, TAction extends AnyAction = AnyAction -> implements IObservable> +> implements IObservable> { /** - * This flag simply states if the resource is synched with the remote. + * This flag informs that the resource (client) is in sync with the master. * It starts as false and it waits for at least one "sync" or "update" call. * * For now it never goes from true to false but that could be a valid use case in the future. */ private isSynchedPromiseDelegate = new PromiseDelegate(); - private $checkedState: Observable>; + // private $checkedState: Observable>; + + private $item: Observable>; + + // public $subscribers: Observable<>; private pubsy = new Pubsy<{ onDispatched: DispatchedEvent, TAction>; @@ -60,12 +68,17 @@ export class MovexResourceObservable< public rid: ResourceIdentifier, private reducer: MovexReducer, + initialSubscribers: Record = {}, + // Passing undefined here in order to get the default state initialCheckedState = computeCheckedState( reducer(undefined as TState, { type: '_init' } as TAction) ) ) { - this.$checkedState = new Observable(initialCheckedState); + this.$item = new Observable({ + subscribers: initialSubscribers, + checkedState: initialCheckedState, + }); // // When the master io returns the async state, update it! // masterConnection.getResourceState().map((state) => { @@ -75,10 +88,32 @@ export class MovexResourceObservable< // TODO: Add Use case for how to deal with creation. When the get doesn't return anything?! // something like: if it returns record doesnt exist, use create instead of get? or io.getOrCreate()? + // const { dispatch, unsubscribe: unsubscribeFromDispatch } = createDispatcher< + // TState, + // TAction + // >(this.$checkedState, this.reducer, { + // onDispatched: (p) => { + // this.pubsy.publish('onDispatched', p); + // }, + // }); + + const $checkedState = this.$item.map((s) => s.checkedState); + + // Propagate the updates up to the root $item, but ensure it doesn't get into a loop + $checkedState.onUpdate((nextCheckedState) => { + // Ensure it's not equal otherwise it goes into an infinite updating loop + if (nextCheckedState !== this.$item.get().checkedState) { + this.$item.update((prev) => ({ + ...prev, + checkedState: nextCheckedState, + })); + } + }); + const { dispatch, unsubscribe: unsubscribeFromDispatch } = createDispatcher< TState, TAction - >(this.$checkedState, this.reducer, { + >($checkedState, this.reducer, { onDispatched: (p) => { this.pubsy.publish('onDispatched', p); }, @@ -116,7 +151,12 @@ export class MovexResourceObservable< this.getUncheckedState() ); - return this.$checkedState.update(computeCheckedState(nextState)).get(); + return this.$item + .update((prev) => ({ + ...prev, + checkedState: computeCheckedState(nextState), + })) + .get(); } /** @@ -138,7 +178,11 @@ export class MovexResourceObservable< return new Err('ChecksumMismatch'); } - this.$checkedState.update(nextCheckedState); + // this.$checkedState.update(nextCheckedState); + this.$item.update((prev) => ({ + ...prev, + checkedState: nextCheckedState, + })); return new Ok(nextCheckedState); } @@ -166,9 +210,15 @@ export class MovexResourceObservable< * @param fn * @returns */ - onUpdate(fn: (state: CheckedState) => void) { + onUpdate( + fn: (p: { + subscribers: Record; + checkedState: CheckedState; + }) => void + ) { // return this.$checkedState.onUpdate(([state]) => fn(state)); - return this.$checkedState.onUpdate(fn); + // return this.$checkedState.onUpdate(fn); + return this.$item.onUpdate(fn); } onDispatched( @@ -178,11 +228,19 @@ export class MovexResourceObservable< } get() { - return this.$checkedState.get(); + // return this.$checkedState.get(); + return this.$item.get(); + } + + getCheckedState() { + return this.get().checkedState; } getUncheckedState() { - return this.$checkedState.get()[0]; + // return this.$checkedState.get()[0]; + // this.get().checkedState[0]; + return this.getCheckedState()[0]; + // return this.$item.get()[0]; } // This is the actual checked state. TODO: Not sure about the names yet @@ -196,15 +254,14 @@ export class MovexResourceObservable< } /** - * This needs to be called each time master has emits an updated state. + * This needs to be called each time master emits an updated state. * The dispatch won't work without it being called at least once. * * @param nextStateGetter * @returns */ - sync(nextStateGetter: NextStateGetter>) { - const res = this.update(nextStateGetter); - + syncState(nextStateGetter: NextStateGetter>) { + const res = this.updateCheckedState(nextStateGetter); this.isSynchedPromiseDelegate.resolve(); return res; @@ -230,18 +287,41 @@ export class MovexResourceObservable< } } - update(nextStateGetter: NextStateGetter>) { - this.$checkedState.update(getNextStateFrom(this.get(), nextStateGetter)); + update(nextStateGetter: NextStateGetter>) { + this.$item.update((prev) => + Observable.getNextStateFrom(prev, nextStateGetter) + ); + } - return this; + updateCheckedState(nextStateGetter: NextStateGetter>) { + return this.update((prev) => ({ + ...prev, + checkedState: Observable.getNextStateFrom( + prev.checkedState, + nextStateGetter + ), + })); } updateUncheckedState(nextStateGetter: NextStateGetter) { - return this.update( - computeCheckedState( - getNextStateFrom(this.getUncheckedState(), nextStateGetter) - ) - ); + return this.update((prev) => ({ + ...prev, + checkedState: computeCheckedState( + Observable.getNextStateFrom(prev.checkedState[0], nextStateGetter) + ), + })); + } + + updateSubscribers( + nextStateGetter: NextStateGetter['subscribers']> + ) { + return this.update((prev) => ({ + ...prev, + subscribers: Observable.getNextStateFrom( + prev.subscribers, + nextStateGetter + ), + })); } // This to be called when the obervable is not used anymore in order to clean the update subscriptions diff --git a/libs/movex/src/lib/dispatch.ts b/libs/movex/src/lib/dispatch.ts index 9df88d39..be8f03b7 100644 --- a/libs/movex/src/lib/dispatch.ts +++ b/libs/movex/src/lib/dispatch.ts @@ -4,7 +4,7 @@ import { checkedStateEquals, computeCheckedState, isAction, -} from 'movex-core-util'; +} from 'movex-core-util'; import type { Observable, CheckedState, @@ -13,7 +13,7 @@ import type { AnyActionOrActionTuple, AnyActionTuple, MovexReducer, -} from 'movex-core-util'; +} from 'movex-core-util'; export type DispatchFn = (actionOrActionTuple: AnyActionOrActionTuple) => void; From fd128043ac1f7a8b37f6546a0c41ec27f2cadc8e Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Wed, 3 Apr 2024 17:14:48 -0600 Subject: [PATCH 06/25] refactor(movex-core-util): :recycle: refactor the "setClientIt" event name and listener --- .../movex-core-util/src/lib/ScketIOEmitter.ts | 20 ------------------- .../src/lib/io/ConnectionToClient.ts | 10 +++++++--- libs/movex-core-util/src/lib/io/IOEvents.ts | 1 + libs/movex/src/lib/init.ts | 3 ++- 4 files changed, 10 insertions(+), 24 deletions(-) diff --git a/libs/movex-core-util/src/lib/ScketIOEmitter.ts b/libs/movex-core-util/src/lib/ScketIOEmitter.ts index 04c9b36e..7028621c 100644 --- a/libs/movex-core-util/src/lib/ScketIOEmitter.ts +++ b/libs/movex-core-util/src/lib/ScketIOEmitter.ts @@ -1,5 +1,4 @@ import { Err, Ok } from 'ts-results'; -import { Pubsy } from 'ts-pubsy'; import { logsy } from './Logsy'; import type { Socket as ServerSocket } from 'socket.io'; import type { Socket as ClientSocket } from 'socket.io-client'; @@ -12,10 +11,6 @@ export type SocketIO = ServerSocket | ClientSocket; export class SocketIOEmitter implements EventEmitter { - private pubsy = new Pubsy<{ - onReceivedClientId: string; - }>(); - private logger: typeof console; constructor( private socket: SocketIO, @@ -26,17 +21,6 @@ export class SocketIOEmitter ) { this.logger = config.logger || console; this.config.waitForResponseMs = this.config.waitForResponseMs || 15 * 1000; - - // This might need to be moved from here into the master connection or somewhere client specific! - this.socket.onAny((ev, clientId) => { - if (ev === '$setClientId' && typeof clientId === 'string') { - this.pubsy.publish('onReceivedClientId', clientId); - } - }); - } - - onReceivedClientId(fn: (clientId: string) => void) { - return this.pubsy.subscribe('onReceivedClientId', fn); } on( @@ -46,8 +30,6 @@ export class SocketIOEmitter ack?: (r: ReturnType) => void ) => void ): this { - // logsy.debug('[SocketEmitter] subscribed to:', event); - this.socket.on(event as string, listener); return this; @@ -60,8 +42,6 @@ export class SocketIOEmitter ack?: (r: ReturnType) => void ) => void ): this { - // logsy.debug('[SocketEmitter] unsubscribed to:', event); - this.socket.off(event as string, listener); return this; diff --git a/libs/movex-core-util/src/lib/io/ConnectionToClient.ts b/libs/movex-core-util/src/lib/io/ConnectionToClient.ts index 2bc080ac..2de8dcc6 100644 --- a/libs/movex-core-util/src/lib/io/ConnectionToClient.ts +++ b/libs/movex-core-util/src/lib/io/ConnectionToClient.ts @@ -5,6 +5,10 @@ export class ConnectionToClient< TState, TAction extends AnyAction, TResourceType extends string -> extends BaseConnection {} - -// console.log('ConnectionToClient class', ConnectionToClient); \ No newline at end of file +> extends BaseConnection { + // This needs to be called right away as the connection gets created + // TODO: Might consider moving into the constructor + emitClientId() { + this.emitter.emit('setClientId', this.clientId); + } +} diff --git a/libs/movex-core-util/src/lib/io/IOEvents.ts b/libs/movex-core-util/src/lib/io/IOEvents.ts index f79e9a92..0f68f615 100644 --- a/libs/movex-core-util/src/lib/io/IOEvents.ts +++ b/libs/movex-core-util/src/lib/io/IOEvents.ts @@ -22,6 +22,7 @@ export type IOEvents< /** * The following events are directed from Client to Master * */ + setClientId: (clientId: string) => void; createResource: (p: { resourceType: TResourceType; diff --git a/libs/movex/src/lib/init.ts b/libs/movex/src/lib/init.ts index 251c8885..2fe1674c 100644 --- a/libs/movex/src/lib/init.ts +++ b/libs/movex/src/lib/init.ts @@ -48,7 +48,8 @@ export const initMovex = ( const emitter = new SocketIOEmitter(socket); - emitter.onReceivedClientId((clientId) => { + emitter.on('setClientId', (clientId) => { + // This might need to be moved from here into the master connection or somewhere client specific! const movex = new MovexFromDefintion( movexDefinition, new ConnectionToMaster(clientId, emitter) From 3f614fe01b9341e1e167817a85e723e9e0fb46d5 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Wed, 3 Apr 2024 17:16:47 -0600 Subject: [PATCH 07/25] feat(movex-server): :sparkles: add an api route to GET individual resources --- libs/movex-server/src/lib/movex-server.ts | 64 +++++++++++++++++++++-- libs/movex-server/src/lib/util.ts | 6 +++ 2 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 libs/movex-server/src/lib/util.ts diff --git a/libs/movex-server/src/lib/movex-server.ts b/libs/movex-server/src/lib/movex-server.ts index 75b24f35..8d8e97ed 100644 --- a/libs/movex-server/src/lib/movex-server.ts +++ b/libs/movex-server/src/lib/movex-server.ts @@ -8,9 +8,13 @@ import { ConnectionToClient, type MovexDefinition, type IOEvents, + isResourceIdentifier, + objectKeys, + toResourceIdentifierObj, } from 'movex-core-util'; import { MemoryMovexStore, MovexStore } from 'movex-store'; import { initMovexMaster } from 'movex-master'; +import { isOneOf } from './util'; export const movexServer = ( { @@ -37,7 +41,10 @@ export const movexServer = ( }, }); - const store = movexStore === 'memory' ? new MemoryMovexStore() : movexStore; + const store = + movexStore === 'memory' + ? new MemoryMovexStore() + : movexStore; const movexMaster = initMovexMaster(definition, store); @@ -48,14 +55,14 @@ export const movexServer = ( const clientId = getClientId(io.handshake.query['clientId'] as string); logsy.log('[MovexServer] Client Connected', clientId); - const connection = new ConnectionToClient( + const connectionToClient = new ConnectionToClient( clientId, new SocketIOEmitter(io) ); - io.emit('$setClientId', clientId); + connectionToClient.emitClientId(); - movexMaster.addClientConnection(connection); + movexMaster.addClientConnection(connectionToClient); io.on('disconnect', () => { logsy.log('[MovexServer] Client Disconnected', clientId); @@ -69,10 +76,57 @@ export const movexServer = ( }); app.get('/store', async (_, res) => { + store.all().map((data) => { + res.header('Content-Type', 'application/json'); + res.send(JSON.stringify(data, null, 4)); + }); + }); + + // Resources + // app.get('/api/resources', async (_, res) => { + // res.header('Content-Type', 'application/json'); + // res.send(JSON.stringify(await store.all().resolveUnwrap(), null, 4)); + // }); + + app.get('/api/resources/:rid', async (req, res) => { + const rawRid = req.params.rid; + + if (!isResourceIdentifier(rawRid)) { + return res.sendStatus(400); // Bad Request + } + + const ridObj = toResourceIdentifierObj(rawRid); + + if (!isOneOf(ridObj.resourceType, objectKeys(definition.resources))) { + return res.sendStatus(400); // Bad Request + } + res.header('Content-Type', 'application/json'); - res.send(JSON.stringify(await store.all().resolveUnwrap(), null, 4)); + + return store + .get(rawRid as any) // TODO: Not sure why this doesn't see it and needs to be casted to any? + .map((data) => { + res.send(JSON.stringify(data, null, 4)); + }) + .mapErr(() => { + res.sendStatus(404); + }); }); + // app.post('/api/resources', async (req, res) => { + // // const rawRid = req.params.rid; + // req + + // if (isResourceIdentifier(rawRid)) { + // res.header('Content-Type', 'application/json'); + // res.send( + // JSON.stringify(await store.get(rawRid as any).resolveUnwrap(), null, 4) + // ); + // } else { + // res.sendStatus(400); + // } + // }); + // //start our server const port = process.env['port'] || 3333; httpServer.listen(port, () => { diff --git a/libs/movex-server/src/lib/util.ts b/libs/movex-server/src/lib/util.ts new file mode 100644 index 00000000..3f079f86 --- /dev/null +++ b/libs/movex-server/src/lib/util.ts @@ -0,0 +1,6 @@ +type TupleToUnionType = T[number]; + +export const isOneOf = ( + k: T | undefined, + listOfOptions: List +): k is TupleToUnionType => !!k && listOfOptions.indexOf(k) > -1; From a120dd10388d42d7606fbd99ae6a377e7dd99542 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Wed, 3 Apr 2024 17:17:35 -0600 Subject: [PATCH 08/25] refactor(movex-react): :recycle: fixed typos --- libs/movex-react/src/lib/MovexProvider.tsx | 4 ++-- libs/movex-react/src/lib/ResourceObservableRegistry.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/libs/movex-react/src/lib/MovexProvider.tsx b/libs/movex-react/src/lib/MovexProvider.tsx index 5a8fc521..8991ad08 100644 --- a/libs/movex-react/src/lib/MovexProvider.tsx +++ b/libs/movex-react/src/lib/MovexProvider.tsx @@ -62,7 +62,7 @@ export const MovexProvider: React.FC< const clientId = movex.getClientId(); - // This resets each time movex reinitiated + // This resets each time movex re-initiates const resourceRegistry = new ResourceObservablesRegistry(movex); const nextState = { @@ -87,7 +87,7 @@ export const MovexProvider: React.FC< setContextState(nextState); }); - // TODO: Maye add destroyer? + // TODO: Maybe add destroyer? }, [props.endpointUrl, props.clientId]); const didPreviouslyConnect = useRef(false); diff --git a/libs/movex-react/src/lib/ResourceObservableRegistry.ts b/libs/movex-react/src/lib/ResourceObservableRegistry.ts index 1bbdfa0c..f22fbf44 100644 --- a/libs/movex-react/src/lib/ResourceObservableRegistry.ts +++ b/libs/movex-react/src/lib/ResourceObservableRegistry.ts @@ -12,6 +12,9 @@ import { MovexResourceObservable, } from 'movex'; +/** + * This is where all of the resourceObservables are being kept by Rid + */ export class ResourceObservablesRegistry< TResourcesMap extends BaseMovexDefinitionResourcesMap > { From 74c77a5905eefe8039bbe858bf18e65fd586fb3e Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Wed, 3 Apr 2024 17:37:05 -0600 Subject: [PATCH 09/25] refactor(movex-core-util/observable): :recycle: take out uneeded code --- libs/movex-core-util/src/lib/Observable/Observable.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/libs/movex-core-util/src/lib/Observable/Observable.ts b/libs/movex-core-util/src/lib/Observable/Observable.ts index 1cb0b750..d930c335 100644 --- a/libs/movex-core-util/src/lib/Observable/Observable.ts +++ b/libs/movex-core-util/src/lib/Observable/Observable.ts @@ -3,9 +3,6 @@ import { isFunction } from '../misc'; export type NextStateGetter = T | ((prev: T) => T); -// export const getNextStateFrom = (prev: T, a: NextStateGetter) => -// isFunction(a) ? a(prev) : a; - export interface IObservable { get: () => T; onUpdate: (fn: (state: T) => void) => () => void; @@ -37,10 +34,8 @@ export class Observable implements IObservable { return this; } - // TODO: Add map for transformation - // TODO: TEST THIS AND ENSURE IT DOESNT CREATE SOME WEIRD LOOPING OR OTHER ISSUES /** - * + * Ability to transform the state * * @param mapFn * @returns From 9cca1aae9ce1a78b0e069d87afa6417be6327c88 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 4 Apr 2024 13:51:00 -0600 Subject: [PATCH 10/25] docs(movex): :memo: fix the README - take out hacktoberfest --- libs/movex/README.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/libs/movex/README.md b/libs/movex/README.md index 4657aa23..270388d0 100644 --- a/libs/movex/README.md +++ b/libs/movex/README.md @@ -39,22 +39,6 @@ In addition it comes pre-packed with: - keeps bad actors away by keeping the Data Reconciliation Logic out of the client reach. [See Authoritative Server](https://www.movex.dev/docs/features/server_authoritative) ---- -
-

🎉🚀 Movex is participating in Hacktoberfest! 🥳😍

-
- -### Here are some ways you can contribute too: -- Fix one of the [#hactoberfest issues](https://github.com/movesthatmatter/movex/issues?q=is%3Aissue+is%3Aopen+label%3Ahacktoberfest). -- Give feedback. -- **🙏 Give us a Github Star** -- Request a new feature -- File a bug report -- Add tests -- Use Movex to build your own game or application. [See examples below](#-examples)! - ---- - ## 🚀 Examples - **Chat App** - https://github.com/GabrielCTroia/movex-next-chat From cebb16a873a4a6530d00afaea0022bd8e198fcb5 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 4 Apr 2024 14:50:57 -0600 Subject: [PATCH 11/25] test(movex-master): :white_check_mark: fix tests after adding the subscribers --- libs/movex-master/package.json | 2 +- .../ClientMaster.chat.spec.ts | 8 +- .../ClientMaster.match.spec.ts | 21 +- .../ClientMaster.rps.spec.ts | 70 +++--- .../ClientMaster.spec.ts | 203 +++++++++++------- libs/movex-master/src/specs/Movex.spec.ts | 42 ++-- 6 files changed, 208 insertions(+), 138 deletions(-) diff --git a/libs/movex-master/package.json b/libs/movex-master/package.json index a3d3bb35..6096e5e5 100644 --- a/libs/movex-master/package.json +++ b/libs/movex-master/package.json @@ -1,6 +1,6 @@ { "name": "movex-master", - "version": "0.1.4", + "version": "0.1.5-0", "license": "MIT", "description": "Movex-master defines the master that wil be used on movex-server and movex-react-local-master", "author": { diff --git a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.chat.spec.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.chat.spec.ts index 38c2bfd6..46d345a9 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.chat.spec.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.chat.spec.ts @@ -51,7 +51,7 @@ test('Adds Single Participant', async () => { await tillNextTick(); - const actual = blueMovex.state; + const actual = blueMovex.state.checkedState; const expected = computeCheckedState({ ...initialChatState, @@ -109,7 +109,7 @@ test('Single Participant Writes a Message', async () => { await tillNextTick(); - const actual = blueMovex.state; + const actual = blueMovex.state.checkedState; const expected = computeCheckedState({ ...initialChatState, @@ -189,7 +189,7 @@ test('Adding Multiple Participants', async () => { await tillNextTick(); - const actual = blueMovex.state; + const actual = blueMovex.state.checkedState; const expected = computeCheckedState({ ...initialChatState, @@ -300,7 +300,7 @@ test('Multiple Participants Write Multiple Messages', async () => { await tillNextTick(); - const actual = blueMovex.state; + const actual = blueMovex.state.checkedState; const expected = computeCheckedState({ ...initialChatState, diff --git a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.match.spec.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.match.spec.ts index ad655789..4d53fb11 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.match.spec.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.match.spec.ts @@ -1,11 +1,10 @@ -import { computeCheckedState, globalLogsy } from 'movex-core-util'; +import { computeCheckedState, globalLogsy } from 'movex-core-util'; import { initialMatchState, tillNextTick, matchReducer, } from 'movex-specs-util'; import { movexClientMasterOrchestrator } from 'movex-master'; -// import matchReducer from 'libs/movex-specs-util/src/lib/resources/matchReducer'; const orchestrator = movexClientMasterOrchestrator(); @@ -55,13 +54,19 @@ test('works with public actions', async () => { await tillNextTick(); - const expected = computeCheckedState({ - ...initialMatchState, - players: { - [whiteClientId]: true, - [blackClientId]: true, + const expected = { + checkedState: computeCheckedState({ + ...initialMatchState, + players: { + [whiteClientId]: true, + [blackClientId]: true, + }, + }), + subscribers: { + 'white-client': null, + 'black-client': null, }, - }); + }; const actual = whiteMovex.state; diff --git a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.rps.spec.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.rps.spec.ts index f5e17c5e..60b885ea 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.rps.spec.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.rps.spec.ts @@ -1,9 +1,5 @@ -import { computeCheckedState, globalLogsy } from 'movex-core-util'; -import { - rpsReducer, - rpsInitialState, - tillNextTick, -} from 'movex-specs-util'; +import { computeCheckedState, globalLogsy } from 'movex-core-util'; +import { rpsReducer, rpsInitialState, tillNextTick } from 'movex-specs-util'; import { movexClientMasterOrchestrator } from 'movex-master'; import { install } from 'console-group'; install(); @@ -79,28 +75,34 @@ test('2 Clients. Both Submitting (White first) WITH Reconciliation and the recon await tillNextTick(); - const expectedAfterPrivateAction = computeCheckedState({ - ...rpsInitialState, - currentGame: { - ...rpsInitialState.currentGame, - players: { - playerA: { - id: clientAId, - label: 'playerA', - }, - playerB: { - id: clientBId, - label: 'playerB', + const expectedAfterPrivateAction = { + checkedState: computeCheckedState({ + ...rpsInitialState, + currentGame: { + ...rpsInitialState.currentGame, + players: { + playerA: { + id: clientAId, + label: 'playerA', + }, + playerB: { + id: clientBId, + label: 'playerB', + }, }, - }, - submissions: { - ...rpsInitialState.currentGame.submissions, - playerA: { - play: 'paper', + submissions: { + ...rpsInitialState.currentGame.submissions, + playerA: { + play: 'paper', + }, }, }, + }), + subscribers: { + 'client-a': null, + 'client-b': null, }, - }); + }; const actualAfterPrivateAction = aMovex.state; @@ -150,7 +152,7 @@ test('2 Clients. Both Submitting (White first) WITH Reconciliation and the recon }, }); - const actualAfterPrivateRevelatoryAction = bMovex.state; + const actualAfterPrivateRevelatoryAction = bMovex.state.checkedState; expect(actualAfterPrivateRevelatoryAction).toEqual( expectedAfterPrivateRevelatoryAction @@ -160,7 +162,9 @@ test('2 Clients. Both Submitting (White first) WITH Reconciliation and the recon expect(aMovex.state).toEqual(bMovex.state); const masterPublicState = await master.getPublicState(rid).resolveUnwrap(); - expect(masterPublicState).toEqual(actualAfterPrivateRevelatoryAction); + expect(masterPublicState).toEqual( + actualAfterPrivateRevelatoryAction + ); }); test('Same Kind Reconciliatory Actions Bug. See https://github.com/movesthatmatter/movex/issues/8', async () => { @@ -262,7 +266,7 @@ test('Same Kind Reconciliatory Actions Bug. See https://github.com/movesthatmatt }, }); - const actualAfterPrivateAction = aMovex.state; + const actualAfterPrivateAction = aMovex.state.checkedState; expect(actualAfterPrivateAction).toEqual(expectedAfterPrivateAction); @@ -310,7 +314,7 @@ test('Same Kind Reconciliatory Actions Bug. See https://github.com/movesthatmatt }, }); - const actualAfterPrivateRevelatoryAction = bMovex.state; + const actualAfterPrivateRevelatoryAction = bMovex.state.checkedState; expect(actualAfterPrivateRevelatoryAction).toEqual( expectedAfterPrivateRevelatoryAction @@ -424,7 +428,7 @@ test('Ensure further actinos can be dispatched after a Master-Resync', async () await tillNextTick(); - expect(bMovex.state).toEqual( + expect(bMovex.state.checkedState).toEqual( computeCheckedState({ ...rpsInitialState, currentGame: { @@ -462,7 +466,7 @@ test('Ensure further actinos can be dispatched after a Master-Resync', async () await tillNextTick(); - expect(aMovex.state).toEqual( + expect(aMovex.state.checkedState).toEqual( computeCheckedState({ ...rpsInitialState, currentGame: { @@ -487,7 +491,7 @@ test('Ensure further actinos can be dispatched after a Master-Resync', async () }) ); - expect(bMovex.state).toEqual( + expect(bMovex.state.checkedState).toEqual( computeCheckedState({ ...rpsInitialState, currentGame: { @@ -557,6 +561,6 @@ test('Ensure further actinos can be dispatched after a Master-Resync', async () }, }); - expect(bMovex.state).toEqual(expectedSharedState); - expect(aMovex.state).toEqual(expectedSharedState); + expect(bMovex.state.checkedState).toEqual(expectedSharedState); + expect(aMovex.state.checkedState).toEqual(expectedSharedState); }); diff --git a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts index 1b2a4cec..2d3ab8db 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts @@ -2,7 +2,7 @@ import { computeCheckedState, globalLogsy, toResourceIdentifierObj, -} from 'movex-core-util'; +} from 'movex-core-util'; import { gameReducer, gameReducerWithDerivedState, @@ -45,6 +45,7 @@ describe('Public Actions', () => { expect(created).toEqual({ rid: toResourceIdentifierObj(created.rid), // The id isn't too important here state: initialGameState, + subscribers: {}, }); const movex = gameClientResource.bind(created.rid); @@ -97,10 +98,16 @@ describe('Public Actions', () => { await tillNextTick(); - const expected = computeCheckedState({ - ...initialGameState, - count: 5, - }); + const expected = { + checkedState: computeCheckedState({ + ...initialGameState, + count: 5, + }), + subscribers: { + 'white-client': null, + 'black-client': null, + }, + }; expect(whiteMovex.state).toEqual(expected); @@ -146,20 +153,26 @@ describe('Private Actions', () => { // This is the sender private // White - const expectedSenderState = computeCheckedState({ - ...initialGameState, - submission: { - status: 'partial', - white: { - canDraw: false, - moves: ['w:E2-E4', 'w:D2-D4'], - }, - black: { - canDraw: true, - moves: [], + const expectedSenderState = { + checkedState: computeCheckedState({ + ...initialGameState, + submission: { + status: 'partial', + white: { + canDraw: false, + moves: ['w:E2-E4', 'w:D2-D4'], + }, + black: { + canDraw: true, + moves: [], + }, }, + }), + subscribers: { + 'white-client': null, + 'black-client': null, }, - }); + }; // And sender gets the new private state const actualSenderState = whiteMovex.state; @@ -182,7 +195,13 @@ describe('Private Actions', () => { // In this case is the same as the public b/c no private changes has been made // Black - const expectedPeerState = publicState; + const expectedPeerState = { + checkedState: publicState, + subscribers: { + 'white-client': null, + 'black-client': null, + }, + }; const actualPeerState = blackMovex.state; // Peer gets the new public state @@ -231,20 +250,26 @@ describe('Private Actions', () => { // This is the sender private // White - const expectedWhiteState = computeCheckedState({ - ...initialGameState, - submission: { - status: 'partial', - white: { - canDraw: false, - moves: ['w:E2-E4', 'w:D2-D4'], - }, - black: { - canDraw: true, - moves: [], + const expectedWhiteState = { + checkedState: computeCheckedState({ + ...initialGameState, + submission: { + status: 'partial', + white: { + canDraw: false, + moves: ['w:E2-E4', 'w:D2-D4'], + }, + black: { + canDraw: true, + moves: [], + }, }, + }), + subscribers: { + 'white-client': null, + 'black-client': null, }, - }); + }; // And sender gets the new private state const actualWhiteState = whiteMovex.state; @@ -271,39 +296,51 @@ describe('Private Actions', () => { await tillNextTick(); // White - const expectedPeerState = computeCheckedState({ - ...initialGameState, - submission: { - status: 'partial', - white: { - canDraw: false, - moves: ['w:E2-E4', 'w:D2-D4'], - }, - black: { - canDraw: false, - moves: [], + const expectedPeerState = { + checkedState: computeCheckedState({ + ...initialGameState, + submission: { + status: 'partial', + white: { + canDraw: false, + moves: ['w:E2-E4', 'w:D2-D4'], + }, + black: { + canDraw: false, + moves: [], + }, }, + }), + subscribers: { + 'white-client': null, + 'black-client': null, }, - }); + }; const actualPeerState = whiteMovex.state; expect(actualPeerState).toEqual(expectedPeerState); // Black - const expectedSenderState = computeCheckedState({ - ...initialGameState, - submission: { - status: 'partial', - white: { - canDraw: false, - moves: [], - }, - black: { - canDraw: false, - moves: ['b:E7-E6'], + const expectedSenderState = { + checkedState: computeCheckedState({ + ...initialGameState, + submission: { + status: 'partial', + white: { + canDraw: false, + moves: [], + }, + black: { + canDraw: false, + moves: ['b:E7-E6'], + }, }, + }), + subscribers: { + 'white-client': null, + 'black-client': null, }, - }); + }; // The Private Action gets set // And sender gets the new private state const actualSenderState = blackMovex.state; @@ -349,18 +386,24 @@ describe('Private Actions', () => { // This is the sender private // White - const expectedWhiteState = computeCheckedState({ - submission: { - white: { - canDraw: false, - moves: ['w:E2-E4', 'w:D2-D4'], - }, - black: { - canDraw: true, - moves: null, + const expectedWhiteState = { + checkedState: computeCheckedState({ + submission: { + white: { + canDraw: false, + moves: ['w:E2-E4', 'w:D2-D4'], + }, + black: { + canDraw: true, + moves: null, + }, }, + }), + subscribers: { + 'white-client': null, + 'black-client': null, }, - }); + }; // And sender gets the new private state const actualWhiteState = whiteMovex.state; @@ -386,28 +429,34 @@ describe('Private Actions', () => { await tillNextTick(); - const expected = computeCheckedState({ - submission: { - white: { - canDraw: false, - moves: ['w:E2-E4', 'w:D2-D4'], - }, - black: { - canDraw: false, - moves: ['b:E7-E6'], + const expectedState = { + checkedState: computeCheckedState({ + submission: { + white: { + canDraw: false, + moves: ['w:E2-E4', 'w:D2-D4'], + }, + black: { + canDraw: false, + moves: ['b:E7-E6'], + }, }, + }), + subscribers: { + 'white-client': null, + 'black-client': null, }, - }); + }; // They are bot equal now const actualPeerState = whiteMovex.state; - expect(actualPeerState).toEqual(expected); + expect(actualPeerState).toEqual(expectedState); const actualSenderState = blackMovex.state; - expect(actualSenderState).toEqual(expected); + expect(actualSenderState).toEqual(expectedState); const masterPublicState = await master.getPublicState(rid).resolveUnwrap(); - expect(masterPublicState).toEqual(expected); + expect(masterPublicState).toEqual(expectedState.checkedState); // expect(actualPeerState[0].submission.status).toBe('reconciled'); }); diff --git a/libs/movex-master/src/specs/Movex.spec.ts b/libs/movex-master/src/specs/Movex.spec.ts index 6d13b8e9..d8d3f652 100644 --- a/libs/movex-master/src/specs/Movex.spec.ts +++ b/libs/movex-master/src/specs/Movex.spec.ts @@ -2,7 +2,7 @@ import { computeCheckedState, globalLogsy, toResourceIdentifierObj, -} from 'movex-core-util'; +} from 'movex-core-util'; import * as consoleGroup from 'console-group'; import { counterReducer, @@ -47,6 +47,7 @@ describe('All', () => { expect(actual).toEqual({ rid: toResourceIdentifierObj(actual.rid), // The id isn't too important here state: { count: 2 }, + subscribers: {}, }); }, 200); @@ -64,11 +65,16 @@ describe('All', () => { const actual = counterResource.bind(rid); const actualDefaultState = actual.state; - expect(actualDefaultState).toEqual(computeCheckedState({ count: 0 })); + expect(actualDefaultState.checkedState).toEqual( + computeCheckedState({ count: 0 }) + ); await tillNextTick(); - const expected = computeCheckedState({ count: 2 }); + const expected = { + checkedState: computeCheckedState({ count: 2 }), + subscribers: { test: null }, + }; expect(actual.state).toEqual(expected); }); @@ -91,9 +97,10 @@ describe('All', () => { await tillNextTick(); const actual = r.get(); - const expected = computeCheckedState({ - count: 3, - }); + const expected = { + checkedState: computeCheckedState({ count: 3 }), + subscribers: { test: null }, + }; expect(actual).toEqual(expected); }); @@ -127,17 +134,22 @@ describe('All', () => { const actual = r.state; - const expected = computeCheckedState({ - ...initialGameState, - submission: { - ...initialGameState.submission, - status: 'partial', - white: { - canDraw: false, - moves: ['w:e2-e4', 'w:d2-d4'], + const expected = { + checkedState: computeCheckedState({ + ...initialGameState, + submission: { + ...initialGameState.submission, + status: 'partial', + white: { + canDraw: false, + moves: ['w:e2-e4', 'w:d2-d4'], + }, }, + }), + subscribers: { + 'test-user': null, }, - }); + }; expect(actual).toEqual(expected); }); From ae3a5d71ebb7b16d58ed3439dc28947c07063fc3 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 4 Apr 2024 14:51:41 -0600 Subject: [PATCH 12/25] chore(version): :hammer: bump to v0.1.5-0 --- libs/movex-core-util/package.json | 2 +- libs/movex-react-local-master/package.json | 2 +- libs/movex-react/package.json | 2 +- libs/movex-server/package.json | 2 +- libs/movex-service/package.json | 2 +- libs/movex-store/package.json | 2 +- libs/movex/package.json | 2 +- package.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libs/movex-core-util/package.json b/libs/movex-core-util/package.json index 35841cb8..505952b7 100644 --- a/libs/movex-core-util/package.json +++ b/libs/movex-core-util/package.json @@ -1,6 +1,6 @@ { "name": "movex-core-util", - "version": "0.1.4", + "version": "0.1.5-0", "description": "Movex Core Util is the library of utilities for Movex", "license": "MIT", "author": { diff --git a/libs/movex-react-local-master/package.json b/libs/movex-react-local-master/package.json index d7d5f5a2..fb864314 100644 --- a/libs/movex-react-local-master/package.json +++ b/libs/movex-react-local-master/package.json @@ -1,6 +1,6 @@ { "name": "movex-react-local-master", - "version": "0.1.4", + "version": "0.1.5-0", "license": "MIT", "description": "Run the movex-master locally with react", "author": { diff --git a/libs/movex-react/package.json b/libs/movex-react/package.json index ffc3a548..b3ea220e 100644 --- a/libs/movex-react/package.json +++ b/libs/movex-react/package.json @@ -1,6 +1,6 @@ { "name": "movex-react", - "version": "0.1.4", + "version": "0.1.5-0", "license": "MIT", "description": "Movex React is the library of React components for Movex", "author": { diff --git a/libs/movex-server/package.json b/libs/movex-server/package.json index 874174f4..e4aab595 100644 --- a/libs/movex-server/package.json +++ b/libs/movex-server/package.json @@ -1,6 +1,6 @@ { "name": "movex-server", - "version": "0.1.4", + "version": "0.1.5-0", "license": "MIT", "type": "commonjs", "description": "Movex Server is the backend runtime for Movex", diff --git a/libs/movex-service/package.json b/libs/movex-service/package.json index 4f34983a..cd0dd2f6 100644 --- a/libs/movex-service/package.json +++ b/libs/movex-service/package.json @@ -1,6 +1,6 @@ { "name": "movex-service", - "version": "0.1.4", + "version": "0.1.5-0", "license": "MIT", "type": "commonjs", "description": "Movex Service is the CLI for Movex", diff --git a/libs/movex-store/package.json b/libs/movex-store/package.json index d4db68c7..f926d2ec 100644 --- a/libs/movex-store/package.json +++ b/libs/movex-store/package.json @@ -1,6 +1,6 @@ { "name": "movex-store", - "version": "0.1.4", + "version": "0.1.5-0", "license": "MIT", "description": "Movex-store defines the store interface and comes with a MemoryStore", "author": { diff --git a/libs/movex/package.json b/libs/movex/package.json index 48afd9ff..3bb2beb1 100644 --- a/libs/movex/package.json +++ b/libs/movex/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.4", + "version": "0.1.5-0", "license": "MIT", "description": "Movex is a Multiplayer (Game) State Synchronization Library using Deterministic Action Propagation without the need to write Server Specific Code.", "author": { diff --git a/package.json b/package.json index c640f030..a583d1c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.4", + "version": "0.1.5-0", "license": "MIT", "author": { "name": "Gabriel C. Troia", From 016ecbf45d43f654e9e25a87b3101339c2bce7ef Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 4 Apr 2024 15:07:46 -0600 Subject: [PATCH 13/25] refactor(movex, movex-core-util, movex-master): :recycle: take out commented code and clean up --- libs/movex-core-util/src/lib/Logsy.ts | 4 +-- .../movex-master/src/lib/MovexMasterServer.ts | 14 +--------- .../src/lib/ConnectionToMasterResource.ts | 26 ++++++++----------- libs/movex/src/lib/MovexResource.ts | 12 --------- libs/movex/src/lib/MovexResourceObservable.ts | 20 -------------- 5 files changed, 14 insertions(+), 62 deletions(-) diff --git a/libs/movex-core-util/src/lib/Logsy.ts b/libs/movex-core-util/src/lib/Logsy.ts index a5db5226..8f3a8745 100644 --- a/libs/movex-core-util/src/lib/Logsy.ts +++ b/libs/movex-core-util/src/lib/Logsy.ts @@ -14,7 +14,7 @@ const globalDisabled = false; const globalLogsyConfigWrapper = { config: { disabled: false, - verbose: true, // TODO: Change this back + verbose: false, }, }; @@ -128,4 +128,4 @@ class Logsy { } export const globalLogsy = new Logsy('', false, globalLogsyConfigWrapper); -export const logsy = globalLogsy; +export const logsy = globalLogsy; \ No newline at end of file diff --git a/libs/movex-master/src/lib/MovexMasterServer.ts b/libs/movex-master/src/lib/MovexMasterServer.ts index 1cb1cbcd..fd2aa564 100644 --- a/libs/movex-master/src/lib/MovexMasterServer.ts +++ b/libs/movex-master/src/lib/MovexMasterServer.ts @@ -6,9 +6,7 @@ import { type AnyAction, type IOEvents, type ConnectionToClient, - AnyResourceIdentifier, - AnyStringResourceIdentifier, - MovexClientResourceShape, + type AnyStringResourceIdentifier, } from 'movex-core-util'; import { AsyncResult } from 'ts-async-results'; import { Err, Ok } from 'ts-results'; @@ -305,12 +303,6 @@ export class MovexMasterServer { onAddResourceSubscriber ); - // This doesn't come from the client, but it is emitted to the clent from the server - // clientConnection.emitter.on( - // 'removeResourceSubscriber', - // onRemoveResourceSubscriber - // ); - this.clientConnectionsByClientId = { ...this.clientConnectionsByClientId, [clientConnection.clientId]: clientConnection as ConnectionToClient< @@ -340,10 +332,6 @@ export class MovexMasterServer { 'addResourceSubscriber', onAddResourceSubscriber ); - // clientConnection.emitter.off( - // 'removeResourceSubscriber', - // onRemoveResourceSubscriber - // ); }; } diff --git a/libs/movex/src/lib/ConnectionToMasterResource.ts b/libs/movex/src/lib/ConnectionToMasterResource.ts index 6c831506..e0fd303e 100644 --- a/libs/movex/src/lib/ConnectionToMasterResource.ts +++ b/libs/movex/src/lib/ConnectionToMasterResource.ts @@ -91,7 +91,11 @@ export class ConnectionToMasterResource< }) => { console.log('ConnectionToMaster onRemoveResourceSubscriberHandler', p); if (toResourceIdentifierObj(p.rid).resourceType !== resourceType) { - console.log('ConnectionToMaster onRemoveResourceSubscriberHandler', 'nooooope', p); + console.log( + 'ConnectionToMaster onRemoveResourceSubscriberHandler', + 'nooooope', + p + ); return; } @@ -107,7 +111,11 @@ export class ConnectionToMasterResource< console.log('ConnectionToMaster onAddResourceSubscriberHandler', p); if (toResourceIdentifierObj(p.rid).resourceType !== resourceType) { - console.log('ConnectionToMaster onAddResourceSubscriberHandler', p, 'noooope'); + console.log( + 'ConnectionToMaster onAddResourceSubscriberHandler', + p, + 'noooope' + ); return; } @@ -174,19 +182,7 @@ export class ConnectionToMasterResource< resourceType, resourceId, }) - .then((res) => - res.ok - ? // ? new Ok({ - // // This sanitizes the data only allowing specific fields to get to the client - // // TODO: This actually should come sanitized from the server, since this is on the client - // // Probably best to where the ack is called - // // rid: res.val.rid, - // // state: res.val.state, - // ...res.val, - // }) - new Ok(res.val) // TODO: Ensure the data is sanitized from the server - : new Err(res.val) - ) + .then((res) => (res.ok ? new Ok(res.val) : new Err(res.val))) ); } diff --git a/libs/movex/src/lib/MovexResource.ts b/libs/movex/src/lib/MovexResource.ts index decd029e..ab3bbad1 100644 --- a/libs/movex/src/lib/MovexResource.ts +++ b/libs/movex/src/lib/MovexResource.ts @@ -70,18 +70,6 @@ export class MovexResource< get(rid: ResourceIdentifier) { return this.connectionToMasterResource.getResource(rid); - // .map(({ rid, subscribers, }) => ({ - // ...resource, - // // This is rempaed in this way in order to return the same payload as "create" - // // rid, - // // state, - // })); - - // TODO: Once a client can bind multiple times this could also sync with the obsservable - // .map((s) => { - // // TODO: This shoou - // // clientResource.sync(s); - // }); } /** diff --git a/libs/movex/src/lib/MovexResourceObservable.ts b/libs/movex/src/lib/MovexResourceObservable.ts index 7bf394a8..8256d845 100644 --- a/libs/movex/src/lib/MovexResourceObservable.ts +++ b/libs/movex/src/lib/MovexResourceObservable.ts @@ -45,18 +45,12 @@ export class MovexResourceObservable< */ private isSynchedPromiseDelegate = new PromiseDelegate(); - // private $checkedState: Observable>; - private $item: Observable>; - // public $subscribers: Observable<>; - private pubsy = new Pubsy<{ onDispatched: DispatchedEvent, TAction>; }>(); - // client: MovexResourceClient; - private dispatcher: ( actionOrActionTuple: ActionOrActionTupleFromAction ) => void; @@ -88,15 +82,6 @@ export class MovexResourceObservable< // TODO: Add Use case for how to deal with creation. When the get doesn't return anything?! // something like: if it returns record doesnt exist, use create instead of get? or io.getOrCreate()? - // const { dispatch, unsubscribe: unsubscribeFromDispatch } = createDispatcher< - // TState, - // TAction - // >(this.$checkedState, this.reducer, { - // onDispatched: (p) => { - // this.pubsy.publish('onDispatched', p); - // }, - // }); - const $checkedState = this.$item.map((s) => s.checkedState); // Propagate the updates up to the root $item, but ensure it doesn't get into a loop @@ -216,8 +201,6 @@ export class MovexResourceObservable< checkedState: CheckedState; }) => void ) { - // return this.$checkedState.onUpdate(([state]) => fn(state)); - // return this.$checkedState.onUpdate(fn); return this.$item.onUpdate(fn); } @@ -237,10 +220,7 @@ export class MovexResourceObservable< } getUncheckedState() { - // return this.$checkedState.get()[0]; - // this.get().checkedState[0]; return this.getCheckedState()[0]; - // return this.$item.get()[0]; } // This is the actual checked state. TODO: Not sure about the names yet From bfbe7ba239c2253ec3e9b7dc36b7811b7f6d9064 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Sat, 6 Apr 2024 15:02:35 -0600 Subject: [PATCH 14/25] Remove unneded code --- libs/movex/src/lib/MovexBoundResource.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/movex/src/lib/MovexBoundResource.ts b/libs/movex/src/lib/MovexBoundResource.ts index 90dea905..2597abec 100644 --- a/libs/movex/src/lib/MovexBoundResource.ts +++ b/libs/movex/src/lib/MovexBoundResource.ts @@ -38,9 +38,10 @@ export class MovexBoundResource< return this.observable.get().subscribers; } - get item() { - return this.observable.get(); - } + // This is not needed i believe, but I need to check, as it goes all the way to the client with private checked state types + // get item() { + // return this.observable.get(); + // } // This to be called when not used anymore in order to clean the update subscriptions destroy = () => { From ded030c50481339d28e438a9fa853cc868551f94 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Sat, 6 Apr 2024 17:10:09 -0600 Subject: [PATCH 15/25] fix(movex-react-local-master): :bug: fix the bndResource --- .../src/lib/MovexLocalMasterProvider.tsx | 2 +- .../src/lib/MovexLocalProvider.tsx | 31 ++++++++++++++++--- libs/movex-react/src/index.ts | 1 + 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/libs/movex-react-local-master/src/lib/MovexLocalMasterProvider.tsx b/libs/movex-react-local-master/src/lib/MovexLocalMasterProvider.tsx index db648fa7..3eb7a533 100644 --- a/libs/movex-react-local-master/src/lib/MovexLocalMasterProvider.tsx +++ b/libs/movex-react-local-master/src/lib/MovexLocalMasterProvider.tsx @@ -8,7 +8,7 @@ import { type GetReducerState, type BaseMovexDefinitionResourcesMap, type MovexDefinition, -} from 'movex-core-util'; +} from 'movex-core-util'; import { MemoryMovexStore, type MovexStoreItem } from 'movex-store'; import { MovexMasterServer, initMovexMaster } from 'movex-master'; import { MovexLocalContext, MovexLocalContextProps } from './MovexLocalContext'; diff --git a/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx b/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx index 720bc0f4..7cefed12 100644 --- a/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx +++ b/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx @@ -2,9 +2,11 @@ import React from 'react'; import { invoke, ConnectionToClient, - type MovexClient, + type MovexClient as MovexClientUser, type MovexDefinition, type BaseMovexDefinitionResourcesMap, + StringKeys, + ResourceIdentifier, } from 'movex-core-util'; import { MovexMasterServer, @@ -12,13 +14,18 @@ import { orchestrateDefinedMovex, getUuid, // This can actually be mocked here as it's just client only! } from 'movex-master'; -import { MovexReactContext, MovexReactContextProps } from 'movex-react'; +import { MovexClient } from 'movex'; +import { + MovexReactContext, + MovexReactContextProps, + MovexResourceObservablesRegistry, +} from 'movex-react'; import { MovexLocalContextConsumerProvider } from './MovexLocalContextConsumer'; type Props = React.PropsWithChildren<{ movexDefinition: MovexDefinition; - clientId?: MovexClient['id']; + clientId?: MovexClientUser['id']; onConnected?: ( state: Extract, { connected: true }> ) => void; @@ -77,12 +84,28 @@ export class MovexLocalProvider< emitterOnMaster ); + // This resets each time movex re-initiates + const resourceRegistry = new MovexResourceObservablesRegistry( + mockedMovex.movex + ); + const nextState = { connected: true, movex: mockedMovex.movex, clientId: mockedMovex.movex.getClientId(), movexDefinition: this.props.movexDefinition, - bindResource: () => () => {}, // Added this on Apr 01 2024, w/o testing - not sure it doesn't create more issues but needed it for tsc + bindResource: >( + rid: ResourceIdentifier, + onStateUpdate: (p: MovexClient.MovexBoundResource) => void + ) => { + const $resource = resourceRegistry.register(rid); + + onStateUpdate(new MovexClient.MovexBoundResource($resource)); + + return $resource.onUpdate(() => { + onStateUpdate(new MovexClient.MovexBoundResource($resource)); + }); + }, } as const; this.setState({ diff --git a/libs/movex-react/src/index.ts b/libs/movex-react/src/index.ts index d4507102..dca97359 100644 --- a/libs/movex-react/src/index.ts +++ b/libs/movex-react/src/index.ts @@ -8,5 +8,6 @@ export { } from './lib/MovexContext'; export { MovexBoundResourceComponent as MovexBoundResource } from './lib/MovexBoundResourceComponent'; +export { ResourceObservablesRegistry as MovexResourceObservablesRegistry } from './lib/ResourceObservableRegistry'; export * from './lib/MovexConnection'; export * from './lib/hooks'; From c1a980944e0ddecfbc64d70199f42cb002f17b54 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Sat, 6 Apr 2024 20:03:21 -0600 Subject: [PATCH 16/25] feat(movex-demo): :sparkles: add an RPS game using the movex-react-local-master --- apps/movex-demo/pages/local/rps-v2/Game.tsx | 109 ++++++++ apps/movex-demo/pages/local/rps-v2/GameUI.tsx | 263 ++++++++++++++++++ apps/movex-demo/pages/local/rps-v2/index.tsx | 22 ++ .../pages/local/rps-v2/movex.config.ts | 10 + apps/movex-demo/pages/local/rps-v2/movex.ts | 241 ++++++++++++++++ 5 files changed, 645 insertions(+) create mode 100644 apps/movex-demo/pages/local/rps-v2/Game.tsx create mode 100644 apps/movex-demo/pages/local/rps-v2/GameUI.tsx create mode 100644 apps/movex-demo/pages/local/rps-v2/index.tsx create mode 100644 apps/movex-demo/pages/local/rps-v2/movex.config.ts create mode 100644 apps/movex-demo/pages/local/rps-v2/movex.ts diff --git a/apps/movex-demo/pages/local/rps-v2/Game.tsx b/apps/movex-demo/pages/local/rps-v2/Game.tsx new file mode 100644 index 00000000..2f059ec6 --- /dev/null +++ b/apps/movex-demo/pages/local/rps-v2/Game.tsx @@ -0,0 +1,109 @@ +import * as React from "react"; +import { MovexBoundResource } from "movex-react"; +import movexConfig from "./movex.config"; +import { initialState } from "./movex"; +import { useState } from "react"; +import { ResourceIdentifier, toResourceIdentifierStr } from "movex-core-util"; +import { GameUI } from "./GameUI"; +// import { MovexStoreItem } from "movex"; +import { MovexLocalInstance } from 'movex-react-local-master'; +import { MovexStoreItem } from 'movex-store'; + +type Props = { + masterStore?: MovexStoreItem; +}; + +export function Game(props: Props) { + const [rpsRid, setRpsRid] = useState>(); + const [masterStateUpdated, setMasterStateUpdated] = useState(false); + + return ( +
+ { + const reg = movex.register("rps"); + + reg.create(initialState).map(({ rid }) => { + setRpsRid(rid); + + setTimeout(() => { + // HACK. Fake the Maser State Update in order to fix a current issue with working with + // master state values, instead of local ones. + // See https://github.com/movesthatmatter/movex/issues/9 for more info. + // This feature witll be added in the close future + setMasterStateUpdated(true); + }, 10); + }); + }} + > + {rpsRid && ( + <> + yesss + ( +
+ +
+ )} + /> + + )} +
+
+
+ + 🆚 + +
+ {props.masterStore && ( +
+
+

+ Master Movex State +

+
+                
+                  {JSON.stringify(
+                    {
+                      submissions: props.masterStore.state[0].submissions,
+                      winner: props.masterStore.state[0].winner
+                    },
+                    null,
+                    1
+                  )}
+                
+              
+
+
+ )} +
+ {rpsRid && masterStateUpdated && ( + + here 2 + ( +
+ yess works + +
+ )} + /> +
+ )} +
+ ); +} diff --git a/apps/movex-demo/pages/local/rps-v2/GameUI.tsx b/apps/movex-demo/pages/local/rps-v2/GameUI.tsx new file mode 100644 index 00000000..98f92ab7 --- /dev/null +++ b/apps/movex-demo/pages/local/rps-v2/GameUI.tsx @@ -0,0 +1,263 @@ +import React from "react"; +import { MovexBoundResourceFromConfig } from "movex-react"; +import { useCallback, useEffect, useMemo } from "react"; +import { logsy } from "movex-core-util"; +import movexConfig from "./movex.config"; +import { + PlayerLabel, + toOppositeLabel, + RPS, + selectAvailableLabels +} from "./movex"; + +type Props = { + boundResource: MovexBoundResourceFromConfig< + typeof movexConfig["resources"], + "rps" + >; + userId: string; + buttonClassName?: string; + containerClassName?: string; + backgroundColor?: string; +}; + +export const GameUI: React.FC = ({ + boundResource, + userId, + ...props +}) => { + const { state, dispatch, dispatchPrivate } = boundResource; + const { players } = state; + + const myPlayerLabel = useMemo((): PlayerLabel | undefined => { + if (!(players.playerA && players.playerB)) { + return undefined; + } + + if (players.playerA.id === userId) { + return "playerA"; + } + + if (players.playerB.id === userId) { + return "playerB"; + } + + return undefined; + }, [players, userId]); + + const oppnentPlayerLabel = useMemo(() => { + return myPlayerLabel ? toOppositeLabel(myPlayerLabel) : undefined; + }, [myPlayerLabel]); + + // Add Player + useEffect(() => { + // TODO: here there is a major issue, as selectAvailableLables works with local state + // but it needs to check on the actual (master) state. How to solve this? + // add an api to be able to read master state seperately? + + // Or in this case change the strategy altogether, and work with master generated values, in which case + // the local optimistic state udate gets turned off by default, so that means it will wait for the real state to update. + // Kinda like a dispatchAndWait + const availableLabels = selectAvailableLabels(state); + + if ( + state.players.playerA?.id === userId || + state.players.playerB?.id === userId + ) { + return; + } + + if (availableLabels.length === 0) { + logsy.warn("Player Slots taken"); + + return; + } + + dispatch({ + type: "addPlayer", + payload: { + id: userId, + playerLabel: availableLabels[0], + atTimestamp: new Date().getTime() + } + }); + }, [userId, state, dispatch]); + + const submit = useCallback( + (play: RPS) => { + if (!myPlayerLabel) { + console.warn("Not A Player"); + return; + } + + dispatchPrivate( + { + type: "submit", + payload: { + playerLabel: myPlayerLabel, + rps: play + }, + isPrivate: true + }, + { + type: "setReadySubmission", + payload: { + playerLabel: myPlayerLabel + } + } + ); + }, + [dispatchPrivate, myPlayerLabel] + ); + + const winner = useMemo(() => { + if (!state.winner) { + return undefined; + } + + if (state.winner === "1/2") { + return "1/2"; + } + + const { + submissions: { playerA } + } = state; + + if (playerA.play === state.winner) { + return state.players.playerA.label; + } + + return state.players.playerB.label; + }, [state.winner]); + + return ( +
+
+
{userId}
+ {state.winner ? ( + <> +

+ {winner === "1/2" + ? "Draw 😫" + : winner === myPlayerLabel + ? "You Won 🎉" + : "You Lost 😢"} +

+ + + ) : ( + <> +
+
+ +
+
+ +
+
+ +
+
+
+ {oppnentPlayerLabel && + state.submissions[oppnentPlayerLabel]?.play && ( +
+ Opponent Submitted{" "} + + ⌛️⏰ + +
+ )} +
+ + )} +
+
+
+

{userId} Movex State

+
+            
+              {JSON.stringify(
+                {
+                  submissions: state.submissions,
+                  winner: state.winner
+                },
+                null,
+                1
+              )}
+            
+          
+
+
+
+ ); +}; diff --git a/apps/movex-demo/pages/local/rps-v2/index.tsx b/apps/movex-demo/pages/local/rps-v2/index.tsx new file mode 100644 index 00000000..0e7cfe30 --- /dev/null +++ b/apps/movex-demo/pages/local/rps-v2/index.tsx @@ -0,0 +1,22 @@ +import { useState } from 'react'; +import movexConfig from './movex.config'; +import { Game } from './Game'; +import { MovexLocalMasterProvider } from 'movex-react-local-master'; +import { MovexStoreItem } from 'movex-store'; + +export default function App() { + const [masterStore, setMasterStore] = useState>(); + + return ( + /** + * This is using the Local Provider in order to simulate + * multiple players in the same browser instance + */ + + + + ); +} diff --git a/apps/movex-demo/pages/local/rps-v2/movex.config.ts b/apps/movex-demo/pages/local/rps-v2/movex.config.ts new file mode 100644 index 00000000..d558b418 --- /dev/null +++ b/apps/movex-demo/pages/local/rps-v2/movex.config.ts @@ -0,0 +1,10 @@ +import { reducer as rpsReducer } from "./movex"; + +// The idea of this is so both the server and the client can read it +// Obviosuly being a node/js env helps a lot :) + +export default { + resources: { + rps: rpsReducer + } +}; diff --git a/apps/movex-demo/pages/local/rps-v2/movex.ts b/apps/movex-demo/pages/local/rps-v2/movex.ts new file mode 100644 index 00000000..2dc0c8c3 --- /dev/null +++ b/apps/movex-demo/pages/local/rps-v2/movex.ts @@ -0,0 +1,241 @@ +// import { Action } from "movex"; +// import { MovexClient } from "movex-core-util"; + +import { Action, MovexClient } from 'movex-core-util'; + +export type PlayerId = MovexClient["id"]; +export type Color = string; + +export type RPS = "rock" | "paper" | "scissors"; + +export const playerLabels = ["playerA", "playerB"] as const; +export type PlayerLabel = "playerA" | "playerB"; + +export type State = Game; + +export const initialState: State = { + players: { + playerA: null, + playerB: null + }, + winner: null, + submissions: { + playerA: null, + playerB: null + } +}; + +export const selectAvailableLabels = (state: State): PlayerLabel[] => { + return playerLabels.filter((l) => state.players[l] === null); +}; + +export function toOppositeLabel( + c: L +): L extends "playerA" ? "playerB" : "playerA"; +export function toOppositeLabel(l: L) { + return l === "playerA" ? "playerB" : "playerA"; +} + +export const getRPSWinner = ([a, b]: [ + RPS | "$SECRET" | null | undefined, + RPS | "$SECRET" | null | undefined +]): RPS | "1/2" | null => { + if (!a || a === "$SECRET" || !b || b === "$SECRET") { + return null; + } + + if (a === b) { + return "1/2"; + } + + if (a === "paper") { + if (b === "rock") { + return a; + } + + return b; + } else if (a === "rock") { + if (b === "scissors") { + return a; + } + + return b; + } else if (b === "paper") { + return a; + } + + return b; +}; + +type Player = { + id: PlayerId; + label: PlayerLabel; +}; + +type RevealedSubmission = { + play: RPS; +}; + +type SecretSubmission = { + play: "$SECRET"; +}; + +export type Submission = RevealedSubmission | SecretSubmission; + +export type GameInProgress = { + players: { + playerA: Player | null; + playerB: Player | null; + }; + submissions: { + playerA: Submission | null; + playerB: Submission | null; + }; + winner: null; +}; + +export type GameCompleted = { + players: { + playerA: Player; + playerB: Player; + }; + submissions: { + playerA: Submission; + playerB: Submission; + }; + winner: RPS | "1/2"; +}; + +export type Game = GameInProgress | GameCompleted; + +export type Actions = + | Action< + "addPlayer", + { + id: PlayerId; + playerLabel: PlayerLabel; + atTimestamp: number; + } + > + | Action<"playAgain"> + | Action< + "submit", + { + playerLabel: PlayerLabel; + rps: RPS; + } + > + | Action< + "setReadySubmission", + { + playerLabel: PlayerLabel; + } + >; + +export const reducer = (state = initialState, action: Actions): State => { + if (action.type === "playAgain") { + return { + ...state, + players: state.players, + winner: null, + submissions: { + playerA: null, + playerB: null + } + }; + } + + if (action.type === "addPlayer") { + // If already taken return + if (state.players[action.payload.playerLabel] !== null) { + return state; + } + + return { + ...state, + + players: { + // TODO: This is just stupid needing a recast b/c it cannot determine if the game is completed or inProgress, but at this point I care not + ...(state.players as any), + [action.payload.playerLabel]: { + label: action.payload.playerLabel, + id: action.payload.id + } + } + }; + } + + if (action.type === "submit") { + // If game is completed + if (state.winner !== null) { + return state; + } + + const oppositeLabel = toOppositeLabel(action.payload.playerLabel); + + // 1st submission + if (state.submissions[oppositeLabel] === null) { + return { + ...state, + submissions: { + ...(action.payload.playerLabel === "playerA" + ? { + playerA: { + play: action.payload.rps + }, + playerB: null + } + : { + playerB: { + play: action.payload.rps + }, + playerA: null + }) + } + }; + } else { + // final submission: game gets completed + const nextSubmission = { + ...state.submissions, + [action.payload.playerLabel]: { + play: action.payload.rps + } + }; + + const nextWinner = getRPSWinner([ + nextSubmission.playerA?.play, + nextSubmission.playerB?.play + ]); + + return { + ...state, + submissions: nextSubmission, + winner: nextWinner as any + }; + } + } else if (action.type === "setReadySubmission") { + // If game is completed + if (state.winner !== null) { + return state; + } + + const nextSubmission = { + ...state.submissions, + [action.payload.playerLabel]: { + play: "$SECRET" + } + }; + + return { + ...state, + submissions: nextSubmission + }; + } + + return state; +}; + +reducer.$canReconcileState = (state: State) => + state.submissions.playerA !== null && state.submissions.playerB !== null; + +export default reducer; From f8a80dcf2dee534d4a8fecd912e2054d3b96b72f Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Sat, 6 Apr 2024 22:24:03 -0600 Subject: [PATCH 17/25] refactor(all): :recycle: make Logsy an Event Emitter and expose in move-react MovexProvider --- libs/movex-core-util/src/lib/Logsy.ts | 135 +++++------ .../movex-core-util/src/lib/ScketIOEmitter.ts | 45 ++-- .../src/lib/MockConnectionEmitter.ts | 17 +- .../movex-master/src/lib/MovexMasterServer.ts | 83 +++---- libs/movex-master/src/lib/orchestrator.ts | 12 +- .../src/lib/MovexLocalMasterProvider.tsx | 36 +-- .../src/lib/MovexBoundResourceComponent.tsx | 3 +- libs/movex-server/src/lib/movex-server.ts | 16 +- .../lib/MemoryMovexStore/MemoryMovexStore.ts | 6 +- .../src/lib/ConnectionToMasterResource.ts | 13 - libs/movex/src/lib/MovexResource.ts | 229 ++++-------------- .../src/lib/MovexResourceObservable.spec.ts | 8 - libs/movex/src/lib/MovexResourceObservable.ts | 6 +- 13 files changed, 226 insertions(+), 383 deletions(-) diff --git a/libs/movex-core-util/src/lib/Logsy.ts b/libs/movex-core-util/src/lib/Logsy.ts index 8f3a8745..f68925c1 100644 --- a/libs/movex-core-util/src/lib/Logsy.ts +++ b/libs/movex-core-util/src/lib/Logsy.ts @@ -1,5 +1,7 @@ // const movexLogsy = Logger.get('Movex'); +import { Pubsy } from 'ts-pubsy'; + type LogsyMethods = | 'log' | 'info' @@ -9,98 +11,70 @@ type LogsyMethods = | 'groupEnd' | 'debug'; -const globalDisabled = false; - -const globalLogsyConfigWrapper = { - config: { - disabled: false, - verbose: false, - }, +export type LoggingEvent = { + method: LogsyMethods; + prefix?: string; + message?: unknown; + payload?: LogsyPayload; }; -// Depreacte from here! maybe make own library +export type LogsyPayload = Record; class Logsy { - constructor( - public prefix: string = '', - disabled = false, - private globalConfig?: typeof globalLogsyConfigWrapper - ) { - // This is needed - if (disabled) { - this.disable(); - } - } - - private handler = ( - method: LogsyMethods, - message?: unknown, - ...optionalParams: unknown[] - ) => { - if (!this.ON || globalLogsyConfigWrapper.config.disabled) { - return; - } + private pubsy = new Pubsy<{ + onLog: LoggingEvent; + }>(); - // If verbose is false, we don't want to log, info or debug - if ( - (method === 'log' || method === 'info' || method === 'debug') && - !globalLogsyConfigWrapper.config.verbose - ) { - return; - } + constructor(public prefix: string = '') {} - const prefix = this.hasGroupOpen() ? '' : this.prefix; + onLog = (fn: (event: LoggingEvent) => void) => + this.pubsy.subscribe('onLog', fn); - if (typeof message === 'string') { - console[method](prefix + ' ' + message, ...optionalParams); - } else { - console[method](prefix, message, ...optionalParams); - } + // To be overriden + // public onLog: (event: LoggingEvent) => void = (event: LoggingEvent) => { + // this.pubsy.publish('onLog', { + // ...event, + // }); + // }; + + private handler = (event: LoggingEvent) => { + const prefix = this.hasGroupOpen() ? '' : this.prefix; + this.pubsy.publish('onLog', { + ...event, + prefix, + }); + // this.onLog({ ...event, prefix }); }; - public ON: boolean = globalDisabled || true; + // public ON: boolean = globalDisabled || true; private activeGroups = 0; - enable() { - this.ON = true; - - if (this.globalConfig) { - this.globalConfig.config.disabled = false; - } - } - - disable() { - this.ON = false; - - if (this.globalConfig) { - this.globalConfig.config.disabled = true; - } - } - - log = (message?: any, ...optionalParams: any[]) => { - this.handler('log', message, ...optionalParams); + log = (message?: string, payload?: LogsyPayload) => { + this.handler({ method: 'log', message, payload }); }; - info = (message?: any, ...optionalParams: any[]) => { - this.handler('info', message, ...optionalParams); + info = (message?: string, payload?: LogsyPayload) => { + this.handler({ method: 'info', message, payload }); }; - warn = (message?: any, ...optionalParams: any[]) => { - this.handler('warn', message, ...optionalParams); + warn = (message?: string, payload?: LogsyPayload) => { + this.handler({ method: 'warn', message, payload }); }; - error = (message?: any, ...optionalParams: any[]) => { - this.handler('error', message, ...optionalParams); + error = (message?: string, payload?: LogsyPayload) => { + this.handler({ method: 'error', message, payload }); }; - group = (message?: any, ...optionalParams: any[]) => { - this.handler('group', message, ...optionalParams); + group = (message?: string, payload?: LogsyPayload) => { + this.handler({ method: 'group', message, payload }); this.openGroup(); }; - groupEnd = () => { - // console.groupEnd() - this.handler('groupEnd'); + groupEnd = (message?: string, payload?: LogsyPayload) => { + if (message) { + this.handler({ method: 'log', message, payload }); + } + this.handler({ method: 'groupEnd' }); this.closeGroup(); }; @@ -118,14 +92,25 @@ class Logsy { private hasGroupOpen = () => this.activeGroups > 0; - debug = (message?: any, ...optionalParams: any[]) => { - this.handler('debug', message, ...optionalParams); + debug = (message?: any, payload?: LogsyPayload) => { + this.handler({ method: 'debug', message, payload }); }; withNamespace = (s: string) => { - return new Logsy(this.prefix + s); + const next = new Logsy(this.prefix + s); + + // next.onLog((...args) => this.onLog(...args)); + // // this.onLog(); + // this.onLog((event) => { + // this.pubsy.publish('onLog', event); + // }); + // next.onLog = (event) => this.pubsy.publish('onLog', event); + next.onLog((event) => { + this.pubsy.publish('onLog', event); + }); + + return next; }; } -export const globalLogsy = new Logsy('', false, globalLogsyConfigWrapper); -export const logsy = globalLogsy; \ No newline at end of file +export const globalLogsy = new Logsy(); diff --git a/libs/movex-core-util/src/lib/ScketIOEmitter.ts b/libs/movex-core-util/src/lib/ScketIOEmitter.ts index 7028621c..4f93cfc0 100644 --- a/libs/movex-core-util/src/lib/ScketIOEmitter.ts +++ b/libs/movex-core-util/src/lib/ScketIOEmitter.ts @@ -1,5 +1,5 @@ import { Err, Ok } from 'ts-results'; -import { logsy } from './Logsy'; +import { globalLogsy } from './Logsy'; import type { Socket as ServerSocket } from 'socket.io'; import type { Socket as ClientSocket } from 'socket.io-client'; import type { EventMap } from 'typed-emitter'; @@ -8,18 +8,17 @@ import type { UnsubscribeFn, WsResponseResultPayload } from './core-types'; export type SocketIO = ServerSocket | ClientSocket; +const logsy = globalLogsy.withNamespace('[SocketIOEmitter]'); + export class SocketIOEmitter implements EventEmitter { - private logger: typeof console; constructor( private socket: SocketIO, private config: { - logger?: typeof console; waitForResponseMs?: number; } = {} ) { - this.logger = config.logger || console; this.config.waitForResponseMs = this.config.waitForResponseMs || 15 * 1000; } @@ -67,7 +66,7 @@ export class SocketIOEmitter acknowledgeCb?: (response: ReturnType) => void ): boolean { const reqId = `${event as string}(${String(Math.random()).slice(-3)})`; - logsy.debug('[ServerSocketEmitter]', reqId, 'Emit:', event, request); + logsy.debug('Emit', { reqId, event, request }); this.socket.emit( event as string, @@ -76,10 +75,10 @@ export class SocketIOEmitter withTimeout( (res: WsResponseResultPayload) => { if (res.ok) { - logsy.debug('[ServerSocketEmitter]', reqId, 'Response Ok:', res); + logsy.debug('Emit Response Ok', { reqId, event, res }); acknowledgeCb(new Ok(res.val) as ReturnType); } else { - logsy.warn('[ServerSocketEmitter]', reqId, 'Response Err:', res); + logsy.debug('Emit Response Err', { reqId, event, res }); acknowledgeCb(new Err(res.val) as ReturnType); } }, @@ -106,13 +105,11 @@ export class SocketIOEmitter ): Promise> { return new Promise((resolve, reject) => { const reqId = `${event as string}(${String(Math.random()).slice(-3)})`; - logsy.debug( - '[ServerSocketEmitter]', + logsy.debug('EmitAndAcknowledge', { reqId, - 'EmitAndAcknowledge:', event, - request - ); + request, + }); this.socket.emit( event as string, @@ -120,20 +117,30 @@ export class SocketIOEmitter withTimeout( (res: WsResponseResultPayload) => { if (res.ok) { - logsy.debug('[ServerSocketEmitter]', reqId, 'Response Ok:', res); + logsy.debug('EmitAndAcknowledge Response Ok', { + reqId, + res, + request, + event, + }); resolve(new Ok(res.val)); } else { - logsy.warn('[ServerSocketEmitter]', reqId, 'Response Err:', res); + logsy.debug('EmitAndAcknowledge Response Err', { + reqId, + res, + request, + event, + }); reject(new Err(res.val)); } }, () => { - logsy.warn( - '[ServerSocketEmitter]', + logsy.error('EmitAndAcknowledge Request Timeout', { + reqId, + request, event, - 'Request Timeout:', - request - ); + }); + // TODO This error could be typed better using a result error reject(new Err('RequestTimeout')); }, diff --git a/libs/movex-master/src/lib/MockConnectionEmitter.ts b/libs/movex-master/src/lib/MockConnectionEmitter.ts index ee8bde91..c2bb1fa9 100644 --- a/libs/movex-master/src/lib/MockConnectionEmitter.ts +++ b/libs/movex-master/src/lib/MockConnectionEmitter.ts @@ -1,13 +1,16 @@ -import type { +import { AnyAction, EventEmitter, UnsubscribeFn, IOEvents, -} from 'movex-core-util'; + globalLogsy, +} from 'movex-core-util'; import { Pubsy } from 'ts-pubsy'; import { getRandomInt, getUuid } from './util'; +const logsy = globalLogsy.withNamespace('MockConnectionEmitter'); + export class MockConnectionEmitter< TState extends any = any, TAction extends AnyAction = AnyAction, @@ -163,14 +166,12 @@ export class MockConnectionEmitter< // TODO: Need a way for this to call the unsubscriber this.ackPubsy.subscribe(ackId, (ackMsg) => { - console.log( - '[MockConnectionEmitter] emit', + logsy.log('Emit', { event, request, - 'response:', - ackMsg - ); - console.trace('[MockConnectionEmitter] Trace', event, request); + response: ackMsg, + }); + acknowledgeCb( ackMsg as ReturnType[E]> ); diff --git a/libs/movex-master/src/lib/MovexMasterServer.ts b/libs/movex-master/src/lib/MovexMasterServer.ts index fd2aa564..9d100412 100644 --- a/libs/movex-master/src/lib/MovexMasterServer.ts +++ b/libs/movex-master/src/lib/MovexMasterServer.ts @@ -99,7 +99,10 @@ export class MovexMasterServer { // Forwardable objectKeys(peerActions.byClientId).forEach((peerId) => { if (!peerActions.byClientId[peerId]) { - logsy.error('Inexistant Peer Connection for peerId:', peerId); + logsy.error('Inexistant Peer Connection for peerId:', { + peerId, + peerActionsByClientId: peerActions.byClientId, + }); return; } @@ -261,11 +264,10 @@ export class MovexMasterServer { const peerConnection = this.clientConnectionsByClientId[peerId]; if (!peerConnection) { - logsy.error( - 'onAddResourceSubscriber: Peer Connection not found for', + logsy.error('OnAddResourceSubscriber PeerConnectionNotFound', { peerId, - this.clientConnectionsByClientId - ); + clientId: this.clientConnectionsByClientId, + }); return; } @@ -312,12 +314,10 @@ export class MovexMasterServer { >, }; - logsy.log( - '[MovexMasterServer] Added Connection Succesfully:', - clientConnection.clientId, - '| Connections', - Object.keys(this.clientConnectionsByClientId).length - ); + logsy.info('Connection Added Succesfully', { + clientId: clientConnection.clientId, + connectionsCount: Object.keys(this.clientConnectionsByClientId).length, + }); // Unsubscribe return () => { @@ -343,39 +343,34 @@ export class MovexMasterServer { this.clientConnectionsByClientId = restOfConnections; - logsy.log( - '[MovexMasterServer] Removed Connection Succesfully for Client:', + logsy.info('Connection Removed', { clientId, - '| Connections Left:', - Object.keys(this.clientConnectionsByClientId).length - ); + connectionsLeft: Object.keys(this.clientConnectionsByClientId).length, + }); } private unsubscribeClientFromResources(clientId: MovexClient['id']) { - const clientSubscricptions = this.subscribersToRidsMap[clientId]; + const clientSubscriptions = this.subscribersToRidsMap[clientId]; - if (!clientSubscricptions) { - logsy.log('No Resource Subscription'); - return; - } - - const subscribedRidsList = objectKeys(clientSubscricptions); + if (clientSubscriptions) { + const subscribedRidsList = objectKeys(clientSubscriptions); - subscribedRidsList.forEach(async (rid) => { - await this.removeResourceSubscriberAndNotifyPeersOfClientUnsubscription( - rid, - clientId - ); + subscribedRidsList.forEach(async (rid) => { + await this.removeResourceSubscriberAndNotifyPeersOfClientUnsubscription( + rid, + clientId + ); - // TODO: should this wait for the client to ack or smtg? probably not needed + // TODO: should this wait for the client to ack or smtg? probably not needed - // Remove the rid from the client's record - const { [rid]: removed, ...rest } = this.subscribersToRidsMap[clientId]; + // Remove the rid from the client's record + const { [rid]: removed, ...rest } = this.subscribersToRidsMap[clientId]; - this.subscribersToRidsMap[clientId] = rest; - }); + this.subscribersToRidsMap[clientId] = rest; + }); + } - logsy.log('Removed client subscriptions ok'); + logsy.info('Client Subscriptions Removed', { clientId }); } private async removeResourceSubscriberAndNotifyPeersOfClientUnsubscription( @@ -387,7 +382,10 @@ export class MovexMasterServer { const masterResource = this.masterResourcesByType[resourceType]; if (!masterResource) { - logsy.error('MasterResourceInexistent', resourceType); + logsy.error( + 'RemoveResourceSubscriberAndNotifyPeersOfClientUnsubscription MasterResourceInexistent', + { resourceType } + ); return; } @@ -399,13 +397,6 @@ export class MovexMasterServer { await masterResource .getSubscribers(rid) .map((clientIds) => { - console.log( - 'notifyPeersOfClientUnsubscription resource', - rid, - 'subscribers', - clientIds - ); - objectKeys(clientIds) .filter((clientId) => clientId !== subscriberClientId) .forEach((peerId) => { @@ -413,9 +404,11 @@ export class MovexMasterServer { if (!peerConnection) { logsy.error( - 'notifyPeersOfClientUnsubscription Peer Connection not found for', - peerId, - this.clientConnectionsByClientId + 'RemoveResourceSubscriberAndNotifyPeersOfClientUnsubscription Peer Connection not found for', + { + peerId, + resourceType, + } ); return; } diff --git a/libs/movex-master/src/lib/orchestrator.ts b/libs/movex-master/src/lib/orchestrator.ts index 7d033aa0..3ef989d5 100644 --- a/libs/movex-master/src/lib/orchestrator.ts +++ b/libs/movex-master/src/lib/orchestrator.ts @@ -2,19 +2,21 @@ import { MovexClient, ResourceIdentifier, invoke, - logsy, + globalLogsy, AnyAction, MovexReducer, ConnectionToMaster, BaseMovexDefinitionResourcesMap, MovexDefinition, ConnectionToClient, -} from 'movex-core-util'; +} from 'movex-core-util'; import { Movex, MovexFromDefintion } from 'movex'; import { MovexMasterResource, MovexMasterServer } from 'movex-master'; import { MemoryMovexStore } from 'movex-store'; import { MockConnectionEmitter } from './MockConnectionEmitter'; +const logsy = globalLogsy.withNamespace('[MovexClientMasterOrchestrator]'); + export const movexClientMasterOrchestrator = () => { let unsubscribe = async () => {}; @@ -136,13 +138,13 @@ export const orchestrateDefinedMovex = < const unsubscribers = [ emitterOnClient._onEmitted((r, ackCb) => { - logsy.log('[Orchestrator] emitterOnClient _onEmitted', r); + logsy.debug('EmitterOnClient _onEmitted', r); // Calling the master with the given event from the client in order to process it emitterOnMaster._publish(r.event, r.payload, ackCb); }), emitterOnMaster._onEmitted((r, ackCb) => { - logsy.log('[Orchestrator] emitterOnMaster _onEmitted', r); - // Calling the client with the given event from the client in order to process it + logsy.debug('EmitterOnMaster _onEmitted', r); + // Calling the client with the given event from the master in order to process it emitterOnClient._publish(r.event, r.payload, ackCb); }), ]; diff --git a/libs/movex-react-local-master/src/lib/MovexLocalMasterProvider.tsx b/libs/movex-react-local-master/src/lib/MovexLocalMasterProvider.tsx index 3eb7a533..11dcdb1a 100644 --- a/libs/movex-react-local-master/src/lib/MovexLocalMasterProvider.tsx +++ b/libs/movex-react-local-master/src/lib/MovexLocalMasterProvider.tsx @@ -1,18 +1,21 @@ import React, { useEffect, useState } from 'react'; import { invoke, - logsy, + globalLogsy, noop, emptyFn, type StringKeys, type GetReducerState, type BaseMovexDefinitionResourcesMap, type MovexDefinition, + LoggingEvent, } from 'movex-core-util'; import { MemoryMovexStore, type MovexStoreItem } from 'movex-store'; import { MovexMasterServer, initMovexMaster } from 'movex-master'; import { MovexLocalContext, MovexLocalContextProps } from './MovexLocalContext'; +const logsy = globalLogsy.withNamespace('[MovexLocalMasterProvider]'); + type Props = React.PropsWithChildren<{ movexDefinition: MovexDefinition; @@ -26,6 +29,9 @@ type Props = >, updateKind: 'create' | 'update' ) => void; + logger?: { + onLog: (event: LoggingEvent) => void; + }; }>; /** @@ -36,7 +42,7 @@ type Props = */ export const MovexLocalMasterProvider: React.FC< Props -> = ({ onInit = noop, onMasterResourceUpdated = noop, ...props }) => { +> = ({ onInit = noop, onMasterResourceUpdated = noop, logger, ...props }) => { const [contextState, setContextState] = useState(); useEffect(() => { @@ -59,21 +65,15 @@ export const MovexLocalMasterProvider: React.FC< >(); const unsubscribers = [ - localStore.onCreated((s) => { - logsy.group('[Master.LocalStore] onCreated'); - logsy.log('Item', s); - logsy.log('All Store', localStore.all()); - logsy.groupEnd(); + localStore.onCreated((item) => { + logsy.info('onCreated', { item }); - onMasterResourceUpdated(s as any, 'create'); + onMasterResourceUpdated(item as any, 'create'); }), - localStore.onUpdated((s) => { - logsy.group('[Master.LocalStore] onUpdated'); - logsy.log('Item', s); - logsy.log('All Store', localStore.all()); - logsy.groupEnd(); + localStore.onUpdated((item) => { + logsy.info('onUpdated', { item }); - onMasterResourceUpdated(s as any, 'update'); + onMasterResourceUpdated(item as any, 'update'); }), ]; @@ -95,6 +95,14 @@ export const MovexLocalMasterProvider: React.FC< } }, [contextState]); + useEffect(() => { + if (logger) { + return globalLogsy.onLog(logger.onLog); + } + + return () => {}; + }, [logger]); + if (!contextState) { return null; } diff --git a/libs/movex-react/src/lib/MovexBoundResourceComponent.tsx b/libs/movex-react/src/lib/MovexBoundResourceComponent.tsx index 381a483f..a4c639ee 100644 --- a/libs/movex-react/src/lib/MovexBoundResourceComponent.tsx +++ b/libs/movex-react/src/lib/MovexBoundResourceComponent.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type { MovexClient } from 'movex'; -import type { +import { GetReducerState, GetReducerAction, ResourceIdentifier, @@ -9,7 +9,6 @@ import type { BaseMovexDefinitionResourcesMap, MovexDefinition, UnsubscribeFn, - DistributiveOmit, } from 'movex-core-util'; import { invoke, isSameResourceIdentifier } from 'movex-core-util'; import { MovexContextStateChange } from './MovexContextStateChange'; diff --git a/libs/movex-server/src/lib/movex-server.ts b/libs/movex-server/src/lib/movex-server.ts index 8d8e97ed..b14bc6df 100644 --- a/libs/movex-server/src/lib/movex-server.ts +++ b/libs/movex-server/src/lib/movex-server.ts @@ -3,7 +3,7 @@ import { Server as SocketServer } from 'socket.io'; import express from 'express'; import cors from 'cors'; import { - logsy, + globalLogsy, SocketIOEmitter, ConnectionToClient, type MovexDefinition, @@ -16,6 +16,8 @@ import { MemoryMovexStore, MovexStore } from 'movex-store'; import { initMovexMaster } from 'movex-master'; import { isOneOf } from './util'; +const logsy = globalLogsy.withNamespace('[MovexServer]'); + export const movexServer = ( { httpServer = http.createServer(), @@ -53,7 +55,7 @@ export const movexServer = ( socket.on('connection', (io) => { const clientId = getClientId(io.handshake.query['clientId'] as string); - logsy.log('[MovexServer] Client Connected', clientId); + logsy.info('Client Connected', { clientId }); const connectionToClient = new ConnectionToClient( clientId, @@ -65,7 +67,7 @@ export const movexServer = ( movexMaster.addClientConnection(connectionToClient); io.on('disconnect', () => { - logsy.log('[MovexServer] Client Disconnected', clientId); + logsy.info('Client Disconnected', { clientId }); movexMaster.removeConnection(clientId); }); @@ -133,10 +135,10 @@ export const movexServer = ( const address = httpServer.address(); if (typeof address !== 'string') { - logsy.info( - `Movex Server started on port ${address?.port} for definition`, - Object.keys(definition.resources) - ); + logsy.info('Server started', { + port, + definitionResources: Object.keys(definition.resources), + }); } }); }; diff --git a/libs/movex-store/src/lib/MemoryMovexStore/MemoryMovexStore.ts b/libs/movex-store/src/lib/MemoryMovexStore/MemoryMovexStore.ts index c2f0774e..d027f2fe 100644 --- a/libs/movex-store/src/lib/MemoryMovexStore/MemoryMovexStore.ts +++ b/libs/movex-store/src/lib/MemoryMovexStore/MemoryMovexStore.ts @@ -31,7 +31,7 @@ import type { } from '../MovexStore'; import { toResultError } from '../ResultError'; -const logsy = globalLogsy.withNamespace('[LocalMovexStore]'); +const logsy = globalLogsy.withNamespace('[MemoryMovexStore]'); export class MemoryMovexStore< TResourcesMap extends BaseMovexDefinitionResourcesMap = BaseMovexDefinitionResourcesMap @@ -304,8 +304,8 @@ export class MemoryMovexStore< ) ); }).mapErr( - AsyncResult.passThrough((e) => { - logsy.error('Update Error', e, rid); + AsyncResult.passThrough((error) => { + logsy.error('Update Error', { error, rid }); }) ); } diff --git a/libs/movex/src/lib/ConnectionToMasterResource.ts b/libs/movex/src/lib/ConnectionToMasterResource.ts index e0fd303e..dbae0148 100644 --- a/libs/movex/src/lib/ConnectionToMasterResource.ts +++ b/libs/movex/src/lib/ConnectionToMasterResource.ts @@ -89,13 +89,7 @@ export class ConnectionToMasterResource< rid: ResourceIdentifier; clientId: MovexClient['id']; }) => { - console.log('ConnectionToMaster onRemoveResourceSubscriberHandler', p); if (toResourceIdentifierObj(p.rid).resourceType !== resourceType) { - console.log( - 'ConnectionToMaster onRemoveResourceSubscriberHandler', - 'nooooope', - p - ); return; } @@ -108,14 +102,7 @@ export class ConnectionToMasterResource< rid: ResourceIdentifier; clientId: MovexClient['id']; }) => { - console.log('ConnectionToMaster onAddResourceSubscriberHandler', p); - if (toResourceIdentifierObj(p.rid).resourceType !== resourceType) { - console.log( - 'ConnectionToMaster onAddResourceSubscriberHandler', - p, - 'noooope' - ); return; } diff --git a/libs/movex/src/lib/MovexResource.ts b/libs/movex/src/lib/MovexResource.ts index ab3bbad1..d78f391b 100644 --- a/libs/movex/src/lib/MovexResource.ts +++ b/libs/movex/src/lib/MovexResource.ts @@ -2,34 +2,22 @@ import type { ResourceIdentifier, ResourceIdentifierStr, UnsubscribeFn, - ActionWithAnyPayload, AnyAction, CheckedReconciliatoryActions, MovexReducer, IOConnection, } from 'movex-core-util'; import { - logsy as rawLogsy, + globalLogsy, toResourceIdentifierObj, toResourceIdentifierStr, - isAction, invoke, } from 'movex-core-util'; import { ConnectionToMasterResource } from './ConnectionToMasterResource'; import { MovexResourceObservable } from './MovexResourceObservable'; import * as deepObject from 'deep-object-diff'; -// TODO: Take away from here as it's adding to the size -const logUnimportantStyle = 'color: grey;'; -const logImportantStyle = 'font-weight: bold;'; -const logIncomingStyle = 'color: #4CAF50; font-weight: bold;'; -const logOutgoingStyle = 'color: #1EA7FD; font-weight: bold;'; -const logOpenConnectionStyle = 'color: #EF5FA0; font-weight: bold'; -const logClosedConnectionStyle = 'color: #DF9D04; font-weight: bold'; -const logErrorStyle = 'color: red; font-weight: bold;'; - -const logsy = rawLogsy.withNamespace('[Movex][MovexResource]'); -// const logsy = rawLogsy; +const logsy = globalLogsy.withNamespace('[Movex][MovexResource]'); export class MovexResource< S, @@ -120,21 +108,14 @@ export class MovexResource< const prevCheckedState = resourceObservable.state; return syncLocalState().map((masterCheckState) => { - logsy.group('State Resynch-ed Warning'); - logsy.log( - '%cPrev (Local) State', - logUnimportantStyle, - prevCheckedState - ); - logsy.log('%cNext (Master) State', logIncomingStyle, masterCheckState); - logsy.debug( - 'Diff', - deepObject.detailedDiff(prevCheckedState, masterCheckState) - ); - logsy.warn( + logsy.warn('State Resynch-ed', { + prevCheckedState, + masterCheckState, + diff: deepObject.detailedDiff(prevCheckedState, masterCheckState), + }); + console.warn( "This shouldn't happen too often! If it does, make sure there's no way around it! See this for more https://github.com/movesthatmatter/movex/issues/8" ); - logsy.groupEnd(); return masterCheckState; }); @@ -151,66 +132,37 @@ export class MovexResource< resourceObservable.syncState(res.state); resourceObservable.updateSubscribers(res.subscribers); }) - .mapErr((e) => { - logsy.error('Add Resource Subscriber Error', e); + .mapErr((error) => { + logsy.error('Add Resource Subscriber Error', { error }); }); const onReconciliateActionsHandler = ( p: CheckedReconciliatoryActions
) => { const prevState = resourceObservable.getCheckedState(); - - logsy.group( - `%c\u{25BC} %cReconciliatory Actions Received (${p.actions.length}). FinalCheckum ${p.finalChecksum}`, - logIncomingStyle, - logUnimportantStyle, - 'Client:', - this.connectionToMaster.clientId - ); - logsy.log('%cPrev state', logUnimportantStyle, prevState[0]); - const nextState = resourceObservable.applyMultipleActions( p.actions ).checkedState; - p.actions.forEach((action, i) => { - logsy.log( - `%cAction(${i + 1}/${p.actions.length}): %c${action.type}`, - logOutgoingStyle, - logImportantStyle, - (action as ActionWithAnyPayload).payload, - action - ); + logsy.log('Reconciliatory Actions Received', { + ...p, + actionsCount: p.actions.length, + clientId: this.connectionToMaster.clientId, + nextState, + prevState, }); - logsy.log('%cNextState', logIncomingStyle, nextState[0]); - logsy.log( - '%cChecksums', - logUnimportantStyle, - prevState[1], - '>', - nextState[1] - ); - - if (nextState[1] === p.finalChecksum) { - logsy.log( - '%cFinal Checksum Matches', - logUnimportantStyle, - p.finalChecksum - ); - } else { + if (nextState[1] !== p.finalChecksum) { // If the checksums are different then it this case it's needed to resync. // See this https://github.com/movesthatmatter/movex/issues/8 resyncLocalState(); // Here is where this happens!!! - logsy.warn( - '%cLocal and Final Master Checksum Mismatch', - logErrorStyle, - nextState[1], - p.finalChecksum - ); + logsy.warn('Local and Final Master Checksum Mismatch', { + ...p, + nextState: nextState[1], + }); } logsy.groupEnd(); @@ -245,33 +197,23 @@ export class MovexResource< // When the checksums are not the same, need to resync the state! // this is expensive and ideally doesn't happen too much. - logsy.group( - `[Movex] Dispatch Ack Error: "Checksums MISMATCH"\n`, - `client: '${this.connectionToMaster.clientId}',\n`, - 'action:', - action - ); + logsy.error(`Dispatch Ack Error: "Checksums MISMATCH"`, { + action, + clientId: this.connectionToMaster.clientId, + }); await resyncLocalState() .map((masterState) => { - logsy.log( - 'Master State:', - JSON.stringify(masterState[0], null, 2), - masterState[1] - ); - logsy.log( - 'Local State:', - JSON.stringify(nextLocalCheckedState[0], null, 2), - nextLocalCheckedState[1] - ); - logsy.log( - 'Diff', - deepObject.detailedDiff(masterState, nextLocalCheckedState) - ); + logsy.info('Resynched Result', { + masterState, + nextLocalCheckedState, + diff: deepObject.detailedDiff( + masterState, + nextLocalCheckedState + ), + }); }) .resolve(); - - logsy.groupEnd(); }); } ), @@ -282,35 +224,12 @@ export class MovexResource< const nextState = resourceObservable.getCheckedState(); - logsy.group( - `%c\u{25BC} %cForwarded Action Received: %c${p.action.type}`, - logIncomingStyle, - logUnimportantStyle, - logImportantStyle, - 'Client:', - this.connectionToMaster.clientId - ); - logsy.log('%cPrev State', logUnimportantStyle, prevState[0]); - logsy.log( - `%cAction: %c${p.action.type}`, - logOutgoingStyle, - logImportantStyle, - (p.action as ActionWithAnyPayload).payload - ); - logsy.log('%cNext State', logIncomingStyle, nextState[0]); - if (prevState[1] !== nextState[1]) { - logsy.log( - '%cchecksums', - logUnimportantStyle, - prevState[1], - '>', - nextState[1] - ); - } else { - logsy.log('%cNo Diff', logErrorStyle, prevState[1]); - } - - logsy.groupEnd(); + logsy.info('Forwarded Action Received', { + ...p, + clientId: this.connectionToMaster.clientId, + prevState, + nextState, + }); }), this.connectionToMasterResource.onReconciliatoryActions( rid, @@ -319,7 +238,7 @@ export class MovexResource< // Subscribers this.connectionToMasterResource.onSubscriberAdded(rid, (clientId) => { - logsy.log('Subscriber Added', clientId); + logsy.info('Subscriber Added', { clientId }); resourceObservable.updateSubscribers((prev) => ({ ...prev, @@ -327,7 +246,7 @@ export class MovexResource< })); }), this.connectionToMasterResource.onSubscriberRemoved(rid, (clientId) => { - logsy.log('Subscriber Removed', clientId); + logsy.info('Subscriber Removed', { clientId }); resourceObservable.updateSubscribers((prev) => { const { [clientId]: removed, ...rest } = prev; @@ -351,66 +270,12 @@ export class MovexResource< next: nextLocalCheckedState, prev: prevLocalCheckedState, }) => { - const [nextLocalState, nextLocalChecksum] = nextLocalCheckedState; - - logsy.group( - `%c\u{25B2} %cAction Dispatched: %c${ - isAction(action) - ? action.type - : action[0].type + ' + ' + action[1].type - }`, - logOutgoingStyle, - logUnimportantStyle, - logImportantStyle, - 'Client:', - this.connectionToMaster.clientId - ); - logsy.log( - '%cPrev state', - logUnimportantStyle, - prevLocalCheckedState[0] - ); - if (isAction(action)) { - logsy.log( - `%cPublic Action: %c${action.type}`, - logOutgoingStyle, - logImportantStyle, - (action as ActionWithAnyPayload).payload - ); - } else { - const [privateAction, publicAction] = action; - logsy.log( - `%cPrivate Action: %c${privateAction.type}`, - logOpenConnectionStyle, - logImportantStyle, - (privateAction as ActionWithAnyPayload).payload - ); - logsy.log( - `%cPublic Action payload: %c${publicAction.type}`, - logOutgoingStyle, - logImportantStyle, - (publicAction as ActionWithAnyPayload).payload - ); - } - logsy.log('%cNext State', logIncomingStyle, nextLocalState); - - if (prevLocalCheckedState[1] !== nextLocalChecksum) { - logsy.log( - '%cChecksums', - logUnimportantStyle, - prevLocalCheckedState[1], - '>', - nextLocalChecksum - ); - } else { - logsy.log( - '%cNo Diff', - logUnimportantStyle, - prevLocalCheckedState[1] - ); - } - - logsy.groupEnd(); + logsy.info('Action Dispatched', { + action, + clientId: this.connectionToMaster.clientId, + prevState: prevLocalCheckedState, + nextLocalState: nextLocalCheckedState, + }); } ), ]; diff --git a/libs/movex/src/lib/MovexResourceObservable.spec.ts b/libs/movex/src/lib/MovexResourceObservable.spec.ts index 98de4d36..16bc56d0 100644 --- a/libs/movex/src/lib/MovexResourceObservable.spec.ts +++ b/libs/movex/src/lib/MovexResourceObservable.spec.ts @@ -13,14 +13,6 @@ import { const rid: ResourceIdentifier = 'counter:test-id'; -beforeAll(() => { - globalLogsy.disable(); -}); - -afterAll(() => { - globalLogsy.enable(); -}); - test('Dispatch Local Actions', async () => { const $resource = new MovexResourceObservable( 'test-client', diff --git a/libs/movex/src/lib/MovexResourceObservable.ts b/libs/movex/src/lib/MovexResourceObservable.ts index 8256d845..96a7aa28 100644 --- a/libs/movex/src/lib/MovexResourceObservable.ts +++ b/libs/movex/src/lib/MovexResourceObservable.ts @@ -2,7 +2,7 @@ import { Pubsy } from 'ts-pubsy'; import { Err, Ok, Result } from 'ts-results'; import { invoke, - logsy, + globalLogsy, computeCheckedState, isAction, Observable, @@ -24,6 +24,8 @@ import type { import { createDispatcher, DispatchedEvent } from './dispatch'; import { PromiseDelegate } from 'promise-delegate'; +const logsy = globalLogsy.withNamespace('[MovexResourceObservable]'); + type ObservedItem = { subscribers: Record; checkedState: CheckedState; @@ -106,7 +108,7 @@ export class MovexResourceObservable< this.dispatcher = (...args: Parameters) => { if (!this.isSynchedPromiseDelegate.settled) { - logsy.info('[Movex] Attempt to dispatch before sync!', ...args); + logsy.warn('Attempt to dispatch before sync!', { args }); } this.isSynchedPromiseDelegate.promise.then(() => { From 96b19aef1178ae74f6fcc72fa2c593035df41b07 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Sat, 6 Apr 2024 22:25:28 -0600 Subject: [PATCH 18/25] feat(rps-v2): :sparkles: hookup a logger to the MovexLocalProvider --- apps/movex-demo/pages/local/rps-v2/Game.tsx | 27 ++++++++----------- apps/movex-demo/pages/local/rps-v2/GameUI.tsx | 4 +-- apps/movex-demo/pages/local/rps-v2/index.tsx | 6 +++++ libs/movex-react/src/lib/MovexProvider.tsx | 15 ++++++++++- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/apps/movex-demo/pages/local/rps-v2/Game.tsx b/apps/movex-demo/pages/local/rps-v2/Game.tsx index 2f059ec6..5e7612a0 100644 --- a/apps/movex-demo/pages/local/rps-v2/Game.tsx +++ b/apps/movex-demo/pages/local/rps-v2/Game.tsx @@ -1,10 +1,10 @@ -import * as React from "react"; -import { MovexBoundResource } from "movex-react"; -import movexConfig from "./movex.config"; -import { initialState } from "./movex"; -import { useState } from "react"; -import { ResourceIdentifier, toResourceIdentifierStr } from "movex-core-util"; -import { GameUI } from "./GameUI"; +import * as React from 'react'; +import { MovexBoundResource } from 'movex-react'; +import movexConfig from './movex.config'; +import { initialState } from './movex'; +import { useState } from 'react'; +import { ResourceIdentifier, toResourceIdentifierStr } from 'movex-core-util'; +import { GameUI } from './GameUI'; // import { MovexStoreItem } from "movex"; import { MovexLocalInstance } from 'movex-react-local-master'; import { MovexStoreItem } from 'movex-store'; @@ -14,7 +14,7 @@ type Props = { }; export function Game(props: Props) { - const [rpsRid, setRpsRid] = useState>(); + const [rpsRid, setRpsRid] = useState>(); const [masterStateUpdated, setMasterStateUpdated] = useState(false); return ( @@ -23,7 +23,7 @@ export function Game(props: Props) { clientId="playerA" movexDefinition={movexConfig} onConnected={(movex) => { - const reg = movex.register("rps"); + const reg = movex.register('rps'); reg.create(initialState).map(({ rid }) => { setRpsRid(rid); @@ -39,8 +39,6 @@ export function Game(props: Props) { }} > {rpsRid && ( - <> - yesss )} /> - )}
@@ -63,7 +60,7 @@ export function Game(props: Props) {
@@ -78,7 +75,7 @@ export function Game(props: Props) { {JSON.stringify( { submissions: props.masterStore.state[0].submissions, - winner: props.masterStore.state[0].winner + winner: props.masterStore.state[0].winner, }, null, 1 @@ -91,13 +88,11 @@ export function Game(props: Props) {
{rpsRid && masterStateUpdated && ( - here 2 (
- yess works
)} diff --git a/apps/movex-demo/pages/local/rps-v2/GameUI.tsx b/apps/movex-demo/pages/local/rps-v2/GameUI.tsx index 98f92ab7..60947fff 100644 --- a/apps/movex-demo/pages/local/rps-v2/GameUI.tsx +++ b/apps/movex-demo/pages/local/rps-v2/GameUI.tsx @@ -1,7 +1,7 @@ import React from "react"; import { MovexBoundResourceFromConfig } from "movex-react"; import { useCallback, useEffect, useMemo } from "react"; -import { logsy } from "movex-core-util"; +import { globalLogsy } from "movex-core-util"; import movexConfig from "./movex.config"; import { PlayerLabel, @@ -68,7 +68,7 @@ export const GameUI: React.FC = ({ } if (availableLabels.length === 0) { - logsy.warn("Player Slots taken"); + globalLogsy.warn("Player Slots taken"); return; } diff --git a/apps/movex-demo/pages/local/rps-v2/index.tsx b/apps/movex-demo/pages/local/rps-v2/index.tsx index 0e7cfe30..0efdd323 100644 --- a/apps/movex-demo/pages/local/rps-v2/index.tsx +++ b/apps/movex-demo/pages/local/rps-v2/index.tsx @@ -15,6 +15,12 @@ export default function App() { { + // console.log('event', method, prefix, message, payload) + console[method](prefix + ' ' + message, payload); + }, + }} > diff --git a/libs/movex-react/src/lib/MovexProvider.tsx b/libs/movex-react/src/lib/MovexProvider.tsx index 8991ad08..87c9422c 100644 --- a/libs/movex-react/src/lib/MovexProvider.tsx +++ b/libs/movex-react/src/lib/MovexProvider.tsx @@ -8,6 +8,8 @@ import { type MovexDefinition, ResourceIdentifier, StringKeys, + LoggingEvent, + globalLogsy, } from 'movex-core-util'; import { MovexClient } from 'movex'; import { ResourceObservablesRegistry } from './ResourceObservableRegistry'; @@ -29,11 +31,14 @@ type Props = { connected: false } > ) => void; + logger?: { + onLog: (event: LoggingEvent) => void; + }; }>; export const MovexProvider: React.FC< Props -> = ({ onConnected = noop, onDisconnected = noop, ...props }) => { +> = ({ onConnected = noop, onDisconnected = noop, logger, ...props }) => { type TResourcesMap = typeof props['movexDefinition']['resources']; const [contextState, setContextState] = useState< @@ -108,6 +113,14 @@ export const MovexProvider: React.FC< } }, [contextState.connected, onDisconnected]); + useEffect(() => { + if (logger) { + return globalLogsy.onLog(logger.onLog); + } + + return () => {}; + }, [logger]); + return ( {props.children} From a29a6ed6d594102ca82d5d1fc80dd621533b5ed2 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 11 Apr 2024 17:10:51 -0600 Subject: [PATCH 19/25] ci(libs version): :ferris_wheel: bump to 0.1.5-1 --- libs/movex-core-util/package.json | 2 +- libs/movex-master/package.json | 2 +- libs/movex-react-local-master/package.json | 2 +- libs/movex-react/package.json | 2 +- libs/movex-server/package.json | 2 +- libs/movex-service/package.json | 2 +- libs/movex-store/package.json | 2 +- libs/movex/package.json | 2 +- package.json | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/movex-core-util/package.json b/libs/movex-core-util/package.json index 505952b7..738f7d27 100644 --- a/libs/movex-core-util/package.json +++ b/libs/movex-core-util/package.json @@ -1,6 +1,6 @@ { "name": "movex-core-util", - "version": "0.1.5-0", + "version": "0.1.5-1", "description": "Movex Core Util is the library of utilities for Movex", "license": "MIT", "author": { diff --git a/libs/movex-master/package.json b/libs/movex-master/package.json index 6096e5e5..2f50b8ca 100644 --- a/libs/movex-master/package.json +++ b/libs/movex-master/package.json @@ -1,6 +1,6 @@ { "name": "movex-master", - "version": "0.1.5-0", + "version": "0.1.5-1", "license": "MIT", "description": "Movex-master defines the master that wil be used on movex-server and movex-react-local-master", "author": { diff --git a/libs/movex-react-local-master/package.json b/libs/movex-react-local-master/package.json index fb864314..a90c6404 100644 --- a/libs/movex-react-local-master/package.json +++ b/libs/movex-react-local-master/package.json @@ -1,6 +1,6 @@ { "name": "movex-react-local-master", - "version": "0.1.5-0", + "version": "0.1.5-1", "license": "MIT", "description": "Run the movex-master locally with react", "author": { diff --git a/libs/movex-react/package.json b/libs/movex-react/package.json index b3ea220e..7b164cc0 100644 --- a/libs/movex-react/package.json +++ b/libs/movex-react/package.json @@ -1,6 +1,6 @@ { "name": "movex-react", - "version": "0.1.5-0", + "version": "0.1.5-1", "license": "MIT", "description": "Movex React is the library of React components for Movex", "author": { diff --git a/libs/movex-server/package.json b/libs/movex-server/package.json index e4aab595..3b6a1736 100644 --- a/libs/movex-server/package.json +++ b/libs/movex-server/package.json @@ -1,6 +1,6 @@ { "name": "movex-server", - "version": "0.1.5-0", + "version": "0.1.5-1", "license": "MIT", "type": "commonjs", "description": "Movex Server is the backend runtime for Movex", diff --git a/libs/movex-service/package.json b/libs/movex-service/package.json index cd0dd2f6..73a09518 100644 --- a/libs/movex-service/package.json +++ b/libs/movex-service/package.json @@ -1,6 +1,6 @@ { "name": "movex-service", - "version": "0.1.5-0", + "version": "0.1.5-1", "license": "MIT", "type": "commonjs", "description": "Movex Service is the CLI for Movex", diff --git a/libs/movex-store/package.json b/libs/movex-store/package.json index f926d2ec..4377810f 100644 --- a/libs/movex-store/package.json +++ b/libs/movex-store/package.json @@ -1,6 +1,6 @@ { "name": "movex-store", - "version": "0.1.5-0", + "version": "0.1.5-1", "license": "MIT", "description": "Movex-store defines the store interface and comes with a MemoryStore", "author": { diff --git a/libs/movex/package.json b/libs/movex/package.json index 3bb2beb1..88347046 100644 --- a/libs/movex/package.json +++ b/libs/movex/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.5-0", + "version": "0.1.5-1", "license": "MIT", "description": "Movex is a Multiplayer (Game) State Synchronization Library using Deterministic Action Propagation without the need to write Server Specific Code.", "author": { diff --git a/package.json b/package.json index a583d1c8..e39a9628 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.5-0", + "version": "0.1.5-1", "license": "MIT", "author": { "name": "Gabriel C. Troia", From cb30fc18d378294f0446de7faedf34fe87f103a2 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 11 Apr 2024 17:29:13 -0600 Subject: [PATCH 20/25] fix(movex-react): :bug: add the logger to MovexProviderClass as well --- libs/movex-react/src/lib/MovexProvider.tsx | 46 +++++++-------- .../src/lib/MovexProviderClass.tsx | 56 ++++++++----------- 2 files changed, 46 insertions(+), 56 deletions(-) diff --git a/libs/movex-react/src/lib/MovexProvider.tsx b/libs/movex-react/src/lib/MovexProvider.tsx index 87c9422c..63a36e12 100644 --- a/libs/movex-react/src/lib/MovexProvider.tsx +++ b/libs/movex-react/src/lib/MovexProvider.tsx @@ -14,30 +14,32 @@ import { import { MovexClient } from 'movex'; import { ResourceObservablesRegistry } from './ResourceObservableRegistry'; -type Props = - React.PropsWithChildren<{ - movexDefinition: MovexDefinition; - endpointUrl: string; - clientId?: MovexClientUser['id']; - onConnected?: ( - state: Extract< - MovexContextProps, - { connected: true } - > - ) => void; - onDisconnected?: ( - state: Extract< - MovexContextProps, - { connected: false } - > - ) => void; - logger?: { - onLog: (event: LoggingEvent) => void; - }; - }>; +export type MovexProviderProps< + TMovexConfigResourcesMap extends BaseMovexDefinitionResourcesMap +> = React.PropsWithChildren<{ + movexDefinition: MovexDefinition; + endpointUrl: string; + clientId?: MovexClientUser['id']; + onConnected?: ( + state: Extract< + MovexContextProps, + { connected: true } + > + ) => void; + onDisconnected?: ( + state: Extract< + MovexContextProps, + { connected: false } + > + ) => void; + logger?: { + onLog: (event: LoggingEvent) => void; + }; + test:number; +}>; export const MovexProvider: React.FC< - Props + MovexProviderProps > = ({ onConnected = noop, onDisconnected = noop, logger, ...props }) => { type TResourcesMap = typeof props['movexDefinition']['resources']; diff --git a/libs/movex-react/src/lib/MovexProviderClass.tsx b/libs/movex-react/src/lib/MovexProviderClass.tsx index 5c1846a8..80c95159 100644 --- a/libs/movex-react/src/lib/MovexProviderClass.tsx +++ b/libs/movex-react/src/lib/MovexProviderClass.tsx @@ -4,25 +4,28 @@ import type { StringKeys, MovexDefinition, BaseMovexDefinitionResourcesMap, -} from 'movex-core-util'; +} from 'movex-core-util'; import type { MovexContextProps } from './MovexContext'; -import { MovexProvider } from './MovexProvider'; +import { MovexProvider, MovexProviderProps } from './MovexProvider'; type Props< TResourcesMap extends BaseMovexDefinitionResourcesMap, TResourceType extends Extract -> = React.PropsWithChildren<{ - movexDefinition: MovexDefinition; - endpointUrl: string; - clientId?: MovexClient['id']; - onConnected?: ( - state: Extract< - MovexContextProps, - { connected: true } - >['movex'] - ) => void; - onDisconnected?: () => void; -}>; +> = React.PropsWithChildren< + Omit< + MovexProviderProps, + 'movexDefinition' | 'onConnected' | 'onDisconnected' + > & { + movexDefinition: MovexDefinition; + onConnected?: ( + state: Extract< + MovexContextProps, + { connected: true } + >['movex'] + ) => void; + onDisconnected?: () => void; + } +>; type State = { clientId?: MovexClient['id']; @@ -36,36 +39,21 @@ export class MovexProviderClass< TResourcesMap extends BaseMovexDefinitionResourcesMap, TResourceType extends StringKeys > extends React.Component, State> { - constructor(props: Props) { - super(props); - - this.state = {}; - } - override render() { + const { onConnected, ...props } = this.props; + return ( { - this.setState({ clientId: r.clientId }); - - this.props.onConnected?.( + onConnected?.( r.movex as Extract< MovexContextProps, { connected: true } >['movex'] ); }} - // onDisconnected={() => { - // this.setState({ clientId: undefined }); - - // this.props.onDisconnected?.(); - // }} - > - {this.props.children} - + {...props} + /> ); } } From 0af75b00a0b9340b959328fa099457dae420e077 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 11 Apr 2024 17:35:40 -0600 Subject: [PATCH 21/25] ci(movex-react): :ferris_wheel: bump version to 0.1.5-1 --- libs/movex-react/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/movex-react/package.json b/libs/movex-react/package.json index 7b164cc0..5999b94d 100644 --- a/libs/movex-react/package.json +++ b/libs/movex-react/package.json @@ -1,6 +1,6 @@ { "name": "movex-react", - "version": "0.1.5-1", + "version": "0.1.5-2", "license": "MIT", "description": "Movex React is the library of React components for Movex", "author": { From 358dd782c5e3ea361702782ce0cd177e3727d4df Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 11 Apr 2024 17:37:52 -0600 Subject: [PATCH 22/25] fix(movex-react): :bug: remove unneded "test" prop from movex-provider --- libs/movex-react/src/lib/MovexProvider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/movex-react/src/lib/MovexProvider.tsx b/libs/movex-react/src/lib/MovexProvider.tsx index 63a36e12..ca9eaaec 100644 --- a/libs/movex-react/src/lib/MovexProvider.tsx +++ b/libs/movex-react/src/lib/MovexProvider.tsx @@ -35,7 +35,6 @@ export type MovexProviderProps< logger?: { onLog: (event: LoggingEvent) => void; }; - test:number; }>; export const MovexProvider: React.FC< From 9660c7608e461823062185bd68f73c2c277f944d Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Thu, 11 Apr 2024 17:38:38 -0600 Subject: [PATCH 23/25] ci(movex-react): :ferris_wheel: bump to 0.1.5-3 --- libs/movex-react/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/movex-react/package.json b/libs/movex-react/package.json index 5999b94d..23a360d2 100644 --- a/libs/movex-react/package.json +++ b/libs/movex-react/package.json @@ -1,6 +1,6 @@ { "name": "movex-react", - "version": "0.1.5-2", + "version": "0.1.5-3", "license": "MIT", "description": "Movex React is the library of React components for Movex", "author": { From 0ab9a6b5ec808da3b9027d5f6aaee11df8721289 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Wed, 17 Apr 2024 13:24:18 -0600 Subject: [PATCH 24/25] feat(all-libs): :sparkles: add ability to pass in Client Info Object and sync it to all peers --- libs/movex-core-util/src/lib/core-types.ts | 11 +- .../src/lib/io/BaseConnection.ts | 10 +- .../src/lib/io/ConnectionToClient.ts | 13 ++- .../src/lib/io/ConnectionToMaster.ts | 20 +++- libs/movex-core-util/src/lib/io/IOEvents.ts | 11 +- .../src/lib/MockConnectionEmitter.ts | 1 - .../movex-master/src/lib/MovexMasterServer.ts | 110 ++++++++++++++++-- libs/movex-master/src/lib/index.ts | 2 +- libs/movex-master/src/lib/util.ts | 40 ++++--- .../ClientMaster.chat.spec.ts | 21 +--- .../ClientMaster.match.spec.ts | 16 +-- .../ClientMaster.rps.spec.ts | 22 +--- .../ClientMaster.spec.ts | 53 ++++----- .../console-group.d.ts | 1 - .../orchestrator.ts | 89 ++++++-------- libs/movex-master/src/specs/Movex.spec.ts | 31 ++--- libs/movex-master/src/specs/Todo.md | 110 ------------------ .../src/lib/ClientMasterOrchestrator.ts | 49 ++++++++ .../src/lib/MovexLocalProvider.tsx | 12 +- libs/movex-react/src/index.ts | 2 + libs/movex-react/src/lib/MovexContext.ts | 44 ++++--- libs/movex-react/src/lib/MovexProvider.tsx | 65 +++++++++-- libs/movex-react/src/lib/hooks.ts | 19 ++- libs/movex-server/src/lib/movex-server.ts | 17 ++- libs/movex-store/src/lib/MovexStore.ts | 7 +- ...urce.ts => ConnectionToMasterResources.ts} | 18 ++- libs/movex/src/lib/Movex.ts | 21 +++- libs/movex/src/lib/MovexFromDefintion.ts | 10 +- libs/movex/src/lib/MovexResource.ts | 44 +++---- libs/movex/src/lib/MovexResourceObservable.ts | 7 +- libs/movex/src/lib/init.ts | 35 +++--- 31 files changed, 499 insertions(+), 412 deletions(-) delete mode 100644 libs/movex-master/src/specs/ClientMasterOrchestration/console-group.d.ts rename libs/movex-master/src/{lib => specs/ClientMasterOrchestration}/orchestrator.ts (58%) delete mode 100644 libs/movex-master/src/specs/Todo.md create mode 100644 libs/movex-react-local-master/src/lib/ClientMasterOrchestrator.ts rename libs/movex/src/lib/{ConnectionToMasterResource.ts => ConnectionToMasterResources.ts} (95%) diff --git a/libs/movex-core-util/src/lib/core-types.ts b/libs/movex-core-util/src/lib/core-types.ts index 3d55fb96..f64d790c 100644 --- a/libs/movex-core-util/src/lib/core-types.ts +++ b/libs/movex-core-util/src/lib/core-types.ts @@ -158,7 +158,7 @@ export type MovexClientResourceShape< > = { state: CheckedState; rid: ResourceIdentifierStr; - subscribers: Record; + subscribers: Record; }; // TODO: Remove all of these if not used @@ -222,9 +222,11 @@ export type GenericResourceOfType = Resource< export type GenericResourceType = GenericResource['type']; -export type MovexClient = { +export type MovexClientInfo = UnknownRecord; + +export type MovexClient = { id: string; - info?: Info; // User Info or whatever + info: Info; // User Info or whatever subscriptions: Record< ResourceIdentifierStr, { @@ -234,6 +236,9 @@ export type MovexClient = { >; }; +export type SanitizedMovexClient = + Pick, 'id' | 'info'>; + export type ResourceIdentifierObj = { resourceType: TResourceType; resourceId: GenericResource['id']; diff --git a/libs/movex-core-util/src/lib/io/BaseConnection.ts b/libs/movex-core-util/src/lib/io/BaseConnection.ts index f94ab5b0..a0c34536 100644 --- a/libs/movex-core-util/src/lib/io/BaseConnection.ts +++ b/libs/movex-core-util/src/lib/io/BaseConnection.ts @@ -1,19 +1,19 @@ import type { EventEmitter } from '../EventEmitter'; import type { AnyAction } from '../action'; -import type { MovexClient } from '../core-types'; +import type { MovexClient, MovexClientInfo } from '../core-types'; import type { IOConnection } from './IOConnection'; import type { IOEvents } from './IOEvents'; export class BaseConnection< TState, TAction extends AnyAction, - TResourceType extends string + TResourceType extends string, + TClientInfo extends MovexClientInfo > implements IOConnection { constructor( public clientId: MovexClient['id'], - public emitter: EventEmitter> + public emitter: EventEmitter>, + public clientInfo: TClientInfo = {} as TClientInfo ) {} } - -// console.log('BaseConnection 3', BaseConnection); \ No newline at end of file diff --git a/libs/movex-core-util/src/lib/io/ConnectionToClient.ts b/libs/movex-core-util/src/lib/io/ConnectionToClient.ts index 2de8dcc6..1cee3557 100644 --- a/libs/movex-core-util/src/lib/io/ConnectionToClient.ts +++ b/libs/movex-core-util/src/lib/io/ConnectionToClient.ts @@ -1,14 +1,23 @@ import type { AnyAction } from '../action'; +import { MovexClientInfo } from '../core-types'; import { BaseConnection } from './BaseConnection'; export class ConnectionToClient< TState, TAction extends AnyAction, - TResourceType extends string -> extends BaseConnection { + TResourceType extends string, + TClientInfo extends MovexClientInfo +> extends BaseConnection { // This needs to be called right away as the connection gets created // TODO: Might consider moving into the constructor emitClientId() { this.emitter.emit('setClientId', this.clientId); } + + emitClientReady() { + this.emitter.emit('onClientReady', { + id: this.clientId, + info: this.clientInfo, + }); + } } diff --git a/libs/movex-core-util/src/lib/io/ConnectionToMaster.ts b/libs/movex-core-util/src/lib/io/ConnectionToMaster.ts index cb1268b9..e8fb1a81 100644 --- a/libs/movex-core-util/src/lib/io/ConnectionToMaster.ts +++ b/libs/movex-core-util/src/lib/io/ConnectionToMaster.ts @@ -1,8 +1,24 @@ import type { AnyAction } from '../action'; +import { + MovexClientInfo, + SanitizedMovexClient, + UnknownRecord, +} from '../core-types'; +import { EventEmitter } from '../EventEmitter'; import { BaseConnection } from './BaseConnection'; +import { IOEvents } from './IOEvents'; export class ConnectionToMaster< TState, TAction extends AnyAction, - TResourceType extends string -> extends BaseConnection {} + TResourceType extends string, + TClientInfo extends MovexClientInfo +> extends BaseConnection { + // constructor( + // clientId: SanitizedMovexClient['id'], + // emitter: EventEmitter>, + // public clientInfo: TClientInfo + // ) { + // super(clientId, emitter); + // } +} diff --git a/libs/movex-core-util/src/lib/io/IOEvents.ts b/libs/movex-core-util/src/lib/io/IOEvents.ts index 0f68f615..ca267340 100644 --- a/libs/movex-core-util/src/lib/io/IOEvents.ts +++ b/libs/movex-core-util/src/lib/io/IOEvents.ts @@ -12,6 +12,7 @@ import type { MovexClient, ResourceIdentifier, ResourceIdentifierStr, + SanitizedMovexClient, } from '../core-types'; export type IOEvents< @@ -22,8 +23,6 @@ export type IOEvents< /** * The following events are directed from Client to Master * */ - setClientId: (clientId: string) => void; - createResource: (p: { resourceType: TResourceType; resourceState: TState; @@ -42,6 +41,7 @@ export type IOEvents< // But adds the client to the Resource list of subscribers addResourceSubscriber: (p: { rid: ResourceIdentifier; + clientInfo?: MovexClient['info']; }) => IOPayloadResult< MovexClientResourceShape, unknown // Type this @@ -85,6 +85,10 @@ export type IOEvents< /** * The following events are directed from Master to Client * */ + // @deprecate in favor ofClientReady + setClientId: (clientId: string) => void; + + onClientReady: (client: SanitizedMovexClient) => void; onFwdAction: ( payload: { @@ -98,7 +102,8 @@ export type IOEvents< ) => IOPayloadResult; onResourceSubscriberAdded: (p: { rid: ResourceIdentifier; - clientId: MovexClient['id']; + client: Pick; + // clientId: MovexClient['id']; }) => IOPayloadResult< void, unknown // Type this diff --git a/libs/movex-master/src/lib/MockConnectionEmitter.ts b/libs/movex-master/src/lib/MockConnectionEmitter.ts index c2bb1fa9..edfdddcb 100644 --- a/libs/movex-master/src/lib/MockConnectionEmitter.ts +++ b/libs/movex-master/src/lib/MockConnectionEmitter.ts @@ -5,7 +5,6 @@ import { IOEvents, globalLogsy, } from 'movex-core-util'; - import { Pubsy } from 'ts-pubsy'; import { getRandomInt, getUuid } from './util'; diff --git a/libs/movex-master/src/lib/MovexMasterServer.ts b/libs/movex-master/src/lib/MovexMasterServer.ts index 9d100412..612b30ad 100644 --- a/libs/movex-master/src/lib/MovexMasterServer.ts +++ b/libs/movex-master/src/lib/MovexMasterServer.ts @@ -7,13 +7,18 @@ import { type IOEvents, type ConnectionToClient, type AnyStringResourceIdentifier, + MovexClientInfo, + SanitizedMovexClient, + GenericResourceType, } from 'movex-core-util'; import { AsyncResult } from 'ts-async-results'; import { Err, Ok } from 'ts-results'; import { MovexMasterResource } from './MovexMasterResource'; -import { storeItemToSanitizedClientResource } from './util'; +import { itemToSanitizedClientResource } from './util'; +import { MovexStoreItem } from 'movex-store'; const logsy = globalLogsy.withNamespace('[MovexMasterServer]'); + /** * This lives on the server most likely, and it's where the * fwd and the reconciliatory action logic reside @@ -26,10 +31,10 @@ export class MovexMasterServer { private clientConnectionsByClientId: Record< MovexClient['id'], - ConnectionToClient + ConnectionToClient > = {}; - // A Map of subscribers to their subsxribed resources + // A Map of subscribers to their subscribed resources private subscribersToRidsMap: Record< MovexClient['id'], Record @@ -39,15 +44,40 @@ export class MovexMasterServer { private masterResourcesByType: Record> ) {} + /** + * Returns the Active Client Connections + * + * @returns + */ + allClients() { + return objectKeys(this.clientConnectionsByClientId).reduce( + (prev, nextClientId) => ({ + ...prev, + [nextClientId]: { + id: this.clientConnectionsByClientId[nextClientId].clientId, + info: this.clientConnectionsByClientId[nextClientId].clientInfo, + subscriptions: { + ...this.subscribersToRidsMap[nextClientId], + subscribedAt: -1, // TODO: fix this if needed + }, + }, + }), + {} as Record + ); + } + /** * Adds the Client Connection and subscribers to all the client events * * @param clientConnection * @returns */ - addClientConnection( - clientConnection: ConnectionToClient - ) { + addClientConnection< + S, + A extends AnyAction, + TResourceType extends string, + TClientInfo extends MovexClientInfo + >(clientConnection: ConnectionToClient) { // Event Handlers const onEmitActionHandler = ( payload: Parameters< @@ -146,9 +176,15 @@ export class MovexMasterServer { masterResource .getClientSpecificResource(rid, clientConnection.clientId) - .map((r) => - acknowledge?.(new Ok(storeItemToSanitizedClientResource(r))) - ) + .map((r) => { + acknowledge?.( + new Ok( + itemToSanitizedClientResource( + this.populateClientInfoToSubscribers(r) + ) + ) + ); + }) .mapErr( AsyncResult.passThrough((e) => { logsy.error('Get Resource Error', e); @@ -215,7 +251,13 @@ export class MovexMasterServer { masterResource .create(resourceType, resourceState, resourceId) .map((r) => - acknowledge?.(new Ok(storeItemToSanitizedClientResource(r))) + acknowledge?.( + new Ok( + itemToSanitizedClientResource( + this.populateClientInfoToSubscribers(r) + ) + ) + ) ) .mapErr( AsyncResult.passThrough((e) => { @@ -254,7 +296,13 @@ export class MovexMasterServer { }; // Send the ack to the just-added-client - acknowledge?.(new Ok(storeItemToSanitizedClientResource(s))); + acknowledge?.( + new Ok( + itemToSanitizedClientResource( + this.populateClientInfoToSubscribers(s) + ) + ) + ); // Let the rest of the peer-clients know as well objectKeys(s.subscribers) @@ -271,9 +319,14 @@ export class MovexMasterServer { return; } + const client: SanitizedMovexClient = { + id: clientConnection.clientId, + info: clientConnection.clientInfo, + }; + peerConnection.emitter.emit('onResourceSubscriberAdded', { rid: payload.rid, - clientId: clientConnection.clientId, + client, }); }); }) @@ -310,6 +363,7 @@ export class MovexMasterServer { [clientConnection.clientId]: clientConnection as ConnectionToClient< any, AnyAction, + any, any >, }; @@ -335,6 +389,38 @@ export class MovexMasterServer { }; } + private populateClientInfoToSubscribers = < + TResourceType extends GenericResourceType, + TState + >( + item: MovexStoreItem + ): MovexStoreItem & { + subscribers: Record< + MovexClient['id'], + { + info: MovexClient['info']; + } + >; + } => ({ + ...item, + subscribers: objectKeys(item.subscribers).reduce( + (prev, next) => ({ + ...prev, + [next]: { + ...item.subscribers[next], + info: this.allClients()[next]?.info ?? {}, + }, + }), + {} as Record< + MovexClient['id'], + { + subscribedAt: number; + info: MovexClient['info']; + } + > + ), + }); + removeConnection(clientId: MovexClient['id']) { this.unsubscribeClientFromResources(clientId); diff --git a/libs/movex-master/src/lib/index.ts b/libs/movex-master/src/lib/index.ts index 07b6ffd0..cccea4d4 100644 --- a/libs/movex-master/src/lib/index.ts +++ b/libs/movex-master/src/lib/index.ts @@ -1,6 +1,6 @@ export * from './MovexMasterServer'; export * from './MovexMasterResource'; export * from './init'; -export * from './orchestrator'; +export * from '../specs/ClientMasterOrchestration/orchestrator'; export * from './MockConnectionEmitter'; export * from './util'; diff --git a/libs/movex-master/src/lib/util.ts b/libs/movex-master/src/lib/util.ts index e06a6f55..edb8d532 100644 --- a/libs/movex-master/src/lib/util.ts +++ b/libs/movex-master/src/lib/util.ts @@ -7,6 +7,8 @@ import { MovexClientResourceShape, toResourceIdentifierStr, objectKeys, + MovexClientInfo, + MovexClient, } from 'movex-core-util'; import { MovexStoreItem } from 'movex-store'; @@ -69,21 +71,29 @@ export function getRandomInt(givenMin: number, givenMax: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } -export const storeItemToSanitizedClientResource = < +export const itemToSanitizedClientResource = < TResourceType extends GenericResourceType, TState >( - storeItem: MovexStoreItem -): MovexClientResourceShape => { - return { - rid: toResourceIdentifierStr(storeItem.rid), - state: storeItem.state, - subscribers: objectKeys(storeItem.subscribers).reduce( - (prev, next) => ({ - ...prev, - [next]: null, - }), - {} as MovexClientResourceShape['subscribers'] - ), - }; -}; + item: MovexStoreItem & { + subscribers: Record< + MovexClient['id'], + { + info: MovexClient['info']; + } + >; + } +): MovexClientResourceShape => ({ + rid: toResourceIdentifierStr(item.rid), + state: item.state, + subscribers: objectKeys(item.subscribers).reduce( + (prev, nextSubId) => ({ + ...prev, + [nextSubId]: { + id: nextSubId, + info: item.subscribers[nextSubId].info || {}, + }, + }), + {} as MovexClientResourceShape['subscribers'] + ), +}); diff --git a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.chat.spec.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.chat.spec.ts index 46d345a9..fc4759d8 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.chat.spec.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.chat.spec.ts @@ -1,24 +1,9 @@ -import { globalLogsy, computeCheckedState } from 'movex-core-util'; -import { - tillNextTick, - chatReducer, - initialChatState, -} from 'movex-specs-util'; -import { movexClientMasterOrchestrator } from 'movex-master'; +import { computeCheckedState } from 'movex-core-util'; +import { tillNextTick, chatReducer, initialChatState } from 'movex-specs-util'; +import { movexClientMasterOrchestrator } from './orchestrator'; const orchestrator = movexClientMasterOrchestrator(); -// console.log('movexClientMasterOrchestrator', movexClientMasterOrchestrator); -// console.log('orchestrator', orchestrator); - -beforeAll(() => { - globalLogsy.disable(); -}); - -afterAll(() => { - globalLogsy.enable(); -}); - beforeEach(async () => { await orchestrator.unsubscribe(); }); diff --git a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.match.spec.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.match.spec.ts index 4d53fb11..36674dc0 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.match.spec.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.match.spec.ts @@ -1,21 +1,13 @@ -import { computeCheckedState, globalLogsy } from 'movex-core-util'; +import { computeCheckedState } from 'movex-core-util'; import { initialMatchState, tillNextTick, matchReducer, } from 'movex-specs-util'; -import { movexClientMasterOrchestrator } from 'movex-master'; +import { movexClientMasterOrchestrator } from './orchestrator'; const orchestrator = movexClientMasterOrchestrator(); -beforeAll(() => { - globalLogsy.disable(); -}); - -afterAll(() => { - globalLogsy.enable(); -}); - beforeEach(async () => { await orchestrator.unsubscribe(); }); @@ -63,8 +55,8 @@ test('works with public actions', async () => { }, }), subscribers: { - 'white-client': null, - 'black-client': null, + 'white-client': {}, + 'black-client': {}, }, }; diff --git a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.rps.spec.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.rps.spec.ts index 60b885ea..03d81568 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.rps.spec.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.rps.spec.ts @@ -1,19 +1,9 @@ -import { computeCheckedState, globalLogsy } from 'movex-core-util'; +import { computeCheckedState } from 'movex-core-util'; import { rpsReducer, rpsInitialState, tillNextTick } from 'movex-specs-util'; -import { movexClientMasterOrchestrator } from 'movex-master'; -import { install } from 'console-group'; -install(); +import { movexClientMasterOrchestrator } from './orchestrator'; const orchestrator = movexClientMasterOrchestrator(); -beforeAll(() => { - globalLogsy.disable(); -}); - -afterAll(() => { - globalLogsy.enable(); -}); - beforeEach(async () => { await orchestrator.unsubscribe(); }); @@ -99,8 +89,8 @@ test('2 Clients. Both Submitting (White first) WITH Reconciliation and the recon }, }), subscribers: { - 'client-a': null, - 'client-b': null, + 'client-a': {}, + 'client-b': {}, }, }; @@ -162,9 +152,7 @@ test('2 Clients. Both Submitting (White first) WITH Reconciliation and the recon expect(aMovex.state).toEqual(bMovex.state); const masterPublicState = await master.getPublicState(rid).resolveUnwrap(); - expect(masterPublicState).toEqual( - actualAfterPrivateRevelatoryAction - ); + expect(masterPublicState).toEqual(actualAfterPrivateRevelatoryAction); }); test('Same Kind Reconciliatory Actions Bug. See https://github.com/movesthatmatter/movex/issues/8', async () => { diff --git a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts index 2d3ab8db..22c16a59 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts @@ -1,8 +1,4 @@ -import { - computeCheckedState, - globalLogsy, - toResourceIdentifierObj, -} from 'movex-core-util'; +import { computeCheckedState, toResourceIdentifierObj } from 'movex-core-util'; import { gameReducer, gameReducerWithDerivedState, @@ -10,17 +6,7 @@ import { initialRawGameStateWithDerivedState, tillNextTick, } from 'movex-specs-util'; -import { movexClientMasterOrchestrator } from 'movex-master'; - -require('console-group').install(); - -beforeAll(() => { - globalLogsy.disable(); -}); - -afterAll(() => { - globalLogsy.enable(); -}); +import { movexClientMasterOrchestrator } from './orchestrator'; const orchestrator = movexClientMasterOrchestrator(); @@ -104,11 +90,14 @@ describe('Public Actions', () => { count: 5, }), subscribers: { - 'white-client': null, - 'black-client': null, + 'white-client': {}, + 'black-client': {}, }, }; + // console.log('whiteMovex', whiteMovex.state); + // console.log('blackMovex', blackMovex.state); + expect(whiteMovex.state).toEqual(expected); // The black would only be the same as white if the master works @@ -169,8 +158,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': null, - 'black-client': null, + 'white-client': {}, + 'black-client': {}, }, }; @@ -198,8 +187,8 @@ describe('Private Actions', () => { const expectedPeerState = { checkedState: publicState, subscribers: { - 'white-client': null, - 'black-client': null, + 'white-client': {}, + 'black-client': {}, }, }; const actualPeerState = blackMovex.state; @@ -266,8 +255,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': null, - 'black-client': null, + 'white-client': {}, + 'black-client': {}, }, }; @@ -312,8 +301,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': null, - 'black-client': null, + 'white-client': {}, + 'black-client': {}, }, }; @@ -337,8 +326,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': null, - 'black-client': null, + 'white-client': {}, + 'black-client': {}, }, }; // The Private Action gets set @@ -400,8 +389,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': null, - 'black-client': null, + 'white-client': {}, + 'black-client': {}, }, }; @@ -443,8 +432,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': null, - 'black-client': null, + 'white-client': {}, + 'black-client': {}, }, }; diff --git a/libs/movex-master/src/specs/ClientMasterOrchestration/console-group.d.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/console-group.d.ts deleted file mode 100644 index 0c364cc4..00000000 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/console-group.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module "console-group"; \ No newline at end of file diff --git a/libs/movex-master/src/lib/orchestrator.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/orchestrator.ts similarity index 58% rename from libs/movex-master/src/lib/orchestrator.ts rename to libs/movex-master/src/specs/ClientMasterOrchestration/orchestrator.ts index 3ef989d5..6fcc63ef 100644 --- a/libs/movex-master/src/lib/orchestrator.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/orchestrator.ts @@ -2,25 +2,32 @@ import { MovexClient, ResourceIdentifier, invoke, - globalLogsy, AnyAction, MovexReducer, ConnectionToMaster, - BaseMovexDefinitionResourcesMap, - MovexDefinition, ConnectionToClient, + MovexClientInfo, } from 'movex-core-util'; -import { Movex, MovexFromDefintion } from 'movex'; +import { Movex } from 'movex'; import { MovexMasterResource, MovexMasterServer } from 'movex-master'; import { MemoryMovexStore } from 'movex-store'; -import { MockConnectionEmitter } from './MockConnectionEmitter'; +import { MockConnectionEmitter } from '../../lib/MockConnectionEmitter'; -const logsy = globalLogsy.withNamespace('[MovexClientMasterOrchestrator]'); +// TODO: This was added on April 16th 2024, when I added the subscribers info (client info) -export const movexClientMasterOrchestrator = () => { +export const movexClientMasterOrchestrator = < + TClientInfo extends MovexClientInfo +>( + clientInfo: TClientInfo = {} as TClientInfo +) => { let unsubscribe = async () => {}; - const orchestrate = ({ + const orchestrate = < + S, + A extends AnyAction, + TResourceType extends string, + TClientInfo extends MovexClientInfo + >({ clientIds, reducer, resourceType, @@ -46,14 +53,19 @@ export const movexClientMasterOrchestrator = () => { const masterConnectionToClient = new ConnectionToClient< S, A, - TResourceType + TResourceType, + TClientInfo >(clientId, emitterOnMaster); const removeClientConnectionFromMaster = masterServer.addClientConnection( masterConnectionToClient ); - const mockedMovex = orchestrateMovex(clientId, emitterOnMaster); + const mockedMovex = orchestrateMovex( + clientId, + emitterOnMaster, + clientInfo + ); // TODO: This could be done better, but since the unsibscriber is async need to work iwth an sync iterator // for now this should do @@ -86,16 +98,15 @@ export const movexClientMasterOrchestrator = () => { }; }; -export type MovexClientMasterOrchestrator = - typeof movexClientMasterOrchestrator; - -export const orchestrateMovex = < +const orchestrateMovex = < TState extends any, - TAction extends AnyAction = AnyAction, - TResourceType extends string = string + TAction extends AnyAction, + TResourceType extends string, + TClientInfo extends MovexClientInfo >( clientId: MovexClient['id'], - emitterOnMaster: MockConnectionEmitter + emitterOnMaster: MockConnectionEmitter, + clientInfo: TClientInfo ) => { const emitterOnClient = new MockConnectionEmitter< TState, @@ -115,44 +126,12 @@ export const orchestrateMovex = < ]; return { - movex: new Movex(new ConnectionToMaster(clientId, emitterOnClient as any)), - emitter: emitterOnClient, - destroy: () => { - unsubscribers.forEach(invoke); - }, - }; -}; - -// TODO: this could get another name and be moved into util since it's used outside in initLocalMasterMovex -export const orchestrateDefinedMovex = < - TResourceMap extends BaseMovexDefinitionResourcesMap ->( - movexDefinition: MovexDefinition, - clientId: MovexClient['id'], - emitterOnMaster: MockConnectionEmitter -) => { - const emitterOnClient = new MockConnectionEmitter( - clientId, - clientId + '-emitter' - ); - - const unsubscribers = [ - emitterOnClient._onEmitted((r, ackCb) => { - logsy.debug('EmitterOnClient _onEmitted', r); - // Calling the master with the given event from the client in order to process it - emitterOnMaster._publish(r.event, r.payload, ackCb); - }), - emitterOnMaster._onEmitted((r, ackCb) => { - logsy.debug('EmitterOnMaster _onEmitted', r); - // Calling the client with the given event from the master in order to process it - emitterOnClient._publish(r.event, r.payload, ackCb); - }), - ]; - - return { - movex: new MovexFromDefintion( - movexDefinition, - new ConnectionToMaster(clientId, emitterOnClient) + movex: new Movex( + new ConnectionToMaster( + clientId, + emitterOnClient as any, // TODO: Fix this type cast + clientInfo + ) ), emitter: emitterOnClient, destroy: () => { diff --git a/libs/movex-master/src/specs/Movex.spec.ts b/libs/movex-master/src/specs/Movex.spec.ts index d8d3f652..e0c04978 100644 --- a/libs/movex-master/src/specs/Movex.spec.ts +++ b/libs/movex-master/src/specs/Movex.spec.ts @@ -1,9 +1,4 @@ -import { - computeCheckedState, - globalLogsy, - toResourceIdentifierObj, -} from 'movex-core-util'; -import * as consoleGroup from 'console-group'; +import { computeCheckedState, toResourceIdentifierObj } from 'movex-core-util'; import { counterReducer, gameReducer, @@ -12,22 +7,12 @@ import { } from 'movex-specs-util'; import { movexClientMasterOrchestrator } from 'movex-master'; -consoleGroup.install(); - const orchestrator = movexClientMasterOrchestrator(); beforeEach(async () => { await orchestrator.unsubscribe(); }); -beforeAll(() => { - globalLogsy.disable(); -}); - -afterAll(() => { - globalLogsy.enable(); -}); - describe('All', () => { test('Create', async () => { const { @@ -38,11 +23,7 @@ describe('All', () => { resourceType: 'counter', }); - const actual = await counterResource - .create({ - count: 2, - }) - .resolveUnwrap(); + const actual = await counterResource.create({ count: 2 }).resolveUnwrap(); expect(actual).toEqual({ rid: toResourceIdentifierObj(actual.rid), // The id isn't too important here @@ -73,7 +54,7 @@ describe('All', () => { const expected = { checkedState: computeCheckedState({ count: 2 }), - subscribers: { test: null }, + subscribers: { test: {} }, }; expect(actual.state).toEqual(expected); @@ -99,7 +80,7 @@ describe('All', () => { const actual = r.get(); const expected = { checkedState: computeCheckedState({ count: 3 }), - subscribers: { test: null }, + subscribers: { test: {} }, }; expect(actual).toEqual(expected); @@ -147,7 +128,7 @@ describe('All', () => { }, }), subscribers: { - 'test-user': null, + 'test-user': {}, }, }; @@ -155,4 +136,6 @@ describe('All', () => { }); }); +// TODO: Add tests for Subscribers Client Info + // TODO: Add more tests diff --git a/libs/movex-master/src/specs/Todo.md b/libs/movex-master/src/specs/Todo.md deleted file mode 100644 index f6a03214..00000000 --- a/libs/movex-master/src/specs/Todo.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -noteId: "1c13b2a0b51f11edadd4d72b1615afa0" -tags: [] - ---- - -## Doing - -## To Do - -Examples/Show Case to add to the docs. Both as Demo running with the server and also running only on the client. - -BUG: There is an interesting bug, where if a client disptached a private action once, and then does it again with a diff value the peers states get into a mismatch. - I think it's because the action doesnt get stored in the patch anymore on the master? Need to look into it - - this is actually because when there are multiple actions of the same kind, what happens sometimes happens is this: - - // if one action rewrite a state field that - // doesnt let an subsequent action to rewrite it. so it just returns prev on subsquequent - -Bug/Nice to have: [masterLocal, probably SocketAsWell] If two clients bind to same resource simultanoeusly, or super close to eahch other, their both take the available first slot in rps locally – but I imagine only one does it for real. Anyhow, it should be able to wait – as the store has a locking system, and only do it once ready. -// But this might be an issue with the way getSlots works on the client in RPs. Need a better system I guess - - I think this is not a bug, but an actual need for a bit of a feature - to be able to swork with server only generated values or state (such as ids, random stuff, etcc), end thus be able to get those, but in that case the dispatch need NOT TO optimistically update local state, but wait for the master to sync! I believe this should work, In the future we culd have some special Movex Value creators such as Movex.createRandom() or createId or etc..which will turn off the optimistic local update by default - - -## Backlog - -## Done - -Jun 7th - -Deployed Movex-docs. Now I'm thinking about the examples/showcase to add to the docs. - -Sometime in Late May - -Wite the SocketIO into MovexIO so it can be used. The archietcture looks like this: - -Ideal Version: -An application (such as a game) uses Movexon the client (React for eg) to deal with it multiplayer state - -The application code gets pushed from github to Matter.io or matter cloud, where the movex-backend runs on the server making the connection to movex(-client) - -Manual Version: - -The code gets pushed manually to matter.io, or run manually by importing matterio-backend and connecting it to movex-server - -So next Steps: - -On Matterio I load movex. I instantiate MovexMaster with a SocketIO Emitter. I also need to instantiate MovexClient with a SocketIO Emitter, so that means the SocketIO lives here no? - -Add an app sample to e2e the movex-backend (socket-io server Emitter) with the movex-client (socket-io client Emitter) with the whole pipeline working (locally) - -Sometime in May - -- Done/ Add reconciliation handler or MovexMaster - -- Make the types work between resources and create movex instance on client. - - Maybe redefine the Resource Reducer Input, The Resource Handler - -May 3rd - -Managed to mock Movex and MockEmitter in a way it was origianlly intended – to be jsut a transport layer not to touch business logic. And now I am at the point -where I can bing in the MasterServer to take the load off the mocker (so the logic leaves inside the real codebase not a test). - -April 6th - -I managed to start the Orchestration process, and where I left off is the Mock Emitter connecting the master with the clients. Need some hooking there but otherwise -I'm there. It works with one client. Now is time for multiple. - -After this, the orchestration should be much simpler, and the testing of the fwd and xreconciliation actions much more straightforward as well. - -TODO After this works: -- Test peer state gets updated correctly -- Test the reconciliatory actions after the private state gets revealed -- Implement the Emitter for Socket.IO - --- - -Today (March 29) I worked on adding the MovexMaster as the Master Orchestrator, and changed the old Movexmaster into MovexMasterResource. -I also added MovexClientResource which used to be the old MovexResource. - -This change is needed mainly to be able to write all the code inside Movex, even the one that lives only on master or the io portion. A code that lives in the codebase is a code that can be tested. All others are possibilities and opportunities for errors. - -I've introduced the concept of MovexIO (WIP), to abstract away the socket/local/test etc protocol that makes the client server communication possible. So then again it's easy to setup in a testing environment. - -Outcomes after WIP is done: -- createMasterEnv is removed or very very thin - no business logic -- the socket layer is abstracted by movexIO -- able to orchestrate the whole master/client connection and test each client getting the correct resource state - -Next To Do - -- rewrite Movex, the entry file. All the previous resource CRUD methods can be removed and only the simple (action/reducer) API alowed - - this also means I can rewrite it to not be socket/io connection agnostic, and thus to be able to provide a tester. - - alos, with this occasion, all the master resource to client resource hooking can happen in here inside the registerResource! (onDispatched -> emitAction, onFwdAction, onReconciliatoryActions), and this file will be the tested one! - ----- - -- Now/ work on MovexMaster + masterEnv - - I feel the need to have a MovexClient, split form the Movex (Client) SDK, in order to design and test the master/client orchestration but without the need to fire up the whole system. Hmm :/ - - Here it would be where the onFwdAction, onReconciliatoryAction, reconciliation and resynchornization logic would live - - This should be split from the MovexClientSDK so it can be easily tested - - Also, the Movex (on Client) could be allowed to load the Master in it to run all locally if so needed. Some usecases could be for not wanting to have another state management for the single player mode of a multiplayer game. Another when there is no server, but just a Peer to Peer arch, where one of the peers becomes the master. - -- Next to do: Movex becomes MovexClient and MovexClient becomes the client portion that reconciles and resynchronizes the state from master! So it can be part of the logic ---- - - - What shoould the master return for the reconciliation portion? - - The series of FWD and Ack for each client no? no cause I don't have the clients there - - and the nthe outside will map them and forward them= - -- Done/ work on $canReconcileState part \ No newline at end of file diff --git a/libs/movex-react-local-master/src/lib/ClientMasterOrchestrator.ts b/libs/movex-react-local-master/src/lib/ClientMasterOrchestrator.ts new file mode 100644 index 00000000..9076d74d --- /dev/null +++ b/libs/movex-react-local-master/src/lib/ClientMasterOrchestrator.ts @@ -0,0 +1,49 @@ +import { MovexFromDefintion } from 'movex'; +import { + BaseMovexDefinitionResourcesMap, + ConnectionToMaster, + globalLogsy as logsy, + invoke, + MovexClient, + MovexDefinition, +} from 'movex-core-util'; +import { MockConnectionEmitter } from 'movex-master'; + +export const orchestrateDefinedMovex = < + TResourceMap extends BaseMovexDefinitionResourcesMap +>( + movexDefinition: MovexDefinition, + clientId: MovexClient['id'], + emitterOnMaster: MockConnectionEmitter +) => { + const emitterOnClient = new MockConnectionEmitter( + clientId, + clientId + '-emitter' + ); + + const unsubscribers = [ + emitterOnClient._onEmitted((r, ackCb) => { + logsy.debug('EmitterOnClient _onEmitted', r); + // Calling the master with the given event from the client in order to process it + emitterOnMaster._publish(r.event, r.payload, ackCb); + }), + emitterOnMaster._onEmitted((r, ackCb) => { + logsy.debug('EmitterOnMaster _onEmitted', r); + // Calling the client with the given event from the master in order to process it + emitterOnClient._publish(r.event, r.payload, ackCb); + }), + ]; + + return { + movex: new MovexFromDefintion( + movexDefinition, + new ConnectionToMaster(clientId, emitterOnClient, { + test: 'orchestrator', // TODO: Take this one out + }) + ), + emitter: emitterOnClient, + destroy: () => { + unsubscribers.forEach(invoke); + }, + }; +}; diff --git a/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx b/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx index 7cefed12..b462c5b9 100644 --- a/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx +++ b/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx @@ -11,7 +11,6 @@ import { import { MovexMasterServer, MockConnectionEmitter, - orchestrateDefinedMovex, getUuid, // This can actually be mocked here as it's just client only! } from 'movex-master'; import { MovexClient } from 'movex'; @@ -19,8 +18,11 @@ import { MovexReactContext, MovexReactContextProps, MovexResourceObservablesRegistry, + MovexReactContextPropsConnected, } from 'movex-react'; import { MovexLocalContextConsumerProvider } from './MovexLocalContextConsumer'; +import { orchestrateDefinedMovex } from './ClientMasterOrchestrator'; +// import { MovexContextPropsConnected } from 'movex-react'; type Props = React.PropsWithChildren<{ @@ -54,6 +56,7 @@ export class MovexLocalProvider< contextState: { connected: false, clientId: undefined, + clientInfo: undefined, }, }; } @@ -89,10 +92,13 @@ export class MovexLocalProvider< mockedMovex.movex ); - const nextState = { + const client = mockedMovex.movex.getClient(); + + const nextState: MovexReactContextPropsConnected = { connected: true, movex: mockedMovex.movex, - clientId: mockedMovex.movex.getClientId(), + clientId: client.id, + clientInfo: client.info, movexDefinition: this.props.movexDefinition, bindResource: >( rid: ResourceIdentifier, diff --git a/libs/movex-react/src/index.ts b/libs/movex-react/src/index.ts index dca97359..b0017d81 100644 --- a/libs/movex-react/src/index.ts +++ b/libs/movex-react/src/index.ts @@ -5,6 +5,8 @@ export { MovexProviderClass as MovexProvider } from './lib/MovexProviderClass'; export { MovexContext as MovexReactContext, type MovexContextProps as MovexReactContextProps, + type MovexContextPropsConnected as MovexReactContextPropsConnected, + type MovexContextPropsNotConnected as MovexReactContextPropsNotConnected, } from './lib/MovexContext'; export { MovexBoundResourceComponent as MovexBoundResource } from './lib/MovexBoundResourceComponent'; diff --git a/libs/movex-react/src/lib/MovexContext.ts b/libs/movex-react/src/lib/MovexContext.ts index bcb2affe..eb771804 100644 --- a/libs/movex-react/src/lib/MovexContext.ts +++ b/libs/movex-react/src/lib/MovexContext.ts @@ -1,6 +1,7 @@ import { createContext } from 'react'; import type { BaseMovexDefinitionResourcesMap, + MovexClientInfo, MovexDefinition, ResourceIdentifier, StringKeys, @@ -10,24 +11,30 @@ import type { MovexClient, Movex } from 'movex'; export type MovexContextProps< TResourcesMap extends BaseMovexDefinitionResourcesMap -> = - | { - connected: false; - clientId: undefined; - movex?: Movex; - movexConfig?: undefined; - bindResource?: () => void; - } - | { - connected: true; - clientId: string; - movex: MovexClient.MovexFromDefintion; - movexDefinition: MovexDefinition; - bindResource: >( - rid: ResourceIdentifier, - onStateUpdate: (p: MovexClient.MovexBoundResource) => void - ) => UnsubscribeFn; - }; +> = MovexContextPropsNotConnected | MovexContextPropsConnected; + +export type MovexContextPropsNotConnected = { + connected: false; + clientId: undefined; + clientInfo: undefined; + movex?: Movex; + movexConfig?: undefined; + bindResource?: () => void; +}; + +export type MovexContextPropsConnected< + TResourcesMap extends BaseMovexDefinitionResourcesMap +> = { + connected: true; + clientId: string; + clientInfo: MovexClientInfo; + movex: MovexClient.MovexFromDefintion; + movexDefinition: MovexDefinition; + bindResource: >( + rid: ResourceIdentifier, + onStateUpdate: (p: MovexClient.MovexBoundResource) => void + ) => UnsubscribeFn; +}; export const MovexContext = createContext< MovexContextProps @@ -35,4 +42,5 @@ export const MovexContext = createContext< movex: undefined, connected: false, clientId: undefined, + clientInfo: undefined, }); diff --git a/libs/movex-react/src/lib/MovexProvider.tsx b/libs/movex-react/src/lib/MovexProvider.tsx index ca9eaaec..d7f5d25c 100644 --- a/libs/movex-react/src/lib/MovexProvider.tsx +++ b/libs/movex-react/src/lib/MovexProvider.tsx @@ -1,11 +1,15 @@ import React, { useEffect, useRef, useState } from 'react'; -import { MovexContextProps, MovexContext } from './MovexContext'; import { - invoke, - noop, - type MovexClient as MovexClientUser, + MovexContextProps, + MovexContext, + MovexContextPropsConnected, +} from './MovexContext'; +import { type BaseMovexDefinitionResourcesMap, type MovexDefinition, + type SanitizedMovexClient, + invoke, + noop, ResourceIdentifier, StringKeys, LoggingEvent, @@ -17,24 +21,58 @@ import { ResourceObservablesRegistry } from './ResourceObservableRegistry'; export type MovexProviderProps< TMovexConfigResourcesMap extends BaseMovexDefinitionResourcesMap > = React.PropsWithChildren<{ + /** + * The definition or movex.config file + */ movexDefinition: MovexDefinition; + + /** + * The Movex Master Instance URL + */ endpointUrl: string; - clientId?: MovexClientUser['id']; + + /** + * An optional Id for the client. Most often the userId in your own application userbase + */ + clientId?: SanitizedMovexClient['id']; + + /** + * This will be shared among all the peers in Movex, so make sure it's sanitized! + */ + clientInfo?: SanitizedMovexClient['info']; + + /** + * Optional Event Logger + */ + logger?: { + onLog: (event: LoggingEvent) => void; + }; + + /** + * This will trigger when the connection is made + * + * @param state + * @returns + */ onConnected?: ( state: Extract< MovexContextProps, { connected: true } > ) => void; + + /** + * This will trigger when movex gets disconnected + * + * @param state + * @returns + */ onDisconnected?: ( state: Extract< MovexContextProps, { connected: false } > ) => void; - logger?: { - onLog: (event: LoggingEvent) => void; - }; }>; export const MovexProvider: React.FC< @@ -47,9 +85,10 @@ export const MovexProvider: React.FC< >({ connected: false, clientId: undefined, + clientInfo: undefined, }); - // TODO: This can all ne moved into the MovexProviderClass and rename it to MovexProviderImplementation + // TODO: This can all be moved into the MovexProviderClass and rename it to MovexProviderImplementation // or this to MovexProviderContainer useEffect(() => { if (contextState.connected) { @@ -60,20 +99,22 @@ export const MovexProvider: React.FC< const movex = await MovexClient.initMovex( { clientId: props.clientId, + clientInfo: props.clientInfo, url: props.endpointUrl, apiKey: '', }, props.movexDefinition ); - const clientId = movex.getClientId(); + const client = movex.getClient(); // This resets each time movex re-initiates const resourceRegistry = new ResourceObservablesRegistry(movex); - const nextState = { + const nextState: MovexContextPropsConnected = { connected: true, - clientId, // TODO: Do I really need this? + clientId: client.id, + clientInfo: client.info, movex, movexDefinition: props.movexDefinition, bindResource: >( diff --git a/libs/movex-react/src/lib/hooks.ts b/libs/movex-react/src/lib/hooks.ts index c61ea87e..a0c6a781 100644 --- a/libs/movex-react/src/lib/hooks.ts +++ b/libs/movex-react/src/lib/hooks.ts @@ -6,6 +6,7 @@ import type { GetReducerState, BaseMovexDefinitionResourcesMap, MovexDefinition, + SanitizedMovexClient, } from 'movex-core-util'; import { isResourceIdentifier, @@ -15,7 +16,6 @@ import { } from 'movex-core-util'; import { MovexClient } from 'movex'; import { MovexContext, MovexContextProps } from './MovexContext'; -import { MovexContextStateChange } from './MovexContextStateChange'; export const useMovex = ( movexConfig: MovexDefinition @@ -30,6 +30,23 @@ export const useMovexClientId = < movexConfig: MovexDefinition ) => useMovex(movexConfig).clientId; +export const useMovexClient = < + TResourcesMap extends BaseMovexDefinitionResourcesMap +>( + movexConfig: MovexDefinition +): SanitizedMovexClient | undefined => { + const mc = useMovex(movexConfig); + + if (!mc.connected) { + return undefined; + } + + return { + id: mc.clientId, + info: mc.clientInfo, + }; +}; + export type MovexResourceFromConfig< TResourcesMap extends BaseMovexDefinitionResourcesMap, TResourceType extends keyof TResourcesMap, diff --git a/libs/movex-server/src/lib/movex-server.ts b/libs/movex-server/src/lib/movex-server.ts index b14bc6df..e92c4f2f 100644 --- a/libs/movex-server/src/lib/movex-server.ts +++ b/libs/movex-server/src/lib/movex-server.ts @@ -11,6 +11,7 @@ import { isResourceIdentifier, objectKeys, toResourceIdentifierObj, + MovexClientInfo, } from 'movex-core-util'; import { MemoryMovexStore, MovexStore } from 'movex-store'; import { initMovexMaster } from 'movex-master'; @@ -55,14 +56,19 @@ export const movexServer = ( socket.on('connection', (io) => { const clientId = getClientId(io.handshake.query['clientId'] as string); - logsy.info('Client Connected', { clientId }); + const clientInfo = JSON.parse( + (io.handshake.query['clientInfo'] as string) || '' + ) as MovexClientInfo; + + logsy.info('Client Connected', { clientId, clientInfo }); const connectionToClient = new ConnectionToClient( clientId, - new SocketIOEmitter(io) + new SocketIOEmitter(io), + clientInfo ); - connectionToClient.emitClientId(); + connectionToClient.emitClientReady(); movexMaster.addClientConnection(connectionToClient); @@ -84,6 +90,11 @@ export const movexServer = ( }); }); + app.get('/connections', async (_, res) => { + res.header('Content-Type', 'application/json'); + res.send(JSON.stringify(movexMaster.allClients(), null, 4)); + }); + // Resources // app.get('/api/resources', async (_, res) => { // res.header('Content-Type', 'application/json'); diff --git a/libs/movex-store/src/lib/MovexStore.ts b/libs/movex-store/src/lib/MovexStore.ts index 245caa4f..ef840e7e 100644 --- a/libs/movex-store/src/lib/MovexStore.ts +++ b/libs/movex-store/src/lib/MovexStore.ts @@ -30,12 +30,7 @@ export type MovexStoreItem< patches?: { [patchGroupKey in string]: MovexStatePatch[]; }; - subscribers: Record< - MovexClient['id'], - { - subscribedAt: number; - } - >; + subscribers: Record; }; export type MovexStoreItemsMapByType< diff --git a/libs/movex/src/lib/ConnectionToMasterResource.ts b/libs/movex/src/lib/ConnectionToMasterResources.ts similarity index 95% rename from libs/movex/src/lib/ConnectionToMasterResource.ts rename to libs/movex/src/lib/ConnectionToMasterResources.ts index dbae0148..47105c02 100644 --- a/libs/movex/src/lib/ConnectionToMasterResource.ts +++ b/libs/movex/src/lib/ConnectionToMasterResources.ts @@ -11,6 +11,7 @@ import type { IOEvents, ConnectionToMaster, MovexClient, + SanitizedMovexClient, } from 'movex-core-util'; import { invoke, @@ -24,7 +25,7 @@ import { Err, Ok } from 'ts-results'; /** * This handles the connection with Master per ResourceType */ -export class ConnectionToMasterResource< +export class ConnectionToMasterResources< TState, TAction extends AnyAction, TResourceType extends string @@ -38,7 +39,10 @@ export class ConnectionToMasterResource< }>(); private subscriberAddedEventPubsy = new Pubsy<{ - [key in `rid:${ResourceIdentifierStr}`]: MovexClient['id']; + [key in `rid:${ResourceIdentifierStr}`]: Pick< + MovexClient, + 'id' | 'info' + >; }>(); private subscriberRemovedEventPubsy = new Pubsy<{ @@ -52,7 +56,8 @@ export class ConnectionToMasterResource< private connectionToMaster: ConnectionToMaster< TState, TAction, - TResourceType + TResourceType, + {} // Fix > ) { const onFwdActionHandler = ( @@ -100,7 +105,7 @@ export class ConnectionToMasterResource< }; const onAddResourceSubscriberHandler = (p: { rid: ResourceIdentifier; - clientId: MovexClient['id']; + client: SanitizedMovexClient; }) => { if (toResourceIdentifierObj(p.rid).resourceType !== resourceType) { return; @@ -108,7 +113,7 @@ export class ConnectionToMasterResource< this.subscriberAddedEventPubsy.publish( `rid:${toResourceIdentifierStr(p.rid)}`, - p.clientId + { id: p.client.id, info: p.client.info } ); }; @@ -184,6 +189,7 @@ export class ConnectionToMasterResource< this.connectionToMaster.emitter .emitAndAcknowledge('addResourceSubscriber', { rid, + clientInfo: this.connectionToMaster.clientInfo, }) .then((res) => (res.ok ? new Ok(res.val) : new Err(res.val))) ); @@ -277,7 +283,7 @@ export class ConnectionToMasterResource< onSubscriberAdded( rid: ResourceIdentifier, - fn: (clientId: MovexClient['id']) => void + fn: (clientId: SanitizedMovexClient) => void ) { return this.subscriberAddedEventPubsy.subscribe( `rid:${toResourceIdentifierStr(rid)}`, diff --git a/libs/movex/src/lib/Movex.ts b/libs/movex/src/lib/Movex.ts index 16b3c26f..5f5eb1f7 100644 --- a/libs/movex/src/lib/Movex.ts +++ b/libs/movex/src/lib/Movex.ts @@ -2,8 +2,8 @@ import type { AnyAction, ConnectionToMaster, MovexReducer, - IOConnection, -} from 'movex-core-util'; + SanitizedMovexClient, +} from 'movex-core-util'; import { MovexResource } from './MovexResource'; export type MovexConfig = { @@ -14,22 +14,33 @@ export type MovexConfig = { waitForResponseMs?: number; }; +// @deprecate in favor of MovexFromDEfinition to clear up redundant code export class Movex { - constructor(private connectionToMaster: IOConnection) {} + constructor( + private connectionToMaster: ConnectionToMaster + ) {} getClientId() { return this.connectionToMaster.clientId; } + getClient(): SanitizedMovexClient { + return { + id: this.getClientId(), + info: this.connectionToMaster.clientInfo, + }; + } + register( resourceType: TResourceType, reducer: MovexReducer ) { return new MovexResource( // This is recasted to make it specific to the ResourceType - this.connectionToMaster as unknown as ConnectionToMaster< + this.connectionToMaster as ConnectionToMaster< S, A, - TResourceType + TResourceType, + any // TODO: Fix this here or above? >, resourceType, reducer diff --git a/libs/movex/src/lib/MovexFromDefintion.ts b/libs/movex/src/lib/MovexFromDefintion.ts index d8b80fdc..f8fa1bf2 100644 --- a/libs/movex/src/lib/MovexFromDefintion.ts +++ b/libs/movex/src/lib/MovexFromDefintion.ts @@ -6,8 +6,8 @@ import type { GetReducerAction, GetReducerState, StringKeys, - IOConnection, -} from 'movex-core-util'; + ConnectionToMaster, +} from 'movex-core-util'; export class MovexFromDefintion< TMovexDefinitionResources extends BaseMovexDefinitionResourcesMap @@ -16,7 +16,7 @@ export class MovexFromDefintion< constructor( private movexDefinition: MovexDefinition, - connectionToMaster: IOConnection + connectionToMaster: ConnectionToMaster ) { this.movex = new Movex(connectionToMaster); } @@ -25,6 +25,10 @@ export class MovexFromDefintion< return this.movex.getClientId(); } + getClient() { + return this.movex.getClient(); + } + register>( resourceType: TResourceType ) { diff --git a/libs/movex/src/lib/MovexResource.ts b/libs/movex/src/lib/MovexResource.ts index d78f391b..c6be929f 100644 --- a/libs/movex/src/lib/MovexResource.ts +++ b/libs/movex/src/lib/MovexResource.ts @@ -5,7 +5,7 @@ import type { AnyAction, CheckedReconciliatoryActions, MovexReducer, - IOConnection, + ConnectionToMaster, } from 'movex-core-util'; import { globalLogsy, @@ -13,7 +13,7 @@ import { toResourceIdentifierStr, invoke, } from 'movex-core-util'; -import { ConnectionToMasterResource } from './ConnectionToMasterResource'; +import { ConnectionToMasterResources } from './ConnectionToMasterResources'; import { MovexResourceObservable } from './MovexResourceObservable'; import * as deepObject from 'deep-object-diff'; @@ -24,7 +24,7 @@ export class MovexResource< A extends AnyAction, TResourceType extends string > { - private connectionToMasterResource: ConnectionToMasterResource< + private connectionToMasterResources: ConnectionToMasterResources< S, A, TResourceType @@ -36,18 +36,18 @@ export class MovexResource< > = {}; constructor( - private connectionToMaster: IOConnection, + private connectionToMaster: ConnectionToMaster, private resourceType: TResourceType, private reducer: MovexReducer ) { - this.connectionToMasterResource = new ConnectionToMasterResource( + this.connectionToMasterResources = new ConnectionToMasterResources( resourceType, this.connectionToMaster ); } create(state: S, resourceId?: string) { - return this.connectionToMasterResource + return this.connectionToMasterResources .create(this.resourceType, state, resourceId) .map((item) => ({ ...item, @@ -57,7 +57,7 @@ export class MovexResource< } get(rid: ResourceIdentifier) { - return this.connectionToMasterResource.getResource(rid); + return this.connectionToMasterResources.getResource(rid); } /** @@ -66,7 +66,7 @@ export class MovexResource< * @param rid * @returns MovexResourceObservable */ - bind(rid: ResourceIdentifier) { + bind(rid: ResourceIdentifier): MovexResourceObservable { // TODO: // What if this is used multiple times for the sameclient? // It should actually store it in the instance so it can be reused rather than created again, I suggest! @@ -84,7 +84,7 @@ export class MovexResource< // resourceObservable.setMasterSyncing(false); const syncLocalState = () => { - return this.connectionToMasterResource + return this.connectionToMasterResources .getState(rid) .map((masterCheckState) => { resourceObservable.syncState(masterCheckState); @@ -113,7 +113,7 @@ export class MovexResource< masterCheckState, diff: deepObject.detailedDiff(prevCheckedState, masterCheckState), }); - console.warn( + logsy.debug( "This shouldn't happen too often! If it does, make sure there's no way around it! See this for more https://github.com/movesthatmatter/movex/issues/8" ); @@ -121,7 +121,7 @@ export class MovexResource< }); }; - this.connectionToMasterResource + this.connectionToMasterResources .addResourceSubscriber(rid) .map((res) => { // TODO: This could be optimized to be returned from the "addResourceSubscriber" directly @@ -178,7 +178,7 @@ export class MovexResource< ({ action, next: nextLocalCheckedState }) => { const [nextLocalState, nextLocalChecksum] = nextLocalCheckedState; - this.connectionToMasterResource + this.connectionToMasterResources .emitAction(rid, action) .map(async (master) => { if (master.reconciled) { @@ -217,7 +217,7 @@ export class MovexResource< }); } ), - this.connectionToMasterResource.onFwdAction(rid, (p) => { + this.connectionToMasterResources.onFwdAction(rid, (p) => { const prevState = resourceObservable.getCheckedState(); resourceObservable.reconciliateAction(p); @@ -231,28 +231,28 @@ export class MovexResource< nextState, }); }), - this.connectionToMasterResource.onReconciliatoryActions( + this.connectionToMasterResources.onReconciliatoryActions( rid, onReconciliateActionsHandler ), // Subscribers - this.connectionToMasterResource.onSubscriberAdded(rid, (clientId) => { - logsy.info('Subscriber Added', { clientId }); - + this.connectionToMasterResources.onSubscriberAdded(rid, (client) => { resourceObservable.updateSubscribers((prev) => ({ ...prev, - [clientId]: null, + [client.id]: client, })); - }), - this.connectionToMasterResource.onSubscriberRemoved(rid, (clientId) => { - logsy.info('Subscriber Removed', { clientId }); + logsy.info('Subscriber Added', { client }); + }), + this.connectionToMasterResources.onSubscriberRemoved(rid, (clientId) => { resourceObservable.updateSubscribers((prev) => { const { [clientId]: removed, ...rest } = prev; return rest; }); + + logsy.info('Subscriber Removed', { clientId }); }), // Destroyers @@ -261,7 +261,7 @@ export class MovexResource< () => resourceObservable.destroy(), // Add the master Resource Destroy as well - () => this.connectionToMasterResource.destroy(), + () => this.connectionToMasterResources.destroy(), // Logger resourceObservable.onDispatched( diff --git a/libs/movex/src/lib/MovexResourceObservable.ts b/libs/movex/src/lib/MovexResourceObservable.ts index 96a7aa28..1a445b31 100644 --- a/libs/movex/src/lib/MovexResourceObservable.ts +++ b/libs/movex/src/lib/MovexResourceObservable.ts @@ -6,6 +6,7 @@ import { computeCheckedState, isAction, Observable, + SanitizedMovexClient, } from 'movex-core-util'; import type { IObservable, @@ -27,7 +28,7 @@ import { PromiseDelegate } from 'promise-delegate'; const logsy = globalLogsy.withNamespace('[MovexResourceObservable]'); type ObservedItem = { - subscribers: Record; + subscribers: Record; checkedState: CheckedState; }; @@ -64,7 +65,7 @@ export class MovexResourceObservable< public rid: ResourceIdentifier, private reducer: MovexReducer, - initialSubscribers: Record = {}, + initialSubscribers: Record = {}, // Passing undefined here in order to get the default state initialCheckedState = computeCheckedState( @@ -199,7 +200,7 @@ export class MovexResourceObservable< */ onUpdate( fn: (p: { - subscribers: Record; + subscribers: Record; checkedState: CheckedState; }) => void ) { diff --git a/libs/movex/src/lib/init.ts b/libs/movex/src/lib/init.ts index 2fe1674c..bb0e55d6 100644 --- a/libs/movex/src/lib/init.ts +++ b/libs/movex/src/lib/init.ts @@ -1,5 +1,10 @@ import io from 'socket.io-client'; -import { ConnectionToMaster, SocketIOEmitter } from 'movex-core-util'; +import { + ConnectionToMaster, + MovexClientInfo, + SanitizedMovexClient, + SocketIOEmitter, +} from 'movex-core-util'; import type { IOEvents, BaseMovexDefinitionResourcesMap, @@ -7,21 +12,14 @@ import type { } from 'movex-core-util'; import { MovexFromDefintion } from './MovexFromDefintion'; -// onResourceStateUpdate( -// rid: AnyResourceIdentifier, -// fn: (nextState: any) => void -// ) { -// return this.pubsy.subscribe(toResourceIdentifierStr(rid), fn); -// } -// } - // TODO: The ClientId ideally isn't given from here bu retrieved somehow else. hmm // Or no? export const initMovex = ( config: { url: string; apiKey: string; - clientId?: string; + clientId?: SanitizedMovexClient['id']; + clientInfo?: MovexClientInfo; }, movexDefinition: MovexDefinition ) => { @@ -32,6 +30,7 @@ export const initMovex = ( // TODO: Here can check if the clientId already exists locally // and send it over in the handshake for the server to determine what to do with it // (i.e. if it's still valid and return it or create a new one) + const socket = io(config.url, { reconnectionDelay: 1000, reconnection: true, @@ -39,20 +38,22 @@ export const initMovex = ( agent: false, upgrade: false, rejectUnauthorized: false, - ...(config.clientId && { - query: { - clientId: config.clientId, - }, - }), + query: { + ...(config.clientId && { clientId: config.clientId }), + ...(config.clientInfo && { + clientInfo: JSON.stringify(config.clientInfo), + }), + }, }); const emitter = new SocketIOEmitter(socket); - emitter.on('setClientId', (clientId) => { + emitter.on('onClientReady', (client) => { // This might need to be moved from here into the master connection or somewhere client specific! + const movex = new MovexFromDefintion( movexDefinition, - new ConnectionToMaster(clientId, emitter) + new ConnectionToMaster(client.id, emitter, client.info) ); // TODO: Add on reject as well? From 92ad4cfe7ad96fe5c765b5d0703d25907ae32586 Mon Sep 17 00:00:00 2001 From: "Gabriel C. Troia" Date: Wed, 17 Apr 2024 13:38:30 -0600 Subject: [PATCH 25/25] ci(libs): :ferris_wheel: bump the version to 0.1.5-4 --- libs/movex-core-util/package.json | 2 +- libs/movex-master/package.json | 2 +- libs/movex-react-local-master/package.json | 2 +- libs/movex-react/package.json | 2 +- libs/movex-server/package.json | 2 +- libs/movex-service/package.json | 2 +- libs/movex-store/package.json | 2 +- libs/movex/package.json | 2 +- package.json | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/movex-core-util/package.json b/libs/movex-core-util/package.json index 738f7d27..7f04fbc9 100644 --- a/libs/movex-core-util/package.json +++ b/libs/movex-core-util/package.json @@ -1,6 +1,6 @@ { "name": "movex-core-util", - "version": "0.1.5-1", + "version": "0.1.5-4", "description": "Movex Core Util is the library of utilities for Movex", "license": "MIT", "author": { diff --git a/libs/movex-master/package.json b/libs/movex-master/package.json index 2f50b8ca..c1ded82a 100644 --- a/libs/movex-master/package.json +++ b/libs/movex-master/package.json @@ -1,6 +1,6 @@ { "name": "movex-master", - "version": "0.1.5-1", + "version": "0.1.5-4", "license": "MIT", "description": "Movex-master defines the master that wil be used on movex-server and movex-react-local-master", "author": { diff --git a/libs/movex-react-local-master/package.json b/libs/movex-react-local-master/package.json index a90c6404..ceaec6ec 100644 --- a/libs/movex-react-local-master/package.json +++ b/libs/movex-react-local-master/package.json @@ -1,6 +1,6 @@ { "name": "movex-react-local-master", - "version": "0.1.5-1", + "version": "0.1.5-4", "license": "MIT", "description": "Run the movex-master locally with react", "author": { diff --git a/libs/movex-react/package.json b/libs/movex-react/package.json index 23a360d2..9a80447f 100644 --- a/libs/movex-react/package.json +++ b/libs/movex-react/package.json @@ -1,6 +1,6 @@ { "name": "movex-react", - "version": "0.1.5-3", + "version": "0.1.5-4", "license": "MIT", "description": "Movex React is the library of React components for Movex", "author": { diff --git a/libs/movex-server/package.json b/libs/movex-server/package.json index 3b6a1736..755a8585 100644 --- a/libs/movex-server/package.json +++ b/libs/movex-server/package.json @@ -1,6 +1,6 @@ { "name": "movex-server", - "version": "0.1.5-1", + "version": "0.1.5-4", "license": "MIT", "type": "commonjs", "description": "Movex Server is the backend runtime for Movex", diff --git a/libs/movex-service/package.json b/libs/movex-service/package.json index 73a09518..51584f1f 100644 --- a/libs/movex-service/package.json +++ b/libs/movex-service/package.json @@ -1,6 +1,6 @@ { "name": "movex-service", - "version": "0.1.5-1", + "version": "0.1.5-4", "license": "MIT", "type": "commonjs", "description": "Movex Service is the CLI for Movex", diff --git a/libs/movex-store/package.json b/libs/movex-store/package.json index 4377810f..70659b7f 100644 --- a/libs/movex-store/package.json +++ b/libs/movex-store/package.json @@ -1,6 +1,6 @@ { "name": "movex-store", - "version": "0.1.5-1", + "version": "0.1.5-4", "license": "MIT", "description": "Movex-store defines the store interface and comes with a MemoryStore", "author": { diff --git a/libs/movex/package.json b/libs/movex/package.json index 88347046..ea7f43f3 100644 --- a/libs/movex/package.json +++ b/libs/movex/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.5-1", + "version": "0.1.5-4", "license": "MIT", "description": "Movex is a Multiplayer (Game) State Synchronization Library using Deterministic Action Propagation without the need to write Server Specific Code.", "author": { diff --git a/package.json b/package.json index e39a9628..a7b30f9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.5-1", + "version": "0.1.5-4", "license": "MIT", "author": { "name": "Gabriel C. Troia",