Skip to content

Commit

Permalink
Merge pull request #223 from movesthatmatter/fix-state-transformer
Browse files Browse the repository at this point in the history
Fix state transformer
  • Loading branch information
GabrielCTroia authored Sep 26, 2024
2 parents d26db7d + b28afd5 commit f745354
Show file tree
Hide file tree
Showing 42 changed files with 809 additions and 609 deletions.
4 changes: 2 additions & 2 deletions apps/movex-demo/pages/chat/[chatId].tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -20,7 +20,7 @@ const ChatSystem: React.FC<Props> = () => {

// 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;
Expand Down
2 changes: 1 addition & 1 deletion libs/movex-core-util/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
9 changes: 5 additions & 4 deletions libs/movex-core-util/src/lib/EventEmitter/IOEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
MovexClient,
ResourceIdentifier,
SanitizedMovexClient,
MovexClientMasterClockOffset,
} from '../core-types';

export type IOEvents<
Expand Down Expand Up @@ -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<number, unknown>; // acknowledges the client timestamp

onFwdAction: (
payload: {
Expand All @@ -106,7 +106,7 @@ export type IOEvents<
) => IOPayloadResult<void, unknown>;
onResourceSubscriberAdded: (p: {
rid: ResourceIdentifier<TResourceType>;
client: Pick<MovexClient, 'id' | 'info'>;
client: SanitizedMovexClient;
// clientId: MovexClient['id'];
}) => IOPayloadResult<
void,
Expand All @@ -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<void, unknown>;
pong: () => IOPayloadResult<void, unknown>;
};
20 changes: 17 additions & 3 deletions libs/movex-core-util/src/lib/EventEmitter/ScketIOEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ export class SocketIOEmitter<
TSocketIO extends SocketIO = ServerSocket | ClientSocket
> implements EventEmitter<TEventMap>
{
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<E extends keyof TEventMap>(
Expand Down Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion libs/movex-core-util/src/lib/Logsy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,3 @@ class Logsy {
}

export const globalLogsy = new Logsy();

11 changes: 10 additions & 1 deletion libs/movex-core-util/src/lib/core-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,17 @@ export type MovexClient<Info extends MovexClientInfo = UnknownRecord> = {
>;
};

export type MovexClientMasterClockOffset = number;

export type SanitizedMovexClient<Info extends UnknownRecord = UnknownRecord> =
Pick<MovexClient<Info>, 'id' | 'info'>;
Pick<MovexClient<Info>, '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<TResourceType extends string> = {
resourceType: TResourceType;
Expand Down
6 changes: 4 additions & 2 deletions libs/movex-core-util/src/lib/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,18 @@ 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<S = any, A extends AnyAction = AnyAction> = ((
state: S,
action: A
) => S) & {
$canReconcileState?: (s: S) => boolean;
$transformState?: (s: S, remoteContext: MovexRemoteContext) => S;
$transformState?: (s: S, masterContext: MovexMasterContext) => S;
};

export type GetReducerState<
Expand Down
2 changes: 1 addition & 1 deletion libs/movex-master/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
46 changes: 44 additions & 2 deletions libs/movex-master/src/lib/ConnectionToClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IOEvents<TState, TAction, TResourceType>>,
public client: SanitizedMovexClient<TClientInfo>
) {}

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,
// });
});
}
}
64 changes: 47 additions & 17 deletions libs/movex-master/src/lib/MovexMasterResource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });

Expand All @@ -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);
Expand All @@ -45,19 +50,23 @@ test('applies public action', async () => {
})
);

const mockMasterContext = createMasterContext({ requestAt: 123 });

const clientAId = 'clienA';

const action: GetReducerAction<typeof counterReducer> = {
type: 'increment',
};

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({
Expand Down Expand Up @@ -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<typeof counterReducer> = {
Expand All @@ -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({
Expand Down Expand Up @@ -156,6 +174,8 @@ test('applies private action UNTIL Reconciliation', async () => {
})
);

const mockMasterContext = createMasterContext({ requestAt: 123 });

const whitePlayer = 'white';
const blackPlayer = 'black';

Expand All @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
Loading

0 comments on commit f745354

Please sign in to comment.