Skip to content

feat(client): [internal] container extensions and presence use #24399

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c207f58
feat(client-container-runtime): path-based address signal routing
jason-ha Apr 16, 2025
8b219d7
improvement(client): use generic for signal content
jason-ha Apr 13, 2025
1872d87
feat(client): internal container extensions interface
jason-ha Apr 16, 2025
62d3fa6
improvement(client): enhance and tighten types for container extensio…
jason-ha Apr 17, 2025
b1f8232
improvement(client): distinguish raw incoming signals
jason-ha Apr 17, 2025
112853b
feat(client): implement internal container extensions support
jason-ha Apr 17, 2025
11c4a19
docs(client-container-definitions): comments for `IRuntimeInternal`
jason-ha Apr 25, 2025
f2e7304
improvement(client-container-loader): replace `as` with typed target
jason-ha Apr 25, 2025
beb23e2
improvement(client-example): use URLSearchParams
jason-ha Apr 25, 2025
ce07bf4
merge: 'main' into presence/infra/support-path-based-address-routing-…
jason-ha Apr 25, 2025
a7e847b
merge: 'main' into presence/infra/support-path-based-address-routing-…
jason-ha May 20, 2025
2f3b12e
refactor(client): remove ContainerExtensionStore from container (load…
jason-ha May 22, 2025
b0d8db2
feat(client): declarative Presence support
jason-ha May 22, 2025
1c4be86
style(client-container-runtime): restore implements ordering
jason-ha May 22, 2025
98b0e92
revert(client-container-runtime): remove dark pathBasedAddressing run…
jason-ha May 22, 2025
f1c3bea
docs(client-presence): `getPresence` now supported
jason-ha May 22, 2025
d92548a
style(client): renames and comments
jason-ha May 23, 2025
a6ad229
refactor(client): generics ordering
jason-ha May 23, 2025
46fa5bf
merge: 'main' into presence/infra/support-path-based-address-routing-…
jason-ha May 23, 2025
5b1ed99
refactor(client-container-runtime): simplify addressed path support
jason-ha May 23, 2025
41cfcc6
build(client-presence): remove unused container-loader dep
jason-ha May 23, 2025
ce572d1
test(client-examples): cleanup deprecated presence init
jason-ha May 23, 2025
1d0fddd
fix(client-fluid-static): fix breaks from merge
jason-ha May 23, 2025
75001bd
merge: 'main' into presence/infra/support-path-based-address-routing-…
jason-ha May 23, 2025
7ec1564
fix(client-example): correct for fluid-static changes
jason-ha May 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
7 changes: 7 additions & 0 deletions .changeset/public-houses-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@fluidframework/presence": minor
"__section": feature
---
"getPresence(container: IFluidContainer): Presence" now supported

`getPresence` is now supported and may be used to directly acquire `Presence` instead of using `ExperimentalPresenceManager` in container schema and calling `getPresenceViaDataObject`. (Both of those are now deprecated.)
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"mocharc",
"multinomial",
"nonfinite",
"privateremarks",
"pseudorandomly",
"reconnections",
"Routerlicious",
Expand Down
19 changes: 5 additions & 14 deletions docs/docs/build/presence.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ sidebar_postition: 10

## Overview

We are introducting a new way to power your ephemeral experiences wth Fluid. Introducing the new Presence APIs (currently in alpha) that provide session-focused utilities for lightweight data sharing and messaging.

We are introducing a new way to power your ephemeral experiences with Fluid. Introducing the new Presence APIs (currently in alpha) that provide session-focused utilities for lightweight data sharing and messaging.
Collaborative features typically rely on each user maintaining their own temporary state, which is subsequently shared with others. For example, in applications featuring multiplayer cursors, the cursor position of each user signifies their state. This state can be further utilized for various purposes such as indicating typing activity or displaying a user's current selection. This concept is referred to as _presence_.

By leveraging this shared state, applications can provide a seamless and interactive collaborative experience, ensuring that users are always aware of each other's actions and selections in real-time.
Expand Down Expand Up @@ -57,21 +56,13 @@ Notifications are special case where no data is retained during a session and al

## Onboarding

While this package is developing as experimental and other Fluid Framework internals are being updated to accommodate it, a temporary Shared Object must be added within container to gain access.
To access Presence APIs, use `getPresence()` with any `IFluidContainer`.

```typescript
import {
getPresenceViaDataObject,
ExperimentalPresenceManager,
} from "@fluidframework/presence/alpha";

const containerSchema = {
initialObjects: {
presence: ExperimentalPresenceManager,
},
} satisfies ContainerSchema;
import { getPresence } from "@fluidframework/presence/alpha";

const presence = await getPresenceViaDataObject(container.initialObjects.presence);
function usePresence(container: IFluidContainer): void {
const presence = await getPresence(container);
```

## Limitations
Expand Down
4 changes: 2 additions & 2 deletions examples/apps/ai-collab/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

"use client";

import { getPresenceViaDataObject } from "@fluidframework/presence/alpha";
import { getPresence } from "@fluidframework/presence/alpha";
import {
Box,
Button,
Expand Down Expand Up @@ -63,7 +63,7 @@ export default function TasksListPage(): JSX.Element {
const _treeView = fluidContainer.initialObjects.appState.viewWith(TREE_CONFIGURATION);
setTreeView(_treeView);

const presence = getPresenceViaDataObject(fluidContainer.initialObjects.presence);
const presence = getPresence(fluidContainer);
setPresenceManagerContext(new PresenceManager(presence));
return { sharedTree: _treeView };
},
Expand Down
6 changes: 0 additions & 6 deletions examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Licensed under the MIT License.
*/

import { ExperimentalPresenceManager } from "@fluidframework/presence/alpha";
import { Tree, type TreeNode, TreeViewConfiguration } from "@fluidframework/tree";
import { SchemaFactoryAlpha } from "@fluidframework/tree/alpha";
import { SharedTree } from "fluid-framework";
Expand Down Expand Up @@ -202,11 +201,6 @@ export const INITIAL_APP_STATE = {
export const CONTAINER_SCHEMA = {
initialObjects: {
appState: SharedTree,
/**
* A Presence Manager object temporarily needs to be placed within container schema
* https://github.com/microsoft/FluidFramework/blob/main/packages/framework/presence/README.md#onboarding
* */
presence: ExperimentalPresenceManager,
},
};

Expand Down
21 changes: 16 additions & 5 deletions examples/apps/presence-tracker/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
*/

import {
getPresence,
// eslint-disable-next-line import/no-deprecated
getPresenceViaDataObject,
// eslint-disable-next-line import/no-deprecated
ExperimentalPresenceManager,
} from "@fluidframework/presence/alpha";
import { TinyliciousClient } from "@fluidframework/tinylicious-client";
Expand All @@ -16,11 +19,15 @@ import { initializeReactions } from "./reactions.js";
import { renderControlPanel, renderFocusPresence, renderMousePresence } from "./view.js";

// Define the schema of the Fluid container.
// This example uses the presence features only, so only that data object is added.
// This example uses the presence features only, so no data object is required.
// But the old experimental presence data object is used to check that old path still works.
// Besides initialObjects is not currently allowed to be empty.
// That version of presence is compatible with all 2.x runtimes. Long-term support without
// data object requires 2.41 or later.
const containerSchema = {
initialObjects: {
// A Presence Manager object temporarily needs to be placed within container schema
// https://github.com/microsoft/FluidFramework/blob/main/packages/framework/presence/README.md#onboarding
// Optional Presence Manager object placed within container schema for experimental presence access
// eslint-disable-next-line import/no-deprecated
presence: ExperimentalPresenceManager,
},
} satisfies ContainerSchema;
Expand Down Expand Up @@ -56,8 +63,12 @@ async function start() {
({ container } = await client.getContainer(id, containerSchema, "2"));
}

// Retrieve a reference to the presence APIs via the data object.
const presence = getPresenceViaDataObject(container.initialObjects.presence);
const useDataObject = new URLSearchParams(location.search).has("useDataObject");
const presence = useDataObject
? // Retrieve a reference to the presence APIs via the data object.
// eslint-disable-next-line import/no-deprecated
getPresenceViaDataObject(container.initialObjects.presence)
: getPresence(container);

// Get the states workspace for the tracker data. This workspace will be created if it doesn't exist.
// We create it with no states; we will pass the workspace to the Mouse and Focus trackers, and they will create value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import {
} from "@fluidframework/azure-client";
import { createDevtoolsLogger, initializeDevtools } from "@fluidframework/devtools/beta";
import { ISharedMap, IValueChanged, SharedMap } from "@fluidframework/map/legacy";
import {
getPresenceViaDataObject,
ExperimentalPresenceManager,
} from "@fluidframework/presence/alpha";
import { getPresence } from "@fluidframework/presence/alpha";
import { createChildLogger } from "@fluidframework/telemetry-utils/legacy";
// eslint-disable-next-line import/no-internal-modules -- #26985: `test-runtime-utils` internal used in example
import { InsecureTokenProvider } from "@fluidframework/test-runtime-utils/internal";
Expand Down Expand Up @@ -76,9 +73,6 @@ const containerSchema = {
/* [id]: DataObject */
map1: SharedMap,
map2: SharedMap,
// A Presence Manager object temporarily needs to be placed within container schema
// https://github.com/microsoft/FluidFramework/blob/main/packages/framework/presence/README.md#onboarding
presence: ExperimentalPresenceManager,
},
} satisfies ContainerSchema;
type DiceRollerContainerSchema = typeof containerSchema;
Expand Down Expand Up @@ -182,7 +176,7 @@ async function start(): Promise<void> {

// Biome insist on no semicolon - https://dev.azure.com/fluidframework/internal/_workitems/edit/9083
const lastRoll: { die1?: DieValue; die2?: DieValue } = {};
const presence = getPresenceViaDataObject(container.initialObjects.presence);
const presence = getPresence(container);
const states = buildDicePresence(presence).states;

// Initialize Devtools
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import {
IRuntimeFactory,
} from "@fluidframework/container-definitions/legacy";
import { Loader } from "@fluidframework/container-loader/legacy";
// eslint-disable-next-line import/no-internal-modules -- #26986: `fluid-static` internal used in examples
import { createDOProviderContainerRuntimeFactory } from "@fluidframework/fluid-static/internal";
import {
createDOProviderContainerRuntimeFactory,
createFluidContainer,
// eslint-disable-next-line import/no-internal-modules -- #26986: `fluid-static` internal used in examples
} from "@fluidframework/fluid-static/internal";
// eslint-disable-next-line import/no-internal-modules -- #26987: `local-driver` internal used in examples
import { LocalSessionStorageDbFactory } from "@fluidframework/local-driver/internal";
import {
Expand Down Expand Up @@ -119,8 +122,7 @@ async function createContainerAndRenderInElement(
);

// Get the Default Object from the Container
const fluidContainer =
(await container.getEntryPoint()) as IFluidContainer<TestContainerSchema>;
const fluidContainer = await createFluidContainer<TestContainerSchema>({ container });
if (createNewFlag) {
await initializeNewContainer(fluidContainer);
await attach?.();
Expand Down
1 change: 1 addition & 0 deletions packages/common/container-definitions/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export enum AttachState {
/**
* The IRuntime represents an instantiation of a code package within a Container.
* Primarily held by the ContainerContext to be able to interact with the running instance of the Container.
*
* @legacy
* @alpha
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,12 @@ export type TelemetryBaseEventPropertyType = string | number | boolean | undefin
// @public
export type TransformedEvent<TThis, E, A extends any[]> = (event: E, listener: (...args: ReplaceIEventThisPlaceHolder<A, TThis>) => void) => TThis;

// @alpha @legacy
export interface TypedMessage {
content: unknown;
type: string;
}

// (No @packageDocumentation comment for this package)

```
43 changes: 43 additions & 0 deletions packages/common/core-interfaces/src/brandedType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

/**
* Base branded type which can be used to annotate other type.
*
* @remarks
* To use derive another class declaration and ideally add additional private
* properties to further distinguish the type.
*
* Since branded types are not real value types, they will always need to be
* created using `as` syntax and very often `as unknown` first.
*
* This class should never exist at runtime, so it is only declared.
*
* @sealed
* @internal
*/
export declare class BrandedType<out Brand> {
/**
* Compile time only marker to make type checking more strict.
* This method will not exist at runtime and accessing it is invalid.
*
* @privateRemarks
* `Brand` is used as the return type of a method rather than a simple
* readonly property as this allows types with two brands to be
* intersected without getting `never`.
* The method takes in `never` to help emphasize that it's not callable.
*/
protected readonly brand: (dummy: never) => Brand;

protected constructor();

/**
* Since this class is a compile time only type brand, `instanceof` will
* never work with it. * This `Symbol.hasInstance` implementation ensures
* that `instanceof` will error if used, and in TypeScript 5.3 and newer
* will produce a compile time error if used.
*/
public static [Symbol.hasInstance](value: never): value is never;
}
21 changes: 14 additions & 7 deletions packages/common/core-interfaces/src/exposedInternalUtilityTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,20 +535,27 @@ export namespace InternalUtilityTypes {
*/
export type IsExactlyObject<T extends object> = IsSameType<T, object>;

/**
* Any Record type.
*
* @system
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- `any` for property types is required to avoid "Index signature for type 'string' is missing in type" in some outside `FlattenIntersection` uses.
export type AnyRecord = Record<keyof any, any>;

/**
* Creates a simple object type from an intersection of multiple.
* @privateRemarks
* `T extends Record` within the implementation encourages tsc to process
* `T extends AnyRecord` within the implementation encourages tsc to process
* intersections within unions.
*
* @system
*/
export type FlattenIntersection<T extends Record<string | number | symbol, unknown>> =
T extends Record<string | number | symbol, unknown>
? {
[K in keyof T]: T[K];
}
: T;
export type FlattenIntersection<T extends AnyRecord> = T extends AnyRecord
? {
[K in keyof T]: T[K];
}
: T;

/**
* Extracts Function portion from an intersection (&) type returning
Expand Down
4 changes: 3 additions & 1 deletion packages/common/core-interfaces/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Licensed under the MIT License.
*/

export type { BrandedType } from "./brandedType.js";

export type { IDisposable } from "./disposable.js";

export type { IErrorBase, IGenericError, IUsageError, IThrottlingWarning } from "./error.js";
Expand Down Expand Up @@ -52,7 +54,7 @@ export type {
export { LogLevel } from "./logger.js";
export type { FluidObjectProviderKeys, FluidObject, FluidObjectKeys } from "./provider.js";
export type { ConfigTypes, IConfigProviderBase } from "./config.js";
export type { ISignalEnvelope } from "./messages.js";
export type { ISignalEnvelope, TypedMessage } from "./messages.js";
export type { ErasedType } from "./erasedType.js";

export type {
Expand Down
6 changes: 4 additions & 2 deletions packages/common/core-interfaces/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,14 @@ export type ReadonlyNonNullJsonObjectWith<T> = ExposedReadonlyNonNullJsonObjectW
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace InternalUtilityTypes {
// TODO: Add documentation
// eslint-disable-next-line jsdoc/require-jsdoc
/* eslint-disable jsdoc/require-jsdoc */
export type FlattenIntersection<T extends ExposedInternalUtilityTypes.AnyRecord> =
ExposedInternalUtilityTypes.FlattenIntersection<T>;
export type IfSameType<
X,
Y,
IfSame = unknown,
IfDifferent = never,
> = ExposedInternalUtilityTypes.IfSameType<X, Y, IfSame, IfDifferent>;
/* eslint-enable jsdoc/require-jsdoc */
}
30 changes: 24 additions & 6 deletions packages/common/core-interfaces/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@
* Licensed under the MIT License.
*/

/**
* A message that has a string `type` associated with `content`.
*
* @remarks
* This type is meant to be used indirectly. Most commonly as a constraint
* for generics of message structures.
*
* @legacy
* @alpha
*/
export interface TypedMessage {
/**
* The type of the message.
*/
type: string;

/**
* The contents of the message.
*/
content: unknown;
}

/**
* @internal
*
Expand All @@ -13,7 +35,7 @@
*
* See at `server/routerlicious/packages/lambdas/src/utils/messageGenerator.ts`.
*/
export interface ISignalEnvelope {
export interface ISignalEnvelope<TMessage extends TypedMessage = TypedMessage> {
/**
* The target for the envelope, undefined for the container
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
* The target for the envelope, undefined for the container
* The target for the envelope, expected to be undefined since these target the container itself

Copy link
Member

Choose a reason for hiding this comment

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

I guess that's not right, I see it used in ContainerRuntime. I was misinterpreting the doc comment for the interface, which seems to indicate that address will always be missing.

*/
Expand All @@ -27,9 +49,5 @@ export interface ISignalEnvelope {
/**
* The contents of the envelope
*/
contents: {
type: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content: any;
};
contents: TMessage;
}
Original file line number Diff line number Diff line change
Expand Up @@ -468,17 +468,17 @@ export interface ISignalClient {
}

// @alpha @legacy
export interface ISignalMessage extends ISignalMessageBase {
export interface ISignalMessage<TMessage extends TypedMessage = TypedMessage> extends ISignalMessageBase<TMessage> {
clientId: string | null;
}

// @alpha @legacy
export interface ISignalMessageBase {
export interface ISignalMessageBase<TMessage extends TypedMessage = TypedMessage> {
clientConnectionNumber?: number;
content: unknown;
content: TMessage["content"];
referenceSequenceNumber?: number;
targetClientId?: string;
type?: string;
type?: TMessage["type"];
}

// @alpha @legacy
Expand Down
Loading
Loading