Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(presence-tracker): Use presence package #23020

Merged
merged 87 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
aaf918b
wip
tylerbutler Nov 6, 2024
a2a55e4
wip
tylerbutler Nov 6, 2024
39741a3
updates
tylerbutler Nov 6, 2024
c3a2b57
sort of works
tylerbutler Nov 6, 2024
5606c11
seems pretty good
tylerbutler Nov 7, 2024
7ce14c6
updates
tylerbutler Nov 7, 2024
5fc6ac7
cleanup
tylerbutler Nov 7, 2024
5f5aa4b
cleanup
tylerbutler Nov 7, 2024
2219cba
Merge branch 'main' into presence-tracker-revamp
tylerbutler Nov 7, 2024
c118fcd
wip
tylerbutler Nov 7, 2024
3bae7a4
revert removed files
tylerbutler Nov 7, 2024
dc9c829
mousetracker with presence workspace
tylerbutler Nov 7, 2024
ae317ab
focustracker with presence
tylerbutler Nov 7, 2024
3cbe492
cleanup
tylerbutler Nov 7, 2024
c635bf0
brng back the trackers
tylerbutler Nov 8, 2024
0d6476d
Merge branch 'main' into presence-tracker-revamp
tylerbutler Nov 8, 2024
a1649d3
breaking change
tylerbutler Nov 8, 2024
a5de697
Merge branch 'main' into presence-tracker-revamp
tylerbutler Nov 11, 2024
3a7f7cd
cleanup
tylerbutler Nov 11, 2024
069a052
use clientValues
tylerbutler Nov 11, 2024
a43de27
cleanup
tylerbutler Nov 11, 2024
d7bfbc0
sort of working
tylerbutler Nov 12, 2024
cd7ed6f
Merge branch 'main' into presence-tracker-revamp
tylerbutler Nov 12, 2024
9237db8
test
tylerbutler Nov 12, 2024
8257169
still sort of works once a second user joins
tylerbutler Nov 12, 2024
3320851
rm unused files
tylerbutler Nov 12, 2024
4b2e178
Merge branch 'main' into presence-tracker-revamp
tylerbutler Nov 27, 2024
116221f
package rename and lint config
tylerbutler Nov 27, 2024
f5fb796
add controllable latency
tylerbutler Nov 27, 2024
4b0dd7d
naming
tylerbutler Nov 27, 2024
0e5f297
Merge branch 'main' into presence-tracker-revamp
tylerbutler Dec 10, 2024
90cbc36
debugging
tylerbutler Dec 11, 2024
d68df9c
cleanup
tylerbutler Dec 11, 2024
0361460
Merge branch 'main' into presence-tracker-revamp
tylerbutler Dec 11, 2024
0f245d5
policy
tylerbutler Dec 11, 2024
b0bdf4a
lockfile
tylerbutler Dec 11, 2024
cb1b48d
feedback
tylerbutler Dec 11, 2024
b7615e5
update test config
tylerbutler Dec 11, 2024
38de027
test updates
tylerbutler Dec 12, 2024
b0650b4
multi-client tests
tylerbutler Dec 12, 2024
34345d0
Merge branch 'main' into presence-tracker-revamp
tylerbutler Dec 12, 2024
dfc37e4
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 6, 2025
7b594b9
run tinylicious for jest tests
tylerbutler Jan 6, 2025
7406ad5
azure function comments
tylerbutler Jan 7, 2025
e133d44
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 7, 2025
1277da7
use tinylicious exclsively
tylerbutler Jan 7, 2025
2683c70
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 13, 2025
3018bbf
fix policy
tylerbutler Jan 13, 2025
4b4647e
fixes
tylerbutler Jan 13, 2025
97a5dbe
feedback
tylerbutler Jan 13, 2025
33668d4
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 13, 2025
c7f5cdc
remove unused script
tylerbutler Jan 13, 2025
5adadea
clean up comments
tylerbutler Jan 13, 2025
f3b6667
Apply suggestions from code review
tylerbutler Jan 13, 2025
9427552
build failure
tylerbutler Jan 13, 2025
ed8bfca
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 14, 2025
5a155e8
feedback
tylerbutler Jan 14, 2025
62f14da
test feedback
tylerbutler Jan 14, 2025
54485e6
try removing the --ci flag
tylerbutler Jan 14, 2025
a52bba2
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 14, 2025
c5b9744
feedback
tylerbutler Jan 14, 2025
bcd24eb
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 15, 2025
7c46f35
feedback
tylerbutler Jan 15, 2025
6940986
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 15, 2025
9489b9b
fix
tylerbutler Jan 15, 2025
6051964
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 15, 2025
de56c92
updates
tylerbutler Jan 15, 2025
93e7767
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 15, 2025
a2d1cfd
Revert "updates"
tylerbutler Jan 15, 2025
e30c829
close browser after all tests to prevent hang
tylerbutler Jan 15, 2025
382b9b7
policy
tylerbutler Jan 15, 2025
61406a0
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 16, 2025
14b2c06
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 16, 2025
e83c8dc
feedback
tylerbutler Jan 16, 2025
0fde8ba
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 16, 2025
c490da8
feedback
tylerbutler Jan 17, 2025
5ce5210
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 22, 2025
aefd485
docs
tylerbutler Jan 23, 2025
e5a0415
tests
tylerbutler Jan 23, 2025
197cd08
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 23, 2025
31f0b19
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 23, 2025
1a3e55f
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 23, 2025
fb85e04
feedback
tylerbutler Jan 23, 2025
e35b898
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 23, 2025
f3d4ec8
rm any
tylerbutler Jan 23, 2025
3dde146
feedback
tylerbutler Jan 23, 2025
596371a
Merge branch 'main' into presence-tracker-revamp
tylerbutler Jan 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/apps/presence-tracker/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
extends: [
require.resolve("@fluidframework/eslint-config-fluid/minimal-deprecated"),
"prettier",
"../../.eslintrc.cjs",
],
rules: {
"@fluid-internal/fluid/no-unchecked-record-access": "warn",
Expand Down
30 changes: 26 additions & 4 deletions examples/apps/presence-tracker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@

**_This demo is a work-in-progress_**

**Presence Tracker** is an example that demonstrates how transient state of audience members can be tracked among other audience members using signals. It does so using fluid-framework's `FluidContainer`, `IServiceAudience`, and `Signaler`.
**Presence Tracker** is an example that demonstrates how the @fluidframework/presence package can be used to share data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I always recommend line-breaking along sentence boundaries for ease of reviewing future content changes.

https://github.com/microsoft/FluidFramework/wiki/Markdown-Best-Practices#line-breaks-along-sentence-boundaries

that does not require persistence between clients. The presence APIs are especially suited to data like real-time cursor
positions, mouse pointer positions, and object selection.

This implementation visualizes the Container in a standalone application, rather than using the webpack-fluid-loader environment that most of our examples use. This implementation relies on [Tinylicious](/server/routerlicious/packages/tinylicious), so there are a few extra steps to get started. We bring our own view that we will bind to the data in the container.
In this example, presence is used to share both mouse position within the application window and the focus state of the
application.

<!-- AUTO-GENERATED-CONTENT:START (EXAMPLE_APP_README_HEADER:usesTinylicious=TRUE) -->
This implementation visualizes the Container in a standalone application, rather than using the `webpack-fluid-loader`
environment that many of our examples use. This implementation relies on
[Tinylicious](/server/routerlicious/packages/tinylicious) as the Fluid service, so it is invoked in the background
automatically when running the scripts . We bring our
own view that we bind to the data in the container.

<!-- AUTO-GENERATED-CONTENT:START (EXAMPLE_APP_README_HEADER:usesTinylicious=FALSE) -->

<!-- prettier-ignore-start -->
<!-- NOTE: This section is automatically generated using @fluid-tools/markdown-magic. Do not update these generated contents directly. -->
Expand All @@ -19,13 +28,26 @@ You can run this example using the following steps:
1. Run `pnpm install` and `pnpm run build:fast --nolint` from the `FluidFramework` root directory.
- For an even faster build, you can add the package name to the build command, like this:
`pnpm run build:fast --nolint @fluid-example/presence-tracker`
1. In a separate terminal, start a Tinylicious server by following the instructions in [Tinylicious](https://github.com/microsoft/FluidFramework/tree/main/server/routerlicious/packages/tinylicious).
1. Run `pnpm start` from this directory and open <http://localhost:8080> in a web browser to see the app running.

<!-- prettier-ignore-end -->

<!-- AUTO-GENERATED-CONTENT:END -->

## Tests

The tests in this example require that tinylicious be running. The tests execute against the "real app" running in Webpack's
dev server; tinylicious is triggered in the background as part of the test invocation.

### Multiple browser clients

The presence APIs do not broadcast state unless multiple clients are connected, so it is necessary to run multiple
clients to test that the presence data is correctly being exchanged between clients. The tests do this by creating
multiple puppeteer clients and pointing them to the same URL. This partially works. However, the most crucial test,
which verifies that changes from one client are reflected on the other, does not yet pass, and is thus skipped. See
AB#28502 (https://dev.azure.com/fluidframework/internal/_workitems/edit/28502)
for more details.

<!-- AUTO-GENERATED-CONTENT:START (README_FOOTER) -->

<!-- prettier-ignore-start -->
Expand Down
2 changes: 1 addition & 1 deletion examples/apps/presence-tracker/jest-puppeteer.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

module.exports = {
server: {
command: `npm run start:client:test -- --no-hot --no-live-reload --port ${process.env["PORT"]}`,
command: `npm run start:client -- --no-hot --no-live-reload --port ${process.env["PORT"]}`,
port: process.env["PORT"],
launchTimeout: 10000,
usedPortAction: "error",
Expand Down
16 changes: 10 additions & 6 deletions examples/apps/presence-tracker/package.json
tylerbutler marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@fluid-example/presence-tracker",
"version": "2.21.0",
"private": true,
"description": "Example Data Object that tracks page focus for Audience members using signals.",
"description": "Example application that tracks page focus and mouse position using the Fluid Framework presence features.",
"homepage": "https://fluidframework.com",
"repository": {
"type": "git",
Expand All @@ -28,11 +28,13 @@
"lint": "fluid-build . --task lint",
"lint:fix": "fluid-build . --task eslint:fix --task format",
"prepack": "npm run webpack",
"start": "webpack serve",
"start:client:test": "webpack serve --config webpack.test.cjs",
jason-ha marked this conversation as resolved.
Show resolved Hide resolved
"start": "start-server-and-test start:tinylicious 7070 start:client",
"start:client": "webpack serve",
"start:tinylicious": "tinylicious",
"test": "npm run test:jest",
"test:jest": "jest --ci",
"test:jest:verbose": "cross-env FLUID_TEST_VERBOSE=1 jest --ci --passWithNoTests",
"test:jest": "cross-env logger__level=crit start-server-and-test tinylicious 7070 test:jest:run",
"test:jest:run": "jest --ci --detectOpenHandles",
"test:jest:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:jest:verbose",
"tinylicious": "tinylicious",
"webpack": "webpack --env production",
"webpack:dev": "webpack --env development"
Expand All @@ -41,13 +43,14 @@
"@fluid-example/example-utils": "workspace:~",
"@fluid-experimental/data-objects": "workspace:~",
"@fluid-internal/client-utils": "workspace:~",
"@fluidframework/azure-client": "workspace:~",
"@fluidframework/container-definitions": "workspace:~",
"@fluidframework/container-runtime-definitions": "workspace:~",
"@fluidframework/core-interfaces": "workspace:~",
"@fluidframework/driver-definitions": "workspace:~",
"@fluidframework/fluid-static": "workspace:~",
"@fluidframework/presence": "workspace:~",
"@fluidframework/runtime-utils": "workspace:~",
"@fluidframework/tinylicious-client": "workspace:~",
"fluid-framework": "workspace:~",
"process": "^0.11.10"
},
Expand All @@ -73,6 +76,7 @@
"puppeteer": "^23.6.0",
"rimraf": "^4.4.0",
"source-map-loader": "^5.0.0",
"start-server-and-test": "^2.0.3",
"tinylicious": "^5.0.0",
"ts-jest": "^29.1.1",
"ts-loader": "^9.5.1",
Expand Down
22 changes: 0 additions & 22 deletions examples/apps/presence-tracker/src/Audience.ts

This file was deleted.

159 changes: 72 additions & 87 deletions examples/apps/presence-tracker/src/FocusTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,120 +3,105 @@
* Licensed under the MIT License.
*/

import { ISignaler } from "@fluid-experimental/data-objects";
import { TypedEventEmitter } from "@fluid-internal/client-utils";
import type { IAzureAudience } from "@fluidframework/azure-client";
import { IContainer } from "@fluidframework/container-definitions/internal";
import { IEvent } from "@fluidframework/core-interfaces";
import { IMember } from "fluid-framework";
import type {
IPresence,
ISessionClient,
LatestValueManager,
PresenceStates,
} from "@fluidframework/presence/alpha";
import { Latest, SessionClientStatus } from "@fluidframework/presence/alpha";

export interface IFocusTrackerEvents extends IEvent {
(event: "focusChanged", listener: () => void): void;
/**
* IFocusState is the data that individual session clients share via presence.
*/
export interface IFocusState {
readonly hasFocus: boolean;
}

export interface IFocusSignalPayload {
userId: string;
focus: boolean;
/**
* Definitions of the events that the FocusTracker raises.
*/
export interface IFocusTrackerEvents extends IEvent {
/**
* The focusChanged event is emitted any time the FocusTracker detects a change in focus in any client, local or
* remote.
*/
(event: "focusChanged", listener: (focusState: IFocusState) => void): void;
}

/**
* The FocusTracker class tracks the focus state of all connected sessions using the Fluid Framework presence features.
* Focus state is tracked automatically by the class instance. As the focus state of connected sessions change, the
* FocusTracker emits a "focusChanged" event
*/
export class FocusTracker extends TypedEventEmitter<IFocusTrackerEvents> {
private static readonly focusSignalType = "changedFocus";
private static readonly focusRequestType = "focusRequest";

/**
* Local map of focus status for clients
*
* @example
*
* ```typescript
* Map<userId, Map<clientid, hasFocus>>
* ```
* A value manager that tracks the latest focus state of connected session clients.
*/
private readonly focusMap = new Map<string, Map<string, boolean>>();

private readonly onFocusSignalFn = (clientId: string, payload: IFocusSignalPayload) => {
const userId: string = payload.userId;
const hasFocus: boolean = payload.focus;

let clientIdMap = this.focusMap.get(userId);
if (clientIdMap === undefined) {
clientIdMap = new Map<string, boolean>();
this.focusMap.set(userId, clientIdMap);
}
clientIdMap.set(clientId, hasFocus);
this.emit("focusChanged");
};
private readonly focus: LatestValueManager<IFocusState>;

constructor(
container: IContainer,
public readonly audience: IAzureAudience,
private readonly signaler: ISignaler,
private readonly presence: IPresence,

/**
* A states workspace that the FocusTracker will use to share focus states with other session clients.
*/
// eslint-disable-next-line @typescript-eslint/ban-types -- empty object is the correct typing
readonly statesWorkspace: PresenceStates<{}>,
) {
super();

this.audience.on("memberRemoved", (clientId: string, member: IMember) => {
const focusClientIdMap = this.focusMap.get(member.id);
if (focusClientIdMap !== undefined) {
focusClientIdMap.delete(clientId);
if (focusClientIdMap.size === 0) {
this.focusMap.delete(member.id);
}
}
this.emit("focusChanged");
});

this.signaler.on("error", (error) => {
this.emit("error", error);
});
this.signaler.onSignal(
FocusTracker.focusSignalType,
(clientId: string, local: boolean, payload: IFocusSignalPayload) => {
this.onFocusSignalFn(clientId, payload);
},
// Create a Latest value manager to track the focus state. The value is initialized with current focus state of the
// window.
statesWorkspace.add(
"focus",
Latest<IFocusState>({ hasFocus: window.document.hasFocus() }),
);

this.signaler.onSignal(FocusTracker.focusRequestType, () => {
this.sendFocusSignal(document.hasFocus());
// Save a reference to the value manager for easy access within the FocusTracker.
this.focus = statesWorkspace.props.focus;

// When the focus value manager is updated, the FocusTracker should emit the focusChanged event.
this.focus.events.on("updated", ({ client, value }) => {
this.emit("focusChanged", this.focus.local);
});

// Listen to the local focus and blur events. On each event, update the local focus state in the value manager, then
// emit the focusChanged event with the local data.
window.addEventListener("focus", () => {
this.sendFocusSignal(true);
this.focus.local = {
hasFocus: true,
};
this.emit("focusChanged", this.focus.local);
});
window.addEventListener("blur", () => {
this.sendFocusSignal(false);
});
container.on("connected", () => {
this.signaler.submitSignal(FocusTracker.focusRequestType);
this.focus.local = {
hasFocus: false,
};
this.emit("focusChanged", this.focus.local);
});
this.signaler.submitSignal(FocusTracker.focusRequestType);
}

/**
* Alert all connected clients that there has been a change to a client's focus
* A map of session clients to focus status.
*/
private sendFocusSignal(hasFocus: boolean) {
this.signaler.submitSignal(FocusTracker.focusSignalType, {
userId: this.audience.getMyself()?.id,
focus: hasFocus,
});
}
public getFocusPresences(): Map<ISessionClient, boolean> {
const statuses: Map<ISessionClient, boolean> = new Map();

public getFocusPresences(): Map<string, boolean> {
const statuses: Map<string, boolean> = new Map<string, boolean>();
this.audience.getMembers().forEach((member, userId) => {
member.connections.forEach((connection) => {
const focus = this.getFocusPresenceForUser(userId, connection.id);
if (focus !== undefined) {
statuses.set(member.name, focus);
}
});
});
return statuses;
}
// Include the local client in the map because this is used to render a
// dashboard of all connected clients.
const currentClient = this.presence.getMyself();
statuses.set(currentClient, this.focus.local.hasFocus);

/**
* Returns focus status of specified client
*/
public getFocusPresenceForUser(userId: string, clientId: string): boolean | undefined {
return this.focusMap.get(userId)?.get(clientId);
for (const { client, value } of this.focus.clientValues()) {
if (client.getConnectionStatus() === SessionClientStatus.Connected) {
const { hasFocus } = value;
statuses.set(client, hasFocus);
}
}

return statuses;
}
}
Loading
Loading