diff --git a/apps/movex-demo/pages/chat/[chatId].tsx b/apps/movex-demo/pages/chat/[chatId].tsx index 359f16ed..f1d08d86 100644 --- a/apps/movex-demo/pages/chat/[chatId].tsx +++ b/apps/movex-demo/pages/chat/[chatId].tsx @@ -1,4 +1,4 @@ -import { useMovexBoundResourceFromRid, useMovexClientId } from 'movex-react'; +import { useMovexBoundResourceFromRid, useMovexClient } from 'movex-react'; import { useRouter } from 'next/router'; import { useMemo } from 'react'; import { toResourceIdentifierObj } from 'movex-core-util'; @@ -20,7 +20,7 @@ const ChatSystem: React.FC = () => { // TODO: Validate the rid is correct inside useMovexBoundResouce const boundResource = useMovexBoundResourceFromRid(movexConfig, rid); - const userId = useMovexClientId(movexConfig); + const userId = useMovexClient(movexConfig)?.id; if (!(boundResource && userId)) { return null; diff --git a/libs/movex-core-util/package.json b/libs/movex-core-util/package.json index 298a7281..301bf117 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.6-23", + "version": "0.1.6-45", "description": "Movex Core Util is the library of utilities for Movex", "license": "MIT", "author": { diff --git a/libs/movex-core-util/src/lib/EventEmitter/IOEvents.ts b/libs/movex-core-util/src/lib/EventEmitter/IOEvents.ts index 252ba367..aa84b3f2 100644 --- a/libs/movex-core-util/src/lib/EventEmitter/IOEvents.ts +++ b/libs/movex-core-util/src/lib/EventEmitter/IOEvents.ts @@ -12,6 +12,7 @@ import type { MovexClient, ResourceIdentifier, SanitizedMovexClient, + MovexClientMasterClockOffset, } from '../core-types'; export type IOEvents< @@ -89,10 +90,9 @@ export type IOEvents< /** * The following events are directed from Master to Client * */ - // @deprecate in favor ofClientReady - setClientId: (clientId: string) => void; + onReady: (p: SanitizedMovexClient) => void; - onClientReady: (client: SanitizedMovexClient) => void; + onClockSync: (p: undefined) => IOPayloadResult; // acknowledges the client timestamp onFwdAction: ( payload: { @@ -106,7 +106,7 @@ export type IOEvents< ) => IOPayloadResult; onResourceSubscriberAdded: (p: { rid: ResourceIdentifier; - client: Pick; + client: SanitizedMovexClient; // clientId: MovexClient['id']; }) => IOPayloadResult< void, @@ -124,6 +124,7 @@ export type IOEvents< * The following events are by-directional (from Client to Master and vice-versa) * */ + // They need to be different than ping/pong because those are native to socket.io ping: () => IOPayloadResult; pong: () => IOPayloadResult; }; diff --git a/libs/movex-core-util/src/lib/EventEmitter/ScketIOEmitter.ts b/libs/movex-core-util/src/lib/EventEmitter/ScketIOEmitter.ts index 63e63ed0..f77e0394 100644 --- a/libs/movex-core-util/src/lib/EventEmitter/ScketIOEmitter.ts +++ b/libs/movex-core-util/src/lib/EventEmitter/ScketIOEmitter.ts @@ -15,13 +15,19 @@ export class SocketIOEmitter< TSocketIO extends SocketIO = ServerSocket | ClientSocket > implements EventEmitter { + protected config: { + waitForResponseMs: number; + }; + constructor( protected socket: TSocketIO, - protected config: { + config?: { waitForResponseMs?: number; - } = {} + } ) { - this.config.waitForResponseMs = this.config.waitForResponseMs || 15 * 1000; + this.config = { + waitForResponseMs: config?.waitForResponseMs || 15 * 1000, + }; } on( @@ -173,6 +179,14 @@ export class SocketIOEmitter< } } +/** + * TODO: Deprecate this in favor of using the native timeout See https://socket.io/docs/v4/emitting-events/#with-timeout + * + * @param onSuccess + * @param onTimeout + * @param timeout + * @returns + */ const withTimeout = ( onSuccess: (...args: any[]) => void, onTimeout: () => void, diff --git a/libs/movex-core-util/src/lib/Logsy.ts b/libs/movex-core-util/src/lib/Logsy.ts index 83a5a6f8..f68925c1 100644 --- a/libs/movex-core-util/src/lib/Logsy.ts +++ b/libs/movex-core-util/src/lib/Logsy.ts @@ -114,4 +114,3 @@ class Logsy { } export const globalLogsy = new Logsy(); - diff --git a/libs/movex-core-util/src/lib/core-types.ts b/libs/movex-core-util/src/lib/core-types.ts index 2e63b04a..289425c0 100644 --- a/libs/movex-core-util/src/lib/core-types.ts +++ b/libs/movex-core-util/src/lib/core-types.ts @@ -237,8 +237,17 @@ export type MovexClient = { >; }; +export type MovexClientMasterClockOffset = number; + export type SanitizedMovexClient = - Pick, 'id' | 'info'>; + Pick, 'id' | 'info'> & { + /** + * This is the diff between client and master needed to be adjusted on the client side + * + * TODO: Still not sure it should be available here - meaning all the peers can read it! + */ + clockOffset: MovexClientMasterClockOffset; + }; export type ResourceIdentifierObj = { resourceType: TResourceType; diff --git a/libs/movex-core-util/src/lib/reducer.ts b/libs/movex-core-util/src/lib/reducer.ts index 9ac6ba68..b44cf3ee 100644 --- a/libs/movex-core-util/src/lib/reducer.ts +++ b/libs/movex-core-util/src/lib/reducer.ts @@ -32,8 +32,10 @@ export type MovexReducerMap< // TAction extends AnyAction = AnyAction // > = (state: TState, action: TAction) => TState; -export type MovexRemoteContext = { +export type MovexMasterContext = { + // @Deprecate in favor of requestAt Props which enables purity now: () => number; // timestamp + requestAt: number; // timestamp }; export type MovexReducer = (( @@ -41,7 +43,7 @@ export type MovexReducer = (( action: A ) => S) & { $canReconcileState?: (s: S) => boolean; - $transformState?: (s: S, remoteContext: MovexRemoteContext) => S; + $transformState?: (s: S, masterContext: MovexMasterContext) => S; }; export type GetReducerState< diff --git a/libs/movex-master/package.json b/libs/movex-master/package.json index b037a0b2..4e3540f9 100644 --- a/libs/movex-master/package.json +++ b/libs/movex-master/package.json @@ -1,6 +1,6 @@ { "name": "movex-master", - "version": "0.1.6-23", + "version": "0.1.6-45", "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/lib/ConnectionToClient.ts b/libs/movex-master/src/lib/ConnectionToClient.ts index 54489522..9168a4b1 100644 --- a/libs/movex-master/src/lib/ConnectionToClient.ts +++ b/libs/movex-master/src/lib/ConnectionToClient.ts @@ -12,12 +12,54 @@ export class ConnectionToClient< TResourceType extends string, TClientInfo extends MovexClientInfo > { + // public latencyMs: number = 0; + + // public clientClockOffset: number = 0; + constructor( public emitter: EventEmitter>, public client: SanitizedMovexClient ) {} - emitClientReady() { - this.emitter.emit('onClientReady', this.client); + async setReady() { + await this.syncClocks(); + + this.emitter.emit('onReady', this.client); + } + + async syncClocks() { + const requestAt = new Date().getTime(); + + // console.log('Sync clock', this.client.id, { requestAt }); + + return this.emitter + .emitAndAcknowledge('onClockSync', undefined) + .then((res) => { + if (res.err) { + // console.log('Sync clock err', this.client.id); + console.error(res.err); + return; + } + + // TODO: This might not be correct - also not sure if this + // it is roughly based on the NTP protocol as described here https://stackoverflow.com/a/15785110/2093626 + // but adjusted for movex - the math might be wrong + // this.latencyMs = requestTime / 2; + + const responseAt = new Date().getTime(); + const requestTime = responseAt - requestAt; + const clientTimeAtRequest = res.val; + + this.client.clockOffset = + clientTimeAtRequest - new Date().getTime() - requestTime; + + // console.log('Sync clock ok', this.client.id, { + // requestAt, + // responseAt, + // requestTime, + // clientTimeAtRequest, + // clientClockOffset: this.client.clockOffset, + // }); + }); } } diff --git a/libs/movex-master/src/lib/MovexMasterResource.spec.ts b/libs/movex-master/src/lib/MovexMasterResource.spec.ts index 36856aaa..661f2579 100644 --- a/libs/movex-master/src/lib/MovexMasterResource.spec.ts +++ b/libs/movex-master/src/lib/MovexMasterResource.spec.ts @@ -11,6 +11,7 @@ import { } from 'movex-core-util'; import { MovexMasterResource } from './MovexMasterResource'; import { MemoryMovexStore } from 'movex-store'; +import { createMasterContext } from './util'; const rid = toResourceIdentifierStr({ resourceType: 'c', resourceId: '1' }); @@ -24,9 +25,13 @@ test('gets initial state', async () => { }) ); - const actualPublic = await master.getPublicState(rid).resolveUnwrap(); + const mockMasterContext = createMasterContext({ requestAt: 123 }); + + const actualPublic = await master + .getPublicState(rid, mockMasterContext) + .resolveUnwrap(); const actualByClient = await master - .getClientSpecificState(rid, 'testClient') + .getClientSpecificState(rid, 'testClient', mockMasterContext) .resolveUnwrap(); const expectedPublic = computeCheckedState(initialCounterState); @@ -45,6 +50,8 @@ test('applies public action', async () => { }) ); + const mockMasterContext = createMasterContext({ requestAt: 123 }); + const clientAId = 'clienA'; const action: GetReducerAction = { @@ -52,12 +59,14 @@ test('applies public action', async () => { }; const actual = await master - .applyAction(rid, clientAId, action) + .applyAction(rid, clientAId, action, mockMasterContext) .resolveUnwrap(); - const actualPublic = await master.getPublicState(rid).resolveUnwrap(); + const actualPublic = await master + .getPublicState(rid, mockMasterContext) + .resolveUnwrap(); const actualByClient = await master - .getClientSpecificState(rid, clientAId) + .getClientSpecificState(rid, clientAId, mockMasterContext) .resolveUnwrap(); const expectedPublic = computeCheckedState({ @@ -90,6 +99,8 @@ test('applies only one private action w/o getting to reconciliation', async () = }) ); + const mockMasterContext = createMasterContext({ requestAt: 123 }); + const senderClientId = 'senderClient'; const privateAction: GetReducerAction = { @@ -103,16 +114,23 @@ test('applies only one private action w/o getting to reconciliation', async () = }; const actual = await master - .applyAction(rid, senderClientId, [privateAction, publicAction]) + .applyAction( + rid, + senderClientId, + [privateAction, publicAction], + mockMasterContext + ) .resolveUnwrap(); - const actualPublicState = await master.getPublicState(rid).resolveUnwrap(); + const actualPublicState = await master + .getPublicState(rid, mockMasterContext) + .resolveUnwrap(); const actualSenderState = await master - .getClientSpecificState(rid, senderClientId) + .getClientSpecificState(rid, senderClientId, mockMasterContext) .resolveUnwrap(); const actualReceiverState = await master - .getClientSpecificState(rid, 'otherClient') + .getClientSpecificState(rid, 'otherClient', mockMasterContext) .resolveUnwrap(); const expectedPublic = computeCheckedState({ @@ -156,6 +174,8 @@ test('applies private action UNTIL Reconciliation', async () => { }) ); + const mockMasterContext = createMasterContext({ requestAt: 123 }); + const whitePlayer = 'white'; const blackPlayer = 'black'; @@ -177,19 +197,24 @@ test('applies private action UNTIL Reconciliation', async () => { // White Private Action const actualActionResultBeforeReconciliation = await master - .applyAction(rid, whitePlayer, [privateWhiteAction, publicWhiteAction]) + .applyAction( + rid, + whitePlayer, + [privateWhiteAction, publicWhiteAction], + mockMasterContext + ) .resolveUnwrap(); const actualPublicStateBeforeReconciliation = await master - .getPublicState(rid) + .getPublicState(rid, mockMasterContext) .resolveUnwrap(); const actualSenderStateBeforeReconciliation = await master - .getClientSpecificState(rid, whitePlayer) + .getClientSpecificState(rid, whitePlayer, mockMasterContext) .resolveUnwrap(); const actualReceiverStateBeforeReconciliation = await master - .getClientSpecificState(rid, blackPlayer) + .getClientSpecificState(rid, blackPlayer, mockMasterContext) .resolveUnwrap(); const expectedPublicStateBeforeReconciliation = computeCheckedState({ @@ -260,19 +285,24 @@ test('applies private action UNTIL Reconciliation', async () => { // Black Private Action (also the Reconciliatory Action) const actualActionResultAfterReconciliation = await master - .applyAction(rid, blackPlayer, [privateBlackAction, publicBlackAction]) + .applyAction( + rid, + blackPlayer, + [privateBlackAction, publicBlackAction], + mockMasterContext + ) .resolveUnwrap(); const actualPublicStateAfterReconciliation = await master - .getPublicState(rid) + .getPublicState(rid, mockMasterContext) .resolveUnwrap(); const actualSenderStateAfterReconciliation = await master - .getClientSpecificState(rid, blackPlayer) + .getClientSpecificState(rid, blackPlayer, mockMasterContext) .resolveUnwrap(); const actualReceiverStateAfterReconciliation = await master - .getClientSpecificState(rid, whitePlayer) + .getClientSpecificState(rid, whitePlayer, mockMasterContext) .resolveUnwrap(); const expectedPublicStateAfterReconciliation = computeCheckedState({ diff --git a/libs/movex-master/src/lib/MovexMasterResource.stateTransformers.spec.ts b/libs/movex-master/src/lib/MovexMasterResource.stateTransformers.spec.ts index 6e85001b..92615e43 100644 --- a/libs/movex-master/src/lib/MovexMasterResource.stateTransformers.spec.ts +++ b/libs/movex-master/src/lib/MovexMasterResource.stateTransformers.spec.ts @@ -5,12 +5,13 @@ import { } from 'movex-specs-util'; import { computeCheckedState, - MovexRemoteContext, + MovexMasterContext, toResourceIdentifierStr, } from 'movex-core-util'; import { MovexMasterResource } from './MovexMasterResource'; import { MemoryMovexStore } from 'movex-store'; import MockDate from 'mockdate'; +import { createMasterContext } from './util'; const rid = toResourceIdentifierStr({ resourceType: 'c', resourceId: '1' }); @@ -28,9 +29,11 @@ test('gets initial state transformed', async () => { }) ); - const actualPublic = await master.getPublicState(rid).resolveUnwrap(); + const mockMasterContext = createMasterContext({ requestAt: 123 }); + + const actualPublic = await master.getPublicState(rid, mockMasterContext).resolveUnwrap(); const actualClientSpecific = await master - .getClientSpecificState(rid, 'testClient') + .getClientSpecificState(rid, 'testClient', mockMasterContext) .resolveUnwrap(); const expected = computeCheckedState({ count: -99 }); @@ -59,9 +62,13 @@ test('gets initial state transformed with Prev State', async () => { }) ); - const actualPublic = await master.getPublicState(rid).resolveUnwrap(); + const mockMasterContext = createMasterContext({ requestAt: 123 }); + + const actualPublic = await master + .getPublicState(rid, mockMasterContext) + .resolveUnwrap(); const actualClientSpecific = await master - .getClientSpecificState(rid, 'testClient') + .getClientSpecificState(rid, 'testClient', mockMasterContext) .resolveUnwrap(); const expected = computeCheckedState({ count: -104 }); @@ -76,7 +83,7 @@ test('gets initial state transformed with PrevState and Movex Context', async () (counterReducer as any).$transformState = ( prev: CounterState, - context: MovexRemoteContext + context: MovexMasterContext ): CounterState => { return { count: context.now() }; }; @@ -90,9 +97,13 @@ test('gets initial state transformed with PrevState and Movex Context', async () }) ); - const actualPublic = await master.getPublicState(rid).resolveUnwrap(); + const mockMasterContext = createMasterContext({ requestAt: 123 }); + + const actualPublic = await master + .getPublicState(rid, mockMasterContext) + .resolveUnwrap(); const actualClientSpecific = await master - .getClientSpecificState(rid, 'testClient') + .getClientSpecificState(rid, 'testClient', mockMasterContext) .resolveUnwrap(); MockDate.reset(); diff --git a/libs/movex-master/src/lib/MovexMasterResource.ts b/libs/movex-master/src/lib/MovexMasterResource.ts index 854ec78f..bb712d78 100644 --- a/libs/movex-master/src/lib/MovexMasterResource.ts +++ b/libs/movex-master/src/lib/MovexMasterResource.ts @@ -12,15 +12,12 @@ import { MovexReducer, isMasterAction, GenericMasterAction, - toMasterAction, - ToMasterAction, invoke, -} from 'movex-core-util'; -import { isAction, toResourceIdentifierStr, computeCheckedState, objectKeys, + MovexMasterContext, } from 'movex-core-util'; import { AsyncOk, AsyncResult } from 'ts-async-results'; import type { MovexStatePatch, MovexStore, MovexStoreItem } from 'movex-store'; @@ -47,25 +44,34 @@ export class MovexMasterResource< private computeClientState( clientId: MovexClient['id'], - item: MovexStoreItem + item: MovexStoreItem, + masterContext: MovexMasterContext ) { const patches = item.patches?.[clientId]; if (patches) { return this.applyStateTransformer( - computeCheckedState(this.mergeStatePatches(item.state[0], patches)) + computeCheckedState(this.mergeStatePatches(item.state[0], patches)), + masterContext ); } - return this.applyStateTransformer(item.state); + return this.applyStateTransformer(item.state, masterContext); } private applyStateTransformer( - checkedState: CheckedState + checkedState: CheckedState, + masterContext: MovexMasterContext ): CheckedState { if (typeof this.reducer.$transformState === 'function') { - const masterContext = { - now: () => new Date().getTime(), // Should the context just be defined here? - }; + // const masterContext: MovexMasterContext = { + // now: () => new Date().getTime(), // Should the context just be defined here?, + // requestAt: + // }; + + // console.log( + // '[Movex-master] applyStateTransformer', + // JSON.stringify({ masterContext }) + // ); return computeCheckedState( this.reducer.$transformState(checkedState[0], masterContext) @@ -115,14 +121,15 @@ export class MovexMasterResource< } getPublicState( - rid: ResourceIdentifier + rid: ResourceIdentifier, + masterContext: MovexMasterContext ) { // 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.getStoreItem(rid) .map((r) => r.state) - .map((s) => this.applyStateTransformer(s)); + .map((s) => this.applyStateTransformer(s, masterContext)); } /** @@ -135,11 +142,12 @@ export class MovexMasterResource< */ getClientSpecificState( rid: ResourceIdentifier, - clientId: MovexClient['id'] + clientId: MovexClient['id'], + masterContext: MovexMasterContext ) { - return this.store - .get(rid, clientId) - .map((item) => this.computeClientState(clientId, item)); + return this.getClientSpecificResource(rid, clientId, masterContext).map( + (s) => s.state + ); } /** @@ -148,31 +156,40 @@ export class MovexMasterResource< * @param rid * @param clientId */ - getClientSpecificResource( + public getClientSpecificResource( rid: ResourceIdentifier, - clientId: MovexClient['id'] + clientId: MovexClient['id'], + masterContext: MovexMasterContext ) { return this.store.get(rid, clientId).map((item) => ({ ...item, - state: this.computeClientState(clientId, item), + state: this.computeClientState(clientId, item, masterContext), })); } private getStateBySubscriberId( - rid: ResourceIdentifier + rid: ResourceIdentifier, + masterContext: MovexMasterContext ) { return this.getStoreItem(rid).map((item) => - this.computeStateForItemSubscribers(item) + this.computeStateForItemSubscribers(item, masterContext) ); } private computeStateForItemSubscribers< TResourceType extends GenericResourceType - >(item: MovexStoreItem) { + >( + item: MovexStoreItem, + masterContext: MovexMasterContext + ) { return objectKeys(item.subscribers).reduce( (prev, nextClientId) => ({ ...prev, - [nextClientId]: this.computeClientState(nextClientId, item), + [nextClientId]: this.computeClientState( + nextClientId, + item, + masterContext + ), }), {} as Record> ); @@ -208,7 +225,8 @@ export class MovexMasterResource< applyAction( rid: ResourceIdentifier, clientId: MovexClient['id'], - actionOrActionTuple: ActionOrActionTupleFromAction + actionOrActionTuple: ActionOrActionTupleFromAction, + masterContext: MovexMasterContext ) { type ForwardablePeerActions = { type: 'forwardable'; @@ -231,224 +249,223 @@ export class MovexMasterResource< peerActions: PeerActions; }; - return this.getStoreItem(rid).flatMap( - (prevItem) => { - const [prevState] = this.computeClientState(clientId, prevItem); - - if (isAction(actionOrActionTuple)) { - const publicAction = invoke(() => { - if (isMasterAction(actionOrActionTuple)) { - return { - wasMasterAction: true, - action: parseMasterAction( - actionOrActionTuple as GenericMasterAction - ) as TAction, - }; - } + return this.getClientSpecificResource(rid, clientId, masterContext).flatMap< + ResponsePayload, + unknown + >((resource) => { + const prevState = resource.state[0]; + if (isAction(actionOrActionTuple)) { + const publicAction = invoke(() => { + if (isMasterAction(actionOrActionTuple)) { return { - action: actionOrActionTuple, + wasMasterAction: true, + action: parseMasterAction( + actionOrActionTuple as GenericMasterAction + ) as TAction, }; - }); + } + + return { + action: actionOrActionTuple, + }; + }); + + return this.store + .updateState(rid, this.reducer(prevState, publicAction.action)) + .map( + (nextPublicState): ResponsePayload => ({ + nextPublic: { + checksum: nextPublicState.state[1], + action: publicAction.action, + wasMasterAction: publicAction.wasMasterAction, + }, + peerActions: { + type: 'forwardable', + byClientId: objectKeys(resource.subscribers).reduce( + (prev, nextClientId) => { + // Exclude the sender + if (nextClientId === clientId) { + return prev; + } + + return { + ...prev, + [nextClientId]: { + checksum: nextPublicState.state[1], + action: publicAction.action, + }, + }; + }, + {} as ForwardablePeerActions['byClientId'] + ), + }, + }) + ); + } - return this.store - .updateState(rid, this.reducer(prevState, publicAction.action)) - .map( - (nextPublicState): ResponsePayload => ({ - nextPublic: { - checksum: nextPublicState.state[1], - action: publicAction.action, - wasMasterAction: publicAction.wasMasterAction, - }, - peerActions: { - type: 'forwardable', - byClientId: objectKeys(prevItem.subscribers).reduce( - (prev, nextClientId) => { - // Exclude the sender - if (nextClientId === clientId) { - return prev; - } + const [privateAction, publicAction] = actionOrActionTuple; + const nextPrivateState = this.reducer(prevState, privateAction); + const privatePatch = getMovexStatePatch(prevState, nextPrivateState); + + return ( + this.store + // Apply the Private Action + .addPrivatePatch(rid, clientId, { + action: privateAction, + patch: privatePatch, + }) + .flatMap((itemWithLatestPatch) => + AsyncResult.all( + new AsyncOk(itemWithLatestPatch), + + this.getClientSpecificState(rid, clientId, masterContext), + + // Apply the Public Action + // *Note The Public Action needs to get applied after the private one! + // otherwise the resulted private patch will be based off of the next public state + // instead of the prev (private) one. + this.store + .updateState(rid, this.reducer(prevState, publicAction)) + .map((s) => s.state) + ) + ) + .flatMap(([nextItem, nextPrivateState, nextPublicState]) => + AsyncResult.all( + new AsyncOk(nextItem), + new AsyncOk(nextPrivateState), + new AsyncOk(nextPublicState), + + // Need to get this after the public state updates + this.getStateBySubscriberId(rid, masterContext) + ) + ) + // Reconciliation Step + .flatMap( + ([ + nextItem, + nextPrivateState, + nextPublicState, + stateBySubscribersId, + ]) => { + if (this.reducer.$canReconcileState?.(nextPublicState[0])) { + const prevPatchesByClientId = nextItem.patches || {}; + + const allPatches = Object.values(prevPatchesByClientId).reduce( + (prev, next) => [...prev, ...next], + [] as MovexStatePatch[] + ); + + // Merge all the private patches into the public state + const mergedState = this.mergeStatePatches( + nextPublicState[0], + allPatches + ); + + // Run it once more through the reducer with the given private action + // In order to calculate any derived state. If no state get calculated in + // this step, in theory it just returns the prev, but in some cases + // when a different field (such as "isWinner" or "status"), needs to get computed + // based on the fields modified by the private action is when it's needed! + const reconciledState = this.reducer( + mergedState, + privateAction + ); + + return this.store + .update(rid, { + state: computeCheckedState(reconciledState), + // Clear the patches from the Item + patches: undefined, + }) + .map((nextReconciledItemFromPublicState) => { + const checkedReconciliatoryActionsByClientId = objectKeys( + prevPatchesByClientId + ).reduce((accum, nextClientId) => { + const { + [nextClientId]: _, + ...peersPrevPatchesByClientId + } = prevPatchesByClientId; + + const allPeersPatchesAsList = objectKeys( + peersPrevPatchesByClientId + ).reduce((prev, nextPeerId) => { + return [ + ...prev, + ...peersPrevPatchesByClientId[nextPeerId].map( + (p) => + ({ + ...p.action, + isPrivate: undefined, // make the action public + } as ToPublicAction) + ), + ]; + }, [] as ToPublicAction[]); return { - ...prev, + ...accum, [nextClientId]: { - checksum: nextPublicState.state[1], - action: publicAction.action, + actions: allPeersPatchesAsList, + finalChecksum: + nextReconciledItemFromPublicState.state[1], + // finalState: nextReconciledItemFromPublicState.state[0], }, }; - }, - {} as ForwardablePeerActions['byClientId'] - ), - }, - }) - ); - } - - const [privateAction, publicAction] = actionOrActionTuple; - const nextPrivateState = this.reducer(prevState, privateAction); - const privatePatch = getMovexStatePatch(prevState, nextPrivateState); - - return ( - this.store - // Apply the Private Action - .addPrivatePatch(rid, clientId, { - action: privateAction, - patch: privatePatch, - }) - .flatMap((itemWithLatestPatch) => - AsyncResult.all( - new AsyncOk(itemWithLatestPatch), - - this.getClientSpecificState(rid, clientId), - - // Apply the Public Action - // *Note The Public Action needs to get applied after the private one! - // otherwise the resulted private patch will be based off of the next public state - // instead of the prev (private) one. - this.store - .updateState(rid, this.reducer(prevState, publicAction)) - .map((s) => s.state) - ) - ) - .flatMap(([nextItem, nextPrivateState, nextPublicState]) => - AsyncResult.all( - new AsyncOk(nextItem), - new AsyncOk(nextPrivateState), - new AsyncOk(nextPublicState), - - // Need to get this after the public state updates - this.getStateBySubscriberId(rid) - ) - ) - // Reconciliation Step - .flatMap( - ([ - nextItem, - nextPrivateState, - nextPublicState, - stateBySubscribersId, - ]) => { - if (this.reducer.$canReconcileState?.(nextPublicState[0])) { - const prevPatchesByClientId = nextItem.patches || {}; - - const allPatches = Object.values( - prevPatchesByClientId - ).reduce( - (prev, next) => [...prev, ...next], - [] as MovexStatePatch[] - ); - - // Merge all the private patches into the public state - const mergedState = this.mergeStatePatches( - nextPublicState[0], - allPatches - ); - - // Run it once more through the reducer with the given private action - // In order to calculate any derived state. If no state get calculated in - // this step, in theory it just returns the prev, but in some cases - // when a different field (such as "isWinner" or "status"), needs to get computed - // based on the fields modified by the private action is when it's needed! - const reconciledState = this.reducer( - mergedState, - privateAction - ); - - return this.store - .update(rid, { - state: computeCheckedState(reconciledState), - // Clear the patches from the Item - patches: undefined, - }) - .map((nextReconciledItemFromPublicState) => { - const checkedReconciliatoryActionsByClientId = objectKeys( - prevPatchesByClientId - ).reduce((accum, nextClientId) => { - const { - [nextClientId]: _, - ...peersPrevPatchesByClientId - } = prevPatchesByClientId; - - const allPeersPatchesAsList = objectKeys( - peersPrevPatchesByClientId - ).reduce((prev, nextPeerId) => { - return [ - ...prev, - ...peersPrevPatchesByClientId[nextPeerId].map( - (p) => - ({ - ...p.action, - isPrivate: undefined, // make the action public - } as ToPublicAction) - ), - ]; - }, [] as ToPublicAction[]); - - return { - ...accum, - [nextClientId]: { - actions: allPeersPatchesAsList, - finalChecksum: - nextReconciledItemFromPublicState.state[1], - // finalState: nextReconciledItemFromPublicState.state[0], - }, - }; - }, {} as Record>); + }, {} as Record>); + + return { + nextPublic: { + checksum: nextReconciledItemFromPublicState.state[1], + action: publicAction, + }, + nextPrivate: { + checksum: nextReconciledItemFromPublicState.state[1], + action: privateAction, + }, + peerActions: { + type: 'reconcilable', + byClientId: checkedReconciliatoryActionsByClientId, + } as ReconcilablePeerActions, + } satisfies ResponsePayload; + }); + } - return { - nextPublic: { - checksum: nextReconciledItemFromPublicState.state[1], - action: publicAction, - }, - nextPrivate: { - checksum: nextReconciledItemFromPublicState.state[1], - action: privateAction, - }, - peerActions: { - type: 'reconcilable', - byClientId: checkedReconciliatoryActionsByClientId, - } as ReconcilablePeerActions, - } satisfies ResponsePayload; - }); + const nexForwardableActionsByClientId = objectKeys( + stateBySubscribersId + ).reduce((prev, nextClientId) => { + // Exclude the sender + if (nextClientId === clientId) { + return prev; } - const nexForwardableActionsByClientId = objectKeys( - stateBySubscribersId - ).reduce((prev, nextClientId) => { - // Exclude the sender - if (nextClientId === clientId) { - return prev; - } - - return { - ...prev, - [nextClientId]: { - action: publicAction, - checksum: stateBySubscribersId[nextClientId][1], - _state: stateBySubscribersId[nextClientId][0], - }, - }; - }, {} as ForwardablePeerActions['byClientId']); - - return new AsyncOk({ - nextPublic: { - checksum: nextPublicState[1], + return { + ...prev, + [nextClientId]: { action: publicAction, + checksum: stateBySubscribersId[nextClientId][1], + _state: stateBySubscribersId[nextClientId][0], }, - nextPrivate: { - checksum: nextPrivateState[1], - action: privateAction, - }, - peerActions: { - type: 'forwardable', - byClientId: nexForwardableActionsByClientId, - }, - } as const); - } - ) - ); - } - ); + }; + }, {} as ForwardablePeerActions['byClientId']); + + return new AsyncOk({ + nextPublic: { + checksum: nextPublicState[1], + action: publicAction, + }, + nextPrivate: { + checksum: nextPrivateState[1], + action: privateAction, + }, + peerActions: { + type: 'forwardable', + byClientId: nexForwardableActionsByClientId, + }, + } as const); + } + ) + ); + }); } // TODO: The ResourceType could be generic, or given in the Class Generic @@ -470,8 +487,8 @@ export class MovexMasterResource< subcriberId: MovexClient['id'] ) { // TODO Optimization: The store could have the append/remove implemented so it doesn't do a round trip looking for prev - return this.store.update(rid, (prev) => { - return { + return this.store + .update(rid, (prev) => ({ ...prev, subscribers: { ...prev.subscribers, @@ -479,8 +496,8 @@ export class MovexMasterResource< subscribedAt: new Date().getTime(), }, }, - }; - }); + })) + .map((s) => s.subscribers); } removeResourceSubscriber( @@ -488,14 +505,16 @@ export class MovexMasterResource< subcriberId: MovexClient['id'] ) { // TODO Optimization: The store could have the append/remove implemented so it doesn't do a round trip looking for prev - return this.store.update(rid, (prev) => { - const { [subcriberId]: removed, ...rest } = prev.subscribers; - - return { - ...prev, - subscribers: rest, - }; - }); + return this.store + .update(rid, (prev) => { + const { [subcriberId]: removed, ...rest } = prev.subscribers; + + return { + ...prev, + subscribers: rest, + }; + }) + .map((s) => s.subscribers); } // updateUncheckedState( diff --git a/libs/movex-master/src/lib/MovexMasterServer.ts b/libs/movex-master/src/lib/MovexMasterServer.ts index 5e533b27..7541ea44 100644 --- a/libs/movex-master/src/lib/MovexMasterServer.ts +++ b/libs/movex-master/src/lib/MovexMasterServer.ts @@ -14,8 +14,10 @@ import { SanitizedMovexClient, GenericResourceType, objectOmit, + ResourceIdentifier, + MovexMasterContext, } from 'movex-core-util'; -import { itemToSanitizedClientResource } from './util'; +import { createMasterContext, itemToSanitizedClientResource } from './util'; import { type ConnectionToClient } from './ConnectionToClient'; const logsy = globalLogsy.withNamespace('[MovexMasterServer]'); @@ -96,8 +98,16 @@ export class MovexMasterServer { return acknowledge?.(new Err('MasterResourceInexistent')); } + const masterContext = createMasterContext({ + extra: { + clientId: clientConnection.client.id, + req: 'onEmitAction', + action: payload.action, + }, + }); + masterResource - .applyAction(rid, clientConnection.client.id, action) + .applyAction(rid, clientConnection.client.id, action, masterContext) .map(({ nextPublic, nextPrivate, peerActions }) => { if (peerActions.type === 'reconcilable') { // TODO: Filter out the client id so it only received the ack @@ -171,37 +181,35 @@ export class MovexMasterServer { }; const onGetResourceHandler = ( - payload: Parameters['getResourceState']>[0], + { rid }: 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')); - } + const masterContext = createMasterContext({ + extra: { + clientId: clientConnection.client.id, + req: 'onGetResourceHandler', + }, + }); - masterResource - .getClientSpecificResource(rid, clientConnection.client.id) + this.getSanitizedClientSpecificResource( + rid, + clientConnection.client, + masterContext + ) .map((r) => { - acknowledge?.( - new Ok( - itemToSanitizedClientResource( - this.populateClientInfoToSubscribers(r) - ) - ) - ); + acknowledge?.(new Ok(r)); }) - .mapErr( - AsyncResult.passThrough((e) => { - logsy.error('Get Resource Error', e); - }) - ) - .mapErr((e) => acknowledge?.(new Err(e))); + .mapErr((error) => { + logsy.error('GetResource Error', { + error, + rid, + clientId: this.clientConnectionsByClientId, + }); + + acknowledge?.(new Err(error)); + }); }; const onGetResourceStateHandler = ( @@ -219,15 +227,25 @@ export class MovexMasterServer { return acknowledge?.(new Err('MasterResourceInexistent')); } + const masterContext = createMasterContext({ + extra: { + clientId: clientConnection.client.id, + req: 'onGetResourceStateHandler', + }, + }); + masterResource - .getClientSpecificState(rid, clientConnection.client.id) + .getClientSpecificState(rid, clientConnection.client.id, masterContext) .map((checkedState) => acknowledge?.(new Ok(checkedState))) - .mapErr( - AsyncResult.passThrough((e) => { - logsy.error('Get Resource Error', e); - }) - ) - .mapErr((e) => acknowledge?.(new Err(e))); + .mapErr((error) => { + logsy.error('GetResourceState Error', { + error, + rid, + clientId: this.clientConnectionsByClientId, + }); + + acknowledge?.(new Err(error)); + }); }; const onGetResourceSubscribersHandler = ( @@ -259,23 +277,31 @@ export class MovexMasterServer { return acknowledge?.(new Err('MasterResourceInexistent')); } + const masterContext = createMasterContext({ + extra: { + clientId: clientConnection.client.id, + req: 'onCreateResourceHandler', + }, + }); + masterResource .create(resourceType, resourceState, resourceId) - .map((r) => - acknowledge?.( - new Ok( - itemToSanitizedClientResource( - this.populateClientInfoToSubscribers(r) - ) - ) - ) + .flatMap((r) => + this.getSanitizedClientSpecificResource( + r.rid, + clientConnection.client, + masterContext + ).mapErr((e) => e) ) - .mapErr( - AsyncResult.passThrough((e) => { - logsy.error(''); - }) - ) - .mapErr(() => acknowledge?.(new Err('UnknownError'))); // TODO: Type this using the ResultError from Matterio + .map((r) => acknowledge?.(new Ok(r))) + .mapErr((error) => { + logsy.error('OnCreateResourceHandler', { + error, + clientId: this.clientConnectionsByClientId, + }); + + acknowledge?.(new Err('UnknownError')); + }); // TODO: Type this using the ResultError from Matterio }; const onAddResourceSubscriber = ( @@ -294,54 +320,69 @@ export class MovexMasterServer { return acknowledge?.(new Err('MasterResourceInexistent')); } + const masterContext = createMasterContext({ + extra: { + clientId: clientConnection.client.id, + req: 'onAddResourceSubscriber', + }, + }); + masterResource .addResourceSubscriber(payload.rid, clientConnection.client.id) - .map((s) => { + .flatMap(() => + this.getSanitizedClientSpecificResource( + payload.rid, + clientConnection.client, + masterContext + ) + ) + .map((sanitizedResource) => { // Keep a record of the rid it just subscribed to so it can also be unsubscribed this.subscribersToRidsMap = { ...this.subscribersToRidsMap, [clientConnection.client.id]: { ...this.subscribersToRidsMap[clientConnection.client.id], - [s.rid]: undefined, + [sanitizedResource.rid]: undefined, }, }; // Send the ack to the just-added-client - acknowledge?.( - new Ok( - itemToSanitizedClientResource( - this.populateClientInfoToSubscribers(s) - ) - ) - ); + acknowledge?.(new Ok(sanitizedResource)); // Let the rest of the peer-clients know as well - objectKeys(s.subscribers) + objectKeys(sanitizedResource.subscribers) // Take out just-added-client .filter((clientId) => clientId !== clientConnection.client.id) .forEach((peerId) => { const peerConnection = this.clientConnectionsByClientId[peerId]; if (!peerConnection) { - logsy.error('OnAddResourceSubscriber PeerConnectionNotFound', { - peerId, - clientId: this.clientConnectionsByClientId, - }); + logsy.error( + 'OnAddResourceSubscriber PeerConnectionNotFound Error', + { + peerId, + rid: payload.rid, + clientId: this.clientConnectionsByClientId, + } + ); return; } - const client: SanitizedMovexClient = { - id: clientConnection.client.id, - info: clientConnection.client.info, - }; - peerConnection.emitter.emit('onResourceSubscriberAdded', { rid: payload.rid, - client, + client: clientConnection.client, // TODO: Ensure this doesn't add more props than needed }); }); }) - .mapErr((e) => acknowledge?.(new Err('UnknownError'))); // TODO: Type this using the ResultError from Matterio + .mapErr((error) => { + logsy.error('OnAddResourceSubscriber Error', { + error, + rid: payload.rid, + clientId: this.clientConnectionsByClientId, + }); + + acknowledge?.(new Err('UnknownError')); + }); // TODO: Type this using the ResultError from Matterio }; const onPingHandler = ( @@ -401,11 +442,36 @@ export class MovexMasterServer { }; } + private getSanitizedClientSpecificResource( + rid: ResourceIdentifier, + client: SanitizedMovexClient, + masterContext: MovexMasterContext + ) { + const masterResource = + this.masterResourcesByType[toResourceIdentifierObj(rid).resourceType]; + + if (!masterResource) { + return new Err('MasterResourceInexistent'); + } + + return masterResource + .getClientSpecificResource(rid, client.id, masterContext) + .map((r) => + itemToSanitizedClientResource( + this.populateClientInfoToSubscribers(r), + client.clockOffset + ) + ); + } + public getPublicResourceCheckedState< S, A extends AnyAction, TResourceType extends string - >({ rid }: Parameters['getResourceState']>[0]) { + >( + { rid }: Parameters['getResourceState']>[0], + masterContext: MovexMasterContext + ) { const masterResource = this.masterResourcesByType[toResourceIdentifierObj(rid).resourceType]; @@ -415,7 +481,7 @@ export class MovexMasterServer { ); } - return masterResource.getPublicState(rid); + return masterResource.getPublicState(rid, masterContext); } private populateClientInfoToSubscribers = < diff --git a/libs/movex-master/src/lib/init.ts b/libs/movex-master/src/lib/init.ts index 8ed4200b..66d11843 100644 --- a/libs/movex-master/src/lib/init.ts +++ b/libs/movex-master/src/lib/init.ts @@ -1,11 +1,13 @@ import { + globalLogsy, objectKeys, type BaseMovexDefinitionResourcesMap, type MovexDefinition, -} from 'movex-core-util'; +} from 'movex-core-util'; import { MovexMasterResource } from './MovexMasterResource'; import { MovexMasterServer } from './MovexMasterServer'; import type { MovexStore } from 'movex-store'; +const pkgVersion = require('../../package.json').version; export const initMovexMaster = < TResourcesMap extends BaseMovexDefinitionResourcesMap @@ -13,6 +15,13 @@ export const initMovexMaster = < definition: MovexDefinition, store: MovexStore // | 'redis' once it's implemented ) => { + // Run this only if in node! + // if (process?.env) { + + globalLogsy.info(`[MovexMaster] v${pkgVersion || 'Client-version'} initiating...`); + + // } + const mapOfResourceReducers = objectKeys(definition.resources).reduce( (accum, nextResoureType) => { const nextReducer = definition.resources[nextResoureType]; diff --git a/libs/movex-master/src/lib/util.ts b/libs/movex-master/src/lib/util.ts index 96fc93d9..045d5431 100644 --- a/libs/movex-master/src/lib/util.ts +++ b/libs/movex-master/src/lib/util.ts @@ -1,10 +1,5 @@ export { v4 as getUuid } from 'uuid'; -import { - applyReducer, - compare, - deepClone, - applyPatch, -} from 'fast-json-patch'; +import { applyReducer, compare, deepClone, applyPatch } from 'fast-json-patch'; import { JsonPatch, isObject, @@ -16,6 +11,9 @@ import { GenericMasterAction, MasterQueries, ToPublicAction, + MovexMasterContext, + SanitizedMovexClient, + UnknownRecord, } from 'movex-core-util'; import { MovexStoreItem } from 'movex-store'; @@ -89,7 +87,8 @@ export const itemToSanitizedClientResource = < info: MovexClient['info']; } >; - } + }, + clockOffset: number ): MovexClientResourceShape => ({ rid: toResourceIdentifierStr(item.rid), state: item.state, @@ -99,6 +98,7 @@ export const itemToSanitizedClientResource = < [nextSubId]: { id: nextSubId, info: item.subscribers[nextSubId].info || {}, + clockOffset, }, }), {} as MovexClientResourceShape['subscribers'] @@ -168,3 +168,26 @@ export const parseMasterAction = ( payload: nextAction.payload, } as ToPublicAction; }; + +export const createMasterContext = (p?: { + requestAt?: number; + extra?: UnknownRecord; +}): MovexMasterContext => ({ + // @Deprecate in favor of requestAt Props which enables purity + now: () => new Date().getTime(), + + requestAt: p?.requestAt || new Date().getTime(), + + ...(p?.extra && { _extra: p?.extra }), +}); + +export const createSanitizedMovexClient = < + TInfo extends SanitizedMovexClient['info'] = SanitizedMovexClient['info'] +>( + id: string, + p?: { info?: TInfo; clockOffset?: SanitizedMovexClient['clockOffset'] } +): SanitizedMovexClient => ({ + id, + info: p?.info || {}, + clockOffset: p?.clockOffset || 0, +}); 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 e961a481..26965008 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.match.spec.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.match.spec.ts @@ -4,6 +4,7 @@ import { tillNextTick, matchReducer, } from 'movex-specs-util'; +import { createSanitizedMovexClient } from '../../lib'; import { movexClientMasterOrchestrator } from './orchestrator'; const orchestrator = movexClientMasterOrchestrator(); @@ -55,14 +56,8 @@ test('works with public actions', async () => { }, }), subscribers: { - 'white-client': { - id: 'white-client', - info: {}, - }, - 'black-client': { - id: 'black-client', - info: {}, - }, + 'white-client': createSanitizedMovexClient('white-client'), + 'black-client': createSanitizedMovexClient('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 00601aec..cd1c9ad5 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.rps.spec.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.rps.spec.ts @@ -1,5 +1,6 @@ import { computeCheckedState } from 'movex-core-util'; import { rpsReducer, rpsInitialState, tillNextTick } from 'movex-specs-util'; +import { createSanitizedMovexClient } from '../../lib'; import { movexClientMasterOrchestrator } from './orchestrator'; const orchestrator = movexClientMasterOrchestrator(); @@ -89,14 +90,8 @@ test('2 Clients. Both Submitting (White first) WITH Reconciliation and the recon }, }), subscribers: { - 'client-a': { - id: 'client-a', - info: {}, - }, - 'client-b': { - id: 'client-b', - info: {}, - }, + 'client-a': createSanitizedMovexClient('client-a'), + 'client-b': createSanitizedMovexClient('client-b'), }, }; diff --git a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts index 4f14e5ee..6ef10c4e 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/ClientMaster.spec.ts @@ -6,6 +6,7 @@ import { initialRawGameStateWithDerivedState, tillNextTick, } from 'movex-specs-util'; +import { createSanitizedMovexClient } from '../../lib'; import { movexClientMasterOrchestrator } from './orchestrator'; const orchestrator = movexClientMasterOrchestrator(); @@ -91,14 +92,8 @@ describe('Public Actions', () => { count: 5, }), subscribers: { - 'white-client': { - id: 'white-client', - info: {}, - }, - 'black-client': { - id: 'black-client', - info: {}, - }, + 'white-client': createSanitizedMovexClient('white-client'), + 'black-client': createSanitizedMovexClient('black-client'), }, }; @@ -165,14 +160,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': { - id: 'white-client', - info: {}, - }, - 'black-client': { - id: 'black-client', - info: {}, - }, + 'white-client': createSanitizedMovexClient('white-client'), + 'black-client': createSanitizedMovexClient('black-client'), }, }; @@ -200,14 +189,8 @@ describe('Private Actions', () => { const expectedPeerState = { checkedState: publicState, subscribers: { - 'white-client': { - id: 'white-client', - info: {}, - }, - 'black-client': { - id: 'black-client', - info: {}, - }, + 'white-client': createSanitizedMovexClient('white-client'), + 'black-client': createSanitizedMovexClient('black-client'), }, }; const actualPeerState = blackMovex.get(); @@ -274,14 +257,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': { - id: 'white-client', - info: {}, - }, - 'black-client': { - id: 'black-client', - info: {}, - }, + 'white-client': createSanitizedMovexClient('white-client'), + 'black-client': createSanitizedMovexClient('black-client'), }, }; @@ -326,14 +303,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': { - id: 'white-client', - info: {}, - }, - 'black-client': { - id: 'black-client', - info: {}, - }, + 'white-client': createSanitizedMovexClient('white-client'), + 'black-client': createSanitizedMovexClient('black-client'), }, }; @@ -357,14 +328,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': { - id: 'white-client', - info: {}, - }, - 'black-client': { - id: 'black-client', - info: {}, - }, + 'white-client': createSanitizedMovexClient('white-client'), + 'black-client': createSanitizedMovexClient('black-client'), }, }; // The Private Action gets set @@ -426,14 +391,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': { - id: 'white-client', - info: {}, - }, - 'black-client': { - id: 'black-client', - info: {}, - }, + 'white-client': createSanitizedMovexClient('white-client'), + 'black-client': createSanitizedMovexClient('black-client'), }, }; @@ -475,14 +434,8 @@ describe('Private Actions', () => { }, }), subscribers: { - 'white-client': { - id: 'white-client', - info: {}, - }, - 'black-client': { - id: 'black-client', - info: {}, - }, + 'white-client': createSanitizedMovexClient('white-client'), + 'black-client': createSanitizedMovexClient('black-client'), }, }; diff --git a/libs/movex-master/src/specs/ClientMasterOrchestration/orchestrator.ts b/libs/movex-master/src/specs/ClientMasterOrchestration/orchestrator.ts index 4e02888a..4641146b 100644 --- a/libs/movex-master/src/specs/ClientMasterOrchestration/orchestrator.ts +++ b/libs/movex-master/src/specs/ClientMasterOrchestration/orchestrator.ts @@ -5,6 +5,7 @@ import { MovexReducer, MovexClientInfo, SanitizedMovexClient, + MovexMasterContext, } from 'movex-core-util'; import { Movex, ConnectionToMaster } from 'movex'; import { MovexMasterResource, MovexMasterServer } from 'movex-master'; @@ -45,10 +46,11 @@ export const movexClientMasterOrchestrator = < const clientEmitters: MockConnectionEmitter[] = []; const clients = clientIds.map((clientId) => { - const client = { + const client: SanitizedMovexClient = { id: clientId, // TODO: If this needs to be given here is where it can be info: {} as TClientInfo, + clockOffset: 0, }; // Would this be the only one for both client and master or seperate? @@ -88,10 +90,16 @@ export const movexClientMasterOrchestrator = < return mockedMovex.movex.register(resourceType, reducer); }); + // TODO: This might need to change according to the needs of the test + const masterContext: MovexMasterContext = { + now: () => new Date().getTime(), + requestAt: new Date().getTime(), + }; + return { master: { getPublicState: (rid: ResourceIdentifier) => - masterResource.getPublicState(rid), + masterResource.getPublicState(rid, masterContext), }, clients, $util: { diff --git a/libs/movex-master/src/specs/Movex.spec.ts b/libs/movex-master/src/specs/Movex.spec.ts index 10647ef4..c16b62f0 100644 --- a/libs/movex-master/src/specs/Movex.spec.ts +++ b/libs/movex-master/src/specs/Movex.spec.ts @@ -5,7 +5,10 @@ import { initialGameState, tillNextTick, } from 'movex-specs-util'; -import { movexClientMasterOrchestrator } from 'movex-master'; +import { + createSanitizedMovexClient, + movexClientMasterOrchestrator, +} from 'movex-master'; const orchestrator = movexClientMasterOrchestrator(); @@ -55,10 +58,7 @@ describe('All', () => { const expected = { checkedState: computeCheckedState({ count: 2 }), subscribers: { - 'test-user': { - id: 'test-user', - info: {}, - }, + 'test-user': createSanitizedMovexClient('test-user'), }, }; @@ -86,10 +86,7 @@ describe('All', () => { const expected = { checkedState: computeCheckedState({ count: 3 }), subscribers: { - 'test-user': { - id: 'test-user', - info: {}, - }, + 'test-user': createSanitizedMovexClient('test-user'), }, }; @@ -138,10 +135,7 @@ describe('All', () => { }, }), subscribers: { - 'test-user': { - id: 'test-user', - info: {}, - }, + 'test-user': createSanitizedMovexClient('test-user'), }, }; diff --git a/libs/movex-master/src/specs/Movex.stateTransformers.spec.ts b/libs/movex-master/src/specs/Movex.stateTransformers.spec.ts index d5fc00da..8958de9a 100644 --- a/libs/movex-master/src/specs/Movex.stateTransformers.spec.ts +++ b/libs/movex-master/src/specs/Movex.stateTransformers.spec.ts @@ -5,7 +5,7 @@ import { speedPushGameReducer, tillNextTick, } from 'movex-specs-util'; -import { movexClientMasterOrchestrator } from 'movex-master'; +import { createSanitizedMovexClient, movexClientMasterOrchestrator } from 'movex-master'; import MockDate from 'mockdate'; const orchestrator = movexClientMasterOrchestrator(); @@ -52,10 +52,7 @@ test('State is changed (to status="completed") when a related ACTION DISPATCH tr timeToNextPushMs: initialSpeedPushGameState.timeToNextPushMs, }), subscribers: { - 'test-user': { - id: 'test-user', - info: {}, - }, + 'test-user': createSanitizedMovexClient('test-user'), }, }; diff --git a/libs/movex-react-local-master/src/lib/ClientMasterOrchestrator.ts b/libs/movex-react-local-master/src/lib/ClientMasterOrchestrator.ts index b653d38c..0e607315 100644 --- a/libs/movex-react-local-master/src/lib/ClientMasterOrchestrator.ts +++ b/libs/movex-react-local-master/src/lib/ClientMasterOrchestrator.ts @@ -41,6 +41,7 @@ export const orchestrateDefinedMovex = < info: { _clientType: 'orchestrator', // TODO: Take this one out }, + clockOffset: 0, }) ), emitter: emitterOnClient, diff --git a/libs/movex-react-local-master/src/lib/MovexLocalInstance.tsx b/libs/movex-react-local-master/src/lib/MovexLocalInstance.tsx index 316f9949..903e4ad5 100644 --- a/libs/movex-react-local-master/src/lib/MovexLocalInstance.tsx +++ b/libs/movex-react-local-master/src/lib/MovexLocalInstance.tsx @@ -46,7 +46,7 @@ export class MovexLocalInstance< clientId={this.props.clientId} movexDefinition={this.props.movexDefinition} onConnected={(r) => { - this.setState({ clientId: r.clientId }); + this.setState({ clientId: r.client.id }); this.props.onConnected?.(r.movex); }} diff --git a/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx b/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx index e2db53cd..64025b38 100644 --- a/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx +++ b/libs/movex-react-local-master/src/lib/MovexLocalProvider.tsx @@ -62,6 +62,7 @@ export class MovexLocalProvider< const clientWithInfo: SanitizedMovexClient<{}> = { id: this.props.clientId || getUuid(), info: {}, + clockOffset: 0, }; // This should be defined as real source not just as a mock @@ -99,8 +100,7 @@ export class MovexLocalProvider< const nextState: MovexReactContextPropsConnected = { status: 'connected', movex: mockedMovex.movex, - clientId: client.id, - clientInfo: client.info, + client, movexDefinition: this.props.movexDefinition, bindResource: >( rid: ResourceIdentifier, diff --git a/libs/movex-react/package.json b/libs/movex-react/package.json index 2b88eb49..3c40d13d 100644 --- a/libs/movex-react/package.json +++ b/libs/movex-react/package.json @@ -1,6 +1,6 @@ { "name": "movex-react", - "version": "0.1.6-23", + "version": "0.1.6-45", "license": "MIT", "description": "Movex React is the library of React components for Movex", "author": { diff --git a/libs/movex-react/src/lib/MovexBoundResourceComponent.tsx b/libs/movex-react/src/lib/MovexBoundResourceComponent.tsx index 99dcdb97..fcee6959 100644 --- a/libs/movex-react/src/lib/MovexBoundResourceComponent.tsx +++ b/libs/movex-react/src/lib/MovexBoundResourceComponent.tsx @@ -129,7 +129,7 @@ export class MovexBoundResourceComponent< if (r.status === 'connected') { this.init( // r.movex as MovexClient.MovexFromDefintion, - r.clientId, + r.client.id, r.bindResource ); } diff --git a/libs/movex-react/src/lib/MovexContext.ts b/libs/movex-react/src/lib/MovexContext.ts index e67fe033..e0f2d3f0 100644 --- a/libs/movex-react/src/lib/MovexContext.ts +++ b/libs/movex-react/src/lib/MovexContext.ts @@ -4,6 +4,7 @@ import type { MovexClientInfo, MovexDefinition, ResourceIdentifier, + SanitizedMovexClient, StringKeys, UnsubscribeFn, } from 'movex-core-util'; @@ -15,8 +16,9 @@ export type MovexContextProps< export type MovexContextPropsNotConnected = { status: 'disconnected' | 'initiating' | 'connectionError'; - clientId: undefined; - clientInfo: undefined; + // clientId: undefined; + // clientInfo: undefined; + client?: undefined; movex?: Movex; movexConfig?: undefined; bindResource?: () => void; @@ -26,8 +28,9 @@ export type MovexContextPropsConnected< TResourcesMap extends BaseMovexDefinitionResourcesMap > = { status: 'connected'; - clientId: string; - clientInfo: MovexClientInfo; + // clientId: string; + // clientInfo: MovexClientInfo; + client: SanitizedMovexClient; movex: MovexClient.MovexFromDefinition; movexDefinition: MovexDefinition; bindResource: >( @@ -39,8 +42,6 @@ export type MovexContextPropsConnected< export const initialMovexContext = { movex: undefined, status: 'initiating', - clientId: undefined, - clientInfo: undefined, } as const; export const MovexContext = diff --git a/libs/movex-react/src/lib/MovexProvider.tsx b/libs/movex-react/src/lib/MovexProvider.tsx index c20ff6eb..263d5a6b 100644 --- a/libs/movex-react/src/lib/MovexProvider.tsx +++ b/libs/movex-react/src/lib/MovexProvider.tsx @@ -76,8 +76,9 @@ export const MovexProvider: React.FC< const nextState: MovexContextPropsConnected = { status: 'connected', - clientId: client.id, - clientInfo: client.info, + // clientId: client.id, + // clientInfo: client.info, + client, movex, movexDefinition: props.movexDefinition, bindResource: >( diff --git a/libs/movex-react/src/lib/MovexResourceBounder.tsx b/libs/movex-react/src/lib/MovexResourceBounder.tsx index 33d4b8dc..31ad872a 100644 --- a/libs/movex-react/src/lib/MovexResourceBounder.tsx +++ b/libs/movex-react/src/lib/MovexResourceBounder.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import { useMovexBoundResourceFromRid, useMovexClientId } from './hooks'; +import { useMovexBoundResourceFromRid, useMovexClient } from './hooks'; import type { MovexClient } from 'movex'; import type { ResourceIdentifier, MovexDefinition } from 'movex-core-util'; @@ -10,7 +10,7 @@ type Props = React.PropsWithChildren<{ }>; export const MovexResourceBounder: React.FC = (props) => { - const clientId = useMovexClientId(props.movexDefinition); + const clientId = useMovexClient(props.movexDefinition)?.id; const boundResource = useMovexBoundResourceFromRid( props.movexDefinition, props.rid diff --git a/libs/movex-react/src/lib/hooks.ts b/libs/movex-react/src/lib/hooks.ts index b4a9ab24..f0c7d875 100644 --- a/libs/movex-react/src/lib/hooks.ts +++ b/libs/movex-react/src/lib/hooks.ts @@ -24,28 +24,15 @@ export const useMovex = ( MovexContext as Context> ); -export const useMovexClientId = < - TResourcesMap extends BaseMovexDefinitionResourcesMap ->( - movexConfig: MovexDefinition -) => useMovex(movexConfig).clientId; - -export const useMovexClient = < - TResourcesMap extends BaseMovexDefinitionResourcesMap ->( - movexConfig: MovexDefinition -): SanitizedMovexClient | undefined => { - const mc = useMovex(movexConfig); - - if (mc.status !== 'connected') { - return undefined; - } +// Removed on Aug 23 2024 in favor of only having the useMovexClient() +// export const useMovexClientId = ( +// movexConfig: MovexDefinition +// ) => useMovex(movexConfig).clientId; - return { - id: mc.clientId, - info: mc.clientInfo, - }; -}; +export const useMovexClient = ( + movexConfig?: MovexDefinition +): SanitizedMovexClient | undefined => + useMovex(movexConfig || { resources: {} }).client; export type MovexResourceFromConfig< TResourcesMap extends BaseMovexDefinitionResourcesMap, @@ -138,7 +125,7 @@ export const useMovexBoundResourceFromRid = < const unsubscribe = movexContext.bindResource(rid, (boundResource) => { setBoundResource(boundResource); - handlers?.onReady?.({ boundResource, clientId: movexContext.clientId }); + handlers?.onReady?.({ boundResource, clientId: movexContext.client.id }); }); return () => { diff --git a/libs/movex-react/src/specs/hooks/useMovex.spec.ts b/libs/movex-react/src/specs/hooks/useMovex.spec.ts index 25f95569..322ad73d 100644 --- a/libs/movex-react/src/specs/hooks/useMovex.spec.ts +++ b/libs/movex-react/src/specs/hooks/useMovex.spec.ts @@ -10,7 +10,9 @@ describe('useMovex', () => { const { result } = renderHook(() => useMovex(movexConfigMock)); expect(result.current.movex).toBeUndefined(); - expect(result.current.clientId).toBeUndefined(); - expect(result.current.connected).toEqual(false); + expect(result.current.client).toBeUndefined(); + expect(result.current.status).toEqual('initiating'); }); }); + +// TODO: Add more tests \ No newline at end of file diff --git a/libs/movex-server/package.json b/libs/movex-server/package.json index b13ee02b..d1811ab0 100644 --- a/libs/movex-server/package.json +++ b/libs/movex-server/package.json @@ -1,6 +1,6 @@ { "name": "movex-server", - "version": "0.1.6-23", + "version": "0.1.6-45", "license": "MIT", "type": "commonjs", "description": "Movex Server is the backend runtime for Movex", diff --git a/libs/movex-server/src/lib/movex-server.ts b/libs/movex-server/src/lib/movex-server.ts index a3888b50..edc06c66 100644 --- a/libs/movex-server/src/lib/movex-server.ts +++ b/libs/movex-server/src/lib/movex-server.ts @@ -13,14 +13,34 @@ import { MovexClientInfo, } from 'movex-core-util'; import { MemoryMovexStore, MovexStore } from 'movex-store'; -import { ConnectionToClient, initMovexMaster } from 'movex-master'; +import { + ConnectionToClient, + createMasterContext, + initMovexMaster, +} from 'movex-master'; import { delay, isOneOf } from './util'; const pkgVersion = require('../../package.json').version; -const pkgBuild = 3; const logsy = globalLogsy.withNamespace('[MovexServer]'); +logsy.onLog((event) => { + // if (config.DEBUG_MODE) { + console[event.method](event.prefix, event.message, event.payload); + // } + + // if (isOneOf(event.method, ['error', 'warn'])) { + // captureEvent({ + // level: event.method === 'warn' ? 'warning' : event.method, + // message: event.prefix + ' | ' + String(event.message), + // environment: config.ENV, + // // TODO: add more info if needed, like the resource id at least so it can be checked in the store + // // if not more relevant, timely stuff + // // extra: {} + // }); + // } +}); + export const movexServer = ( { httpServer = http.createServer(), @@ -44,6 +64,7 @@ export const movexServer = ( cors: { origin: corsOpts?.origin || '*', }, + // pingInterval: 5000, }); const store = @@ -86,17 +107,32 @@ export const movexServer = ( { id: clientId, info: clientInfo, + clockOffset: 0, // start it at 0 } ); - connectionToClient.emitClientReady(); + await connectionToClient.setReady(); movexMaster.addClientConnection(connectionToClient); + // socket.on('pong', (p) => { + // console.log('on pong', p); + // }); + + // io.on('pong', (p) => { + // console.log('on pong', p); + // }); + + // const pingIntervalId = setInterval(() => { + // io.emit('ping', { clientId }); + // }, 10 * 1000); + io.on('disconnect', () => { logsy.info('Client Disconnected', { clientId }); movexMaster.removeConnection(clientId); + + // clearInterval(pingIntervalId); }); }); @@ -149,6 +185,13 @@ export const movexServer = ( // Public State app.get('/api/resources/:rid/state', async (req, res) => { + const masterContext = createMasterContext({ + extra: { + clientId: 'UNKNOWN', + req: 'getPublicResourceCheckedState', + }, + }); + const rawRid = req.params.rid; if (!isResourceIdentifier(rawRid)) { @@ -164,7 +207,7 @@ export const movexServer = ( res.header('Content-Type', 'application/json'); return movexMaster - .getPublicResourceCheckedState({ rid: ridObj }) + .getPublicResourceCheckedState({ rid: ridObj }, masterContext) .map((checkedState) => res.json(checkedState)) .mapErr((e) => { if ( @@ -196,17 +239,13 @@ export const movexServer = ( httpServer.listen(port, () => { const address = httpServer.address(); - console.log( - `[movex-server] v${pkgVersion}${ - pkgBuild ? ` (build:${pkgBuild})` : '' - } started at port ${port}.` - ); + // console.log(`[movex-server] v${pkgVersion} started at port ${port}.`); - if (typeof address !== 'string') { - logsy.info('Server started', { - port, - definitionResources: Object.keys(definition.resources), - }); - } + // if (typeof address !== 'string') { + logsy.info(`v${pkgVersion} started`, { + port, + definitionResources: Object.keys(definition.resources), + }); + // } }); }; diff --git a/libs/movex-service/package.json b/libs/movex-service/package.json index 59e527f4..718ff3f6 100644 --- a/libs/movex-service/package.json +++ b/libs/movex-service/package.json @@ -1,6 +1,6 @@ { "name": "movex-service", - "version": "0.1.6-23", + "version": "0.1.6-45", "license": "MIT", "type": "commonjs", "description": "Movex Service is the CLI for Movex", diff --git a/libs/movex-service/src/bin.ts b/libs/movex-service/src/bin.ts index dfcc37c0..5755cf3d 100644 --- a/libs/movex-service/src/bin.ts +++ b/libs/movex-service/src/bin.ts @@ -86,7 +86,7 @@ const go = (args: string[]) => { const servePlugin = serve({ main: './.movex/runner.js' }); - console.log(`movex-service v${pkgJson.version}`); + console.log(`[MovexService] v${pkgJson.version}`); if (hasCommand('dev')) { (async () => { diff --git a/libs/movex-store/package.json b/libs/movex-store/package.json index 99d41613..36e591f7 100644 --- a/libs/movex-store/package.json +++ b/libs/movex-store/package.json @@ -1,6 +1,6 @@ { "name": "movex-store", - "version": "0.1.6-23", + "version": "0.1.6-45", "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 114b96d9..5ddbfa96 100644 --- a/libs/movex/package.json +++ b/libs/movex/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.6-23", + "version": "0.1.6-45", "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/libs/movex/src/lib/ConnectionToMaster.ts b/libs/movex/src/lib/ConnectionToMaster.ts index d5d69969..96a5f19a 100644 --- a/libs/movex/src/lib/ConnectionToMaster.ts +++ b/libs/movex/src/lib/ConnectionToMaster.ts @@ -12,8 +12,25 @@ export class ConnectionToMaster< TResourceType extends string, TClientInfo extends MovexClientInfo > { + // public latencyMs: number = 0; + constructor( public emitter: EventEmitter>, - public client: SanitizedMovexClient - ) {} + public client: SanitizedMovexClient, + ) { + // const lastPings: any[] = []; + // console.log('[MOVEX] Subscribing to ping/pong'); + // emitter.on('ping', (payload) => { + // lastPings.push(payload); + // console.log('ping received, latency', payload, lastPings); + // // console.log('pong received, latency', payload); + // emitter.emit('pong' as any, payload); + // }); + // this.emitter.on('ping', ) + } + + // syncClientMasterTime() { + // const requestedAt = new Date().getTime(); + // this.emitter.emit('clock', ) + // } } diff --git a/libs/movex/src/lib/ConnectionToMasterResources.ts b/libs/movex/src/lib/ConnectionToMasterResources.ts index a590a2a9..fa0688a0 100644 --- a/libs/movex/src/lib/ConnectionToMasterResources.ts +++ b/libs/movex/src/lib/ConnectionToMasterResources.ts @@ -1,4 +1,4 @@ -import type { +import { GetIOPayloadErrTypeFrom, GetIOPayloadOKTypeFrom, ResourceIdentifier, @@ -11,6 +11,8 @@ import type { IOEvents, MovexClient, SanitizedMovexClient, + objectOmit, + objectPick, } from 'movex-core-util'; import { invoke, @@ -39,10 +41,7 @@ export class ConnectionToMasterResources< }>(); private subscriberAddedEventPubsy = new Pubsy<{ - [key in `rid:${ResourceIdentifierStr}`]: Pick< - MovexClient, - 'id' | 'info' - >; + [key in `rid:${ResourceIdentifierStr}`]: SanitizedMovexClient; }>(); private subscriberRemovedEventPubsy = new Pubsy<{ @@ -113,7 +112,7 @@ export class ConnectionToMasterResources< this.subscriberAddedEventPubsy.publish( `rid:${toResourceIdentifierStr(p.rid)}`, - { id: p.client.id, info: p.client.info } + objectPick(p.client, ['clockOffset', 'id', 'info']) ); }; diff --git a/libs/movex/src/lib/MovexResource.ts b/libs/movex/src/lib/MovexResource.ts index 72f54fa7..d781d1de 100644 --- a/libs/movex/src/lib/MovexResource.ts +++ b/libs/movex/src/lib/MovexResource.ts @@ -149,33 +149,27 @@ export class MovexResource< p.actions ).checkedState; - logsy.log('Reconciliatory Actions Received', { + logsy.log('ReconciliatoryActions Received', { ...p, - actionsCount: p.actions.length, clientId: this.connectionToMaster.client.id, nextState, prevState, }); 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 + // When the checksums are different then re-sync. + // See https://github.com/movesthatmatter/movex/issues/8 resyncLocalState(); - // Here is where this happens!!! - - logsy.warn('Local and Final Master Checksum Mismatch', { + logsy.warn('ReconciliatoryActions Checksums Mismatch', { ...p, - nextState: nextState[1], + clientId: this.connectionToMaster.client.id, + prevState, + nextState, }); } logsy.groupEnd(); - - // p.actions.map(()) - // TODO: What should the reconciliatry actions do? Apply them all together and check at the end right? - // If the end result don't match the checkusm this is the place where it can reask the master for the new state! - // This is where that amazing logic lives :D }; this.unsubscribersByRid[toResourceIdentifierStr(rid)] = [ @@ -186,40 +180,15 @@ export class MovexResource< masterAction, onEmitMasterActionAck, }) => { - // const [_, nextLocalChecksumPreAck] = nextLocalCheckedState; - - // console.log('comes here sf???'); - this.connectionToMasterResources - // TODO: Left it here - // here's what needs to be added - // This emitter needs to get an extra flag saying that it is a masterAction - // A masterAction is a special type of action that gets set with values computed locally - // but the dispatcher waits for an ack with the master processed action and the next checksum to be applied locally! - // if the checksums don't match, it will do a state re-sync just like usually - // TODO: need to also check what's happening with private actions .emitAction(rid, masterAction || action) - .map(async (response) => { - console.log('response', response); - + .map((response) => { if (response.type === 'reconciliation') { onReconciliateActionsHandler(response); return; } - // Otherwise if it's type === 'ack' - - // const nextLocalChecksum = - // response.type === 'masterActionAck' - // ? onEmitMasterActionAck(response.nextCheckedAction) - // : nextLocalCheckedState[1]; - - // const masterChecksum = - // response.type === 'masterActionAck' - // ? response.nextCheckedAction.checksum - // : response.nextChecksum; - const nextChecksums = invoke(() => { if (response.type === 'masterActionAck') { return { @@ -236,18 +205,10 @@ export class MovexResource< // And the checksums are equal stop here if (nextChecksums.master === nextChecksums.local) { - console.log( - 'Movex REsource checksums are the same not going to resync' - ); - return; } - console.log( - 'Movex REsource checksums are not the same so I am going to resync' - ); - - // When the checksums are not the same, need to resync the state! + // When the checksums aren't the same, need to resync the state! // this is expensive and ideally doesn't happen too much. logsy.error(`Dispatch Ack Error: "Checksums MISMATCH"`, { @@ -257,34 +218,30 @@ export class MovexResource< nextLocalCheckedState, }); - await resyncLocalState() - .map((masterState) => { - logsy.info('Re-synched Response', { - masterState, - nextLocalCheckedState, - diff: deepObject.detailedDiff( - masterState, - nextLocalCheckedState - ), - }); - }) - .resolve(); + resyncLocalState(); }); } ), this.connectionToMasterResources.onFwdAction(rid, (p) => { - const prevState = resourceObservable.getCheckedState(); - - resourceObservable.reconciliateAction(p); - - const nextState = resourceObservable.getCheckedState(); - - logsy.info('Forwarded Action Received', { + logsy.group('FwdAction Received', { ...p, clientId: this.connectionToMaster.client.id, - prevState, - nextState, }); + + const result = resourceObservable.reconciliateAction(p); + + if (result.err) { + logsy.warn('FwdAction Checksums Mismatch', { + ...p, + clientId: this.connectionToMaster.client.id, + prevState: resourceObservable.getCheckedState(), + error: result.val, + }); + + resyncLocalState(); + } + + logsy.groupEnd(); }), this.connectionToMasterResources.onReconciliatoryActions( rid, diff --git a/libs/movex/src/lib/init.ts b/libs/movex/src/lib/init.ts index e1b42950..8a49602f 100644 --- a/libs/movex/src/lib/init.ts +++ b/libs/movex/src/lib/init.ts @@ -3,6 +3,7 @@ import { EmptyFn, invoke, MovexClientInfo, + MovexClientMasterClockOffset, SanitizedMovexClient, SocketIOEmitter, UnknownRecord, @@ -15,6 +16,7 @@ import type { } from 'movex-core-util'; import { MovexFromDefinition } from './MovexFromDefintion'; import { ConnectionToMaster } from './ConnectionToMaster'; +import { Ok } from 'ts-results'; // TODO: The ClientId ideally isn't given from here bu retrieved somehow else. hmm // Or no? @@ -79,9 +81,7 @@ export const initMovex = ( }); } - const onClientReadyHandler = ( - client: SanitizedMovexClient - ) => { + const onReadyHandler = (client: SanitizedMovexClient) => { config.onReady( new MovexFromDefinition( movexDefinition, @@ -90,10 +90,39 @@ export const initMovex = ( ); }; - emitter.on('onClientReady', onClientReadyHandler); + // emitter.on('pong', (payload) => { + // // lastPongs.push(payload); + // console.log('pong received, latency', payload); + // // console.log('pong received, latency', payload); + // }); + + // emitter.on('ping' as any, (x) => { + // emitter.emit('pong' as any, x); + // }); + + const onClockSyncHandler = ( + payload: Parameters['onClockSync']>[0], + acknowledge?: ( + p: ReturnType['onClockSync']> + ) => void + ) => { + const clientTime = new Date().getTime(); + console.log('on clock sync handler', { payload, acknowledge }); + + // Respond with the client time + acknowledge?.(new Ok(clientTime)); + }; + + emitter.on('onClockSync', onClockSyncHandler); + + unsubscribers.push(() => { + socket.off('onClockSync', onClockSyncHandler); + }); + + emitter.on('onReady', onReadyHandler); unsubscribers.push(() => { - socket.off('onClientReady', onClientReadyHandler); + socket.off('onReady', onReadyHandler); }); // Return the destroyer() diff --git a/package.json b/package.json index 8a96d5c9..9fee832a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movex", - "version": "0.1.6-23", + "version": "0.1.6-45", "license": "MIT", "author": { "name": "Gabriel C. Troia", @@ -25,12 +25,12 @@ "test": "nx run-many --all --target=test --skip-nx-cache", "dev-movex-docs": "npx nx serve movex-docs", "lint-all-libs": "npx nx run-many --parallel --target=lint --projects=movex-core-util,movex,movex-server,movex-service,movex-react,movex-vue --quiet", - "push-all-libs--locally": "cd dist/libs/movex && npm version pre && yalc push && cd -; cd dist/libs/movex-server && npm version pre && yalc push && cd -; cd dist/libs/movex-core-util && npm version pre && yalc push && cd -; cd dist/libs/movex-react && npm version pre && yalc push && cd -; cd dist/libs/movex-service && npm version pre && yalc push && cd -; cd dist/libs/movex-store && npm version pre && yalc push && cd -; cd dist/libs/movex-master && npm version pre && yalc push && cd -;", - "push-lib-locally--movex": "cd dist/libs/movex && npm version pre && yalc push && cd -", - "push-lib-locally--movex-react": "cd dist/libs/movex-react && npm version pre && yalc push && cd -", - "push-lib-locally--movex-server": "cd dist/libs/movex-server && npm version pre && yalc push && cd -", - "push-lib-locally--movex-master": "cd dist/libs/movex-master && npm version pre && yalc push && cd -", - "push-lib-locally--movex-store": "cd dist/libs/movex-store && npm version pre && yalc push && cd -", + "push-all-libs--locally": "cd dist/libs/movex && yalc push && cd -; cd dist/libs/movex-server && yalc push && cd -; cd dist/libs/movex-core-util && yalc push && cd -; cd dist/libs/movex-react && yalc push && cd -; cd dist/libs/movex-service && yalc push && cd -; cd dist/libs/movex-store && yalc push && cd -; cd dist/libs/movex-master && yalc push && cd -;", + "push-lib-locally--movex": "cd dist/libs/movex && yalc push && cd -", + "push-lib-locally--movex-react": "cd dist/libs/movex-react && yalc push && cd -", + "push-lib-locally--movex-server": "cd dist/libs/movex-server && yalc push && cd -", + "push-lib-locally--movex-master": "cd dist/libs/movex-master && yalc push && cd -", + "push-lib-locally--movex-store": "cd dist/libs/movex-store && yalc push && cd -", "publish-movex": "cd dist/libs/movex && npm publish && cd -;", "publish-movex-core-util": "cd dist/libs/movex-core-util && npm publish && cd -", "publish-movex-server": "cd dist/libs/movex-server && npm publish && cd -",