feat(client-presence): broadcast control defaults (#23120)

- `BroadcastControls` replace `LatestValueControls`
- `BroadcastControls` maybe specified on `PresenceStates` thru new
`controls` property as defaults for all value managers.
- `PresenceNotifications` redeclared apart from `PresenceStates` to
avoid `controls` exposure. Also yields better API documentation.
- `allowableUpdateLatencyMs` was renamed from `allowableUpdateLatency`
to clarify units are milliseconds. Specifying this value currently has
no effect.
- Unsupported `forcedRefreshInterval` has been removed until
implementation is closer.
This commit is contained in:
Jason Hartman 2024-11-18 11:24:13 -08:00 коммит произвёл GitHub
Родитель 233bcc4c08
Коммит e3c4816e56
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
21 изменённых файлов: 537 добавлений и 183 удалений

Просмотреть файл

@ -22,3 +22,11 @@ To update existing code, access your presence states from the `props` property i
- presenceStatesWorkspace.myMap.local.get("key1");
+ presenceStatesWorkspace.props.myMap.local.get("key1");
```
#### `BroadcastControls` replace `LatestValueControls` ([#23120](https://github.com/microsoft/FluidFramework/pull/23120))
`BroadcastControls` maybe specified on `PresenceStates` thru new `controls` property as defaults for all value managers.
`allowableUpdateLatencyMs` was renamed from `allowableUpdateLatency` to clarify units are milliseconds. Specifying this value currently has no effect, but use is recommended to light up as implementation comes online.
Unsupported `forcedRefreshInterval` has been removed until implementation is closer.

Просмотреть файл

@ -128,6 +128,10 @@ Notifications API is partially implemented. All messages are always broadcast ev
Notifications are fundamentally unreliable at this time as there are no built-in acknowledgements nor retained state. To prevent most common loss of notifications, always check for connection before sending.
### Throttling
Throttling is not yet implemented. `BroadcastControls` exists in the API to provide control over throttling of value updates, but throttling is not yet implemented. It is recommended that `BroadcastControls.allowableUpdateLatencyMs` use is considered and specified to light up once support is added.
<!-- AUTO-GENERATED-CONTENT:START (README_FOOTER) -->
<!-- prettier-ignore-start -->

Просмотреть файл

@ -10,6 +10,16 @@ export function acquirePresence(fluidContainer: IFluidContainer): IPresence;
// @alpha
export function acquirePresenceViaDataObject(fluidLoadable: ExperimentalPresenceDO): IPresence;
// @alpha @sealed
export interface BroadcastControls {
allowableUpdateLatencyMs: number | undefined;
}
// @alpha
export interface BroadcastControlSettings {
readonly allowableUpdateLatencyMs?: number;
}
// @alpha
export type ClientConnectionId = string;
@ -32,7 +42,7 @@ export interface IPresence {
getAttendees(): ReadonlySet<ISessionClient>;
getMyself(): ISessionClient;
getNotifications<NotificationsSchema extends PresenceNotificationsSchema>(notificationsId: PresenceWorkspaceAddress, requestedContent: NotificationsSchema): PresenceNotifications<NotificationsSchema>;
getStates<StatesSchema extends PresenceStatesSchema>(workspaceAddress: PresenceWorkspaceAddress, requestedContent: StatesSchema): PresenceStates<StatesSchema>;
getStates<StatesSchema extends PresenceStatesSchema>(workspaceAddress: PresenceWorkspaceAddress, requestedContent: StatesSchema, controls?: BroadcastControlSettings): PresenceStates<StatesSchema>;
}
// @alpha @sealed
@ -43,12 +53,12 @@ export interface ISessionClient<SpecificSessionClientId extends ClientSessionId
}
// @alpha
export function Latest<T extends object, Key extends string = string>(initialValue: JsonSerializable<T> & JsonDeserialized<T> & object, controls?: LatestValueControls): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<T>, LatestValueManager<T>>;
export function Latest<T extends object, Key extends string = string>(initialValue: JsonSerializable<T> & JsonDeserialized<T> & object, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<T>, LatestValueManager<T>>;
// @alpha
export function LatestMap<T extends object, Keys extends string | number = string | number, RegistrationKey extends string = string>(initialValues?: {
[K in Keys]: JsonSerializable<T> & JsonDeserialized<T>;
}, controls?: LatestValueControls): InternalTypes.ManagerFactory<RegistrationKey, InternalTypes.MapValueState<T>, LatestMapValueManager<T, Keys>>;
}, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory<RegistrationKey, InternalTypes.MapValueState<T>, LatestMapValueManager<T, Keys>>;
// @alpha @sealed
export interface LatestMapItemRemovedClientData<K extends string | number> {
@ -78,7 +88,7 @@ export interface LatestMapValueManager<T, Keys extends string | number = string
clients(): ISessionClient[];
clientValue(client: ISessionClient): ReadonlyMap<Keys, LatestValueData<T>>;
clientValues(): IterableIterator<LatestMapValueClientData<T, Keys>>;
readonly controls: LatestValueControls;
readonly controls: BroadcastControls;
readonly events: ISubscribable<LatestMapValueManagerEvents<T, Keys>>;
readonly local: ValueMap<Keys, T>;
}
@ -99,12 +109,6 @@ export interface LatestValueClientData<T> extends LatestValueData<T> {
client: ISessionClient;
}
// @alpha
export interface LatestValueControls {
allowableUpdateLatency: number;
forcedRefreshInterval: number;
}
// @alpha @sealed
export interface LatestValueData<T> {
// (undocumented)
@ -118,7 +122,7 @@ export interface LatestValueManager<T> {
clients(): ISessionClient[];
clientValue(client: ISessionClient): LatestValueData<T>;
clientValues(): IterableIterator<LatestValueClientData<T>>;
readonly controls: LatestValueControls;
readonly controls: BroadcastControls;
readonly events: ISubscribable<LatestValueManagerEvents<T>>;
get local(): InternalUtilityTypes.FullyReadonly<JsonDeserialized<T>>;
set local(value: JsonSerializable<T> & JsonDeserialized<T>);
@ -178,7 +182,10 @@ export interface PresenceEvents {
}
// @alpha @sealed
export type PresenceNotifications<TSchema extends PresenceNotificationsSchema> = PresenceStates<TSchema, NotificationsManager<any>>;
export interface PresenceNotifications<TSchema extends PresenceNotificationsSchema> {
add<TKey extends string, TValue extends InternalTypes.ValueDirectoryOrState<any>, TManager extends NotificationsManager<any>>(key: TKey, manager: InternalTypes.ManagerFactory<TKey, TValue, TManager>): asserts this is PresenceNotifications<TSchema & Record<TKey, InternalTypes.ManagerFactory<TKey, TValue, TManager>>>;
readonly props: PresenceStatesEntries<TSchema>;
}
// @alpha
export interface PresenceNotificationsSchema {
@ -189,6 +196,7 @@ export interface PresenceNotificationsSchema {
// @alpha @sealed
export interface PresenceStates<TSchema extends PresenceStatesSchema, TManagerConstraints = unknown> {
add<TKey extends string, TValue extends InternalTypes.ValueDirectoryOrState<any>, TManager extends TManagerConstraints>(key: TKey, manager: InternalTypes.ManagerFactory<TKey, TValue, TManager>): asserts this is PresenceStates<TSchema & Record<TKey, InternalTypes.ManagerFactory<TKey, TValue, TManager>>, TManagerConstraints>;
readonly controls: BroadcastControls;
readonly props: PresenceStatesEntries<TSchema>;
}
@ -200,18 +208,18 @@ export type PresenceStatesEntries<TSchema extends PresenceStatesSchema> = {
readonly [Key in keyof TSchema]: ReturnType<TSchema[Key]>["manager"] extends InternalTypes.StateValue<infer TManager> ? TManager : never;
};
// @alpha
export type PresenceStatesEntry<TKey extends string, TValue extends InternalTypes.ValueDirectoryOrState<unknown>, TManager = unknown> = InternalTypes.ManagerFactory<TKey, TValue, TManager>;
// @alpha
export interface PresenceStatesSchema {
// (undocumented)
[key: string]: PresenceStatesEntry<typeof key, InternalTypes.ValueDirectoryOrState<any>>;
[key: string]: PresenceWorkspaceEntry<typeof key, InternalTypes.ValueDirectoryOrState<any>>;
}
// @alpha
export type PresenceWorkspaceAddress = `${string}:${string}`;
// @alpha
export type PresenceWorkspaceEntry<TKey extends string, TValue extends InternalTypes.ValueDirectoryOrState<unknown>, TManager = unknown> = InternalTypes.ManagerFactory<TKey, TValue, TManager>;
// @alpha
export const SessionClientStatus: {
readonly Connected: "Connected";

Просмотреть файл

@ -0,0 +1,132 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Common controls for Value Managers.
*
* @sealed
* @alpha
*/
export interface BroadcastControls {
/**
* Maximum time in milliseconds that a local value update is allowed
* to remain pending before it must be broadcast.
*
* @remarks
* There is no guarantee of broadcast within time allowed
* as other conditions such as disconnect or service throttling may
* cause a delay.
*
* Setting to `undefined` will restore to a system default.
*/
allowableUpdateLatencyMs: number | undefined;
/**
* Target time in milliseconds between oldest changed local state
* has been broadcast and forced rebroadcast of all local values.
* A value of less than 10 disables forced refresh.
*
* @privateRemarks
* Any time less than 10 milliseconds is likely to generate too
* many signals. Ideally this feature becomes obsolete as
* we understand the system better and account for holes.
*/
// forcedRefreshIntervalMs is removed until it is supported.
// forcedRefreshIntervalMs: number | undefined;
}
/**
* Value set to configure {@link BroadcastControls}.
*
* @alpha
*/
export interface BroadcastControlSettings {
/**
* {@inheritdoc BroadcastControls.allowableUpdateLatencyMs}
*
* @defaultValue 60 [milliseconds]
*/
readonly allowableUpdateLatencyMs?: number;
/**
* {@inheritdoc BroadcastControls.forcedRefreshIntervalMs}
*
* @defaultValue 0 (disabled)
*/
// forcedRefreshIntervalMs is removed until it is supported.
// readonly forcedRefreshIntervalMs?: number;
}
class ForcedRefreshControl
implements
Pick<
BroadcastControls & { forcedRefreshIntervalMs: number | undefined },
"forcedRefreshIntervalMs"
>
{
private _forcedRefreshInterval: number | undefined;
public constructor(settings?: BroadcastControlSettings) {
// this._forcedRefreshInterval = settings?.forcedRefreshIntervalMs;
}
public get forcedRefreshIntervalMs(): number | undefined {
return this._forcedRefreshInterval;
}
public set forcedRefreshIntervalMs(value: number | undefined) {
if (value === undefined) {
this._forcedRefreshInterval = undefined;
} else {
this._forcedRefreshInterval = value >= 10 ? value : undefined;
if (value >= 10) {
// TODO: enable periodic forced refresh
throw new Error("Forced Refresh feature is not implemented");
}
}
}
}
/**
* @internal
*/
export class OptionalBroadcastControl
extends ForcedRefreshControl
implements BroadcastControls
{
public allowableUpdateLatencyMs: number | undefined;
public constructor(settings?: BroadcastControlSettings) {
super(settings);
this.allowableUpdateLatencyMs = settings?.allowableUpdateLatencyMs;
}
}
/**
* Implements {@link BroadcastControls} but always provides defined value for
* {@link BroadcastControls.allowableUpdateLatencyMs | allowableUpdateLatencyMs}.
*
* If {@link BroadcastControls.allowableUpdateLatencyMs | allowableUpdateLatencyMs}
* is set to `undefined`, the default will be restored.
*
* @internal
*/
export class RequiredBroadcastControl
extends ForcedRefreshControl
implements BroadcastControls
{
private _allowableUpdateLatencyMs: number;
public constructor(private readonly defaultAllowableUpdateLatencyMs: number) {
super();
this._allowableUpdateLatencyMs = defaultAllowableUpdateLatencyMs;
}
public get allowableUpdateLatencyMs(): number {
return this._allowableUpdateLatencyMs;
}
public set allowableUpdateLatencyMs(value: number | undefined) {
this._allowableUpdateLatencyMs = value ?? this.defaultAllowableUpdateLatencyMs;
}
}

Просмотреть файл

@ -112,7 +112,7 @@ export namespace InternalTypes {
key: TKey,
datastoreHandle: StateDatastoreHandle<TKey, TValue>,
) => {
value?: TValue;
initialData?: { value: TValue; allowableUpdateLatencyMs: number | undefined };
manager: StateValue<TManager>;
});

Просмотреть файл

@ -32,9 +32,9 @@ export type {
PresenceNotificationsSchema,
PresenceStates,
PresenceStatesEntries,
PresenceStatesEntry,
PresenceStatesSchema,
PresenceWorkspaceAddress,
PresenceWorkspaceEntry,
} from "./types.js";
export {
@ -45,6 +45,11 @@ export {
SessionClientStatus,
} from "./presence.js";
export type {
BroadcastControls,
BroadcastControlSettings,
} from "./broadcastControls.js";
export { acquirePresence } from "./experimentalAccess.js";
export {
@ -53,7 +58,6 @@ export {
ExperimentalPresenceManager,
} from "./datastorePresenceManagerFactory.js";
export type { LatestValueControls } from "./latestValueControls.js";
export {
LatestMap,
type LatestMapItemRemovedClientData,

Просмотреть файл

@ -14,13 +14,11 @@ import type { IRuntimeInternal } from "@fluidframework/presence/internal/contain
/**
* @internal
*/
export interface ClientRecord<
TValue extends InternalTypes.ValueDirectoryOrState<unknown> | undefined,
> {
export interface ClientRecord<TValue extends InternalTypes.ValueDirectoryOrState<unknown>> {
// Caution: any particular item may or may not exist
// Typescript does not support absent keys without forcing type to also be undefined.
// See https://github.com/microsoft/TypeScript/issues/42810.
[ClientSessionId: ClientSessionId]: Exclude<TValue, undefined>;
[ClientSessionId: ClientSessionId]: TValue;
}
/**

Просмотреть файл

@ -3,9 +3,9 @@
* Licensed under the MIT License.
*/
import type { BroadcastControls, BroadcastControlSettings } from "./broadcastControls.js";
import { OptionalBroadcastControl } from "./broadcastControls.js";
import type { ValueManager } from "./internalTypes.js";
import type { LatestValueControls } from "./latestValueControls.js";
import { LatestValueControl } from "./latestValueControls.js";
import type {
LatestValueClientData,
LatestValueData,
@ -189,10 +189,7 @@ class ValueMapImpl<T, K extends string | number> implements ValueMap<K, T> {
private countDefined: number;
public constructor(
private readonly value: InternalTypes.MapValueState<T>,
private readonly localUpdate: (
updates: InternalTypes.MapValueState<T>,
forceUpdate: boolean,
) => void,
private readonly localUpdate: (updates: InternalTypes.MapValueState<T>) => void,
) {
// All initial items are expected to be defined.
// TODO assert all defined and/or update type.
@ -210,7 +207,7 @@ class ValueMapImpl<T, K extends string | number> implements ValueMap<K, T> {
item.value = value;
}
const update = { rev: this.value.rev, items: { [key]: item } };
this.localUpdate(update, /* forceUpdate */ false);
this.localUpdate(update);
}
public clear(): void {
@ -288,7 +285,7 @@ export interface LatestMapValueManager<T, Keys extends string | number = string
/**
* Controls for management of sending updates.
*/
readonly controls: LatestValueControls;
readonly controls: BroadcastControls;
/**
* Current value map for this client.
@ -317,7 +314,7 @@ class LatestMapValueManagerImpl<
Required<ValueManager<T, InternalTypes.MapValueState<T>>>
{
public readonly events = createEmitter<LatestMapValueManagerEvents<T, Keys>>();
public readonly controls: LatestValueControl;
public readonly controls: OptionalBroadcastControl;
public constructor(
private readonly key: RegistrationKey,
@ -326,14 +323,16 @@ class LatestMapValueManagerImpl<
InternalTypes.MapValueState<T>
>,
public readonly value: InternalTypes.MapValueState<T>,
controlSettings: LatestValueControls,
controlSettings: BroadcastControlSettings | undefined,
) {
this.controls = new LatestValueControl(controlSettings);
this.controls = new OptionalBroadcastControl(controlSettings);
this.local = new ValueMapImpl<T, Keys>(
value,
(updates: InternalTypes.MapValueState<T>, forceUpdate: boolean) => {
datastore.localUpdate(key, updates, forceUpdate);
(updates: InternalTypes.MapValueState<T>) => {
datastore.localUpdate(key, updates, {
allowableUpdateLatencyMs: this.controls.allowableUpdateLatencyMs,
});
},
);
}
@ -449,7 +448,7 @@ export function LatestMap<
initialValues?: {
[K in Keys]: JsonSerializable<T> & JsonDeserialized<T>;
},
controls?: LatestValueControls,
controls?: BroadcastControlSettings,
): InternalTypes.ManagerFactory<
RegistrationKey,
InternalTypes.MapValueState<T>,
@ -463,12 +462,6 @@ export function LatestMap<
value.items[key] = { rev: 0, timestamp, value: initialValues[key as Keys] };
}
}
const controlSettings = controls
? { ...controls }
: {
allowableUpdateLatency: 60,
forcedRefreshInterval: 0,
};
const factory = (
key: RegistrationKey,
datastoreHandle: InternalTypes.StateDatastoreHandle<
@ -489,7 +482,7 @@ export function LatestMap<
key,
datastoreFromHandle(datastoreHandle),
value,
controlSettings,
controls,
),
),
});

Просмотреть файл

@ -1,59 +0,0 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Common controls for the Latest* Value Managers.
*
* @alpha
*/
export interface LatestValueControls {
/**
* Maximum time in milliseconds that a local value update is allowed
* to remain pending before it must be broadcast.
*
* @remarks There is no guarantee of broadcast within time allowed
* as other conditions such as disconnect or service throttling may
* cause a delay.
*/
allowableUpdateLatency: number;
/**
* Target time in milliseconds between oldest changed local state
* has been broadcast and forced rebroadcast of all local values.
* A value of less than 10 disables forced refresh.
*
* @defaultValue 0
*
* @privateRemarks
* Any time less than 10 milliseconds is likely to generate too
* many signals. Ideally this feature becomes obsolete as
* we understand the system better and account for holes.
*/
forcedRefreshInterval: number;
}
/**
* @internal
*/
export class LatestValueControl implements LatestValueControls {
public allowableUpdateLatency: number;
private _forcedRefreshInterval: number;
public constructor(settings: LatestValueControls) {
this.allowableUpdateLatency = settings.allowableUpdateLatency;
this._forcedRefreshInterval = settings.forcedRefreshInterval;
}
public get forcedRefreshInterval(): number {
return this._forcedRefreshInterval;
}
public set forcedRefreshInterval(value: number) {
this._forcedRefreshInterval = value < 10 ? 0 : value;
if (this._forcedRefreshInterval >= 10) {
// TODO: enable periodic forced refresh
throw new Error("Forced Refresh feature is not implemented");
}
}
}

Просмотреть файл

@ -3,10 +3,10 @@
* Licensed under the MIT License.
*/
import type { BroadcastControls, BroadcastControlSettings } from "./broadcastControls.js";
import { OptionalBroadcastControl } from "./broadcastControls.js";
import type { ValueManager } from "./internalTypes.js";
import { brandedObjectEntries } from "./internalTypes.js";
import type { LatestValueControls } from "./latestValueControls.js";
import { LatestValueControl } from "./latestValueControls.js";
import type { LatestValueClientData, LatestValueData } from "./latestValueTypes.js";
import type { ISessionClient } from "./presence.js";
import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js";
@ -52,7 +52,7 @@ export interface LatestValueManager<T> {
/**
* Controls for management of sending updates.
*/
readonly controls: LatestValueControls;
readonly controls: BroadcastControls;
/**
* Current state for this client.
@ -83,15 +83,15 @@ class LatestValueManagerImpl<T, Key extends string>
Required<ValueManager<T, InternalTypes.ValueRequiredState<T>>>
{
public readonly events = createEmitter<LatestValueManagerEvents<T>>();
public readonly controls: LatestValueControl;
public readonly controls: OptionalBroadcastControl;
public constructor(
private readonly key: Key,
private readonly datastore: StateDatastore<Key, InternalTypes.ValueRequiredState<T>>,
public readonly value: InternalTypes.ValueRequiredState<T>,
controlSettings: LatestValueControls,
controlSettings: BroadcastControlSettings | undefined,
) {
this.controls = new LatestValueControl(controlSettings);
this.controls = new OptionalBroadcastControl(controlSettings);
}
public get local(): InternalUtilityTypes.FullyReadonly<JsonDeserialized<T>> {
@ -102,7 +102,9 @@ class LatestValueManagerImpl<T, Key extends string>
this.value.rev += 1;
this.value.timestamp = Date.now();
this.value.value = value;
this.datastore.localUpdate(this.key, this.value, /* forceUpdate */ false);
this.datastore.localUpdate(this.key, this.value, {
allowableUpdateLatencyMs: this.controls.allowableUpdateLatencyMs,
});
}
public *clientValues(): IterableIterator<LatestValueClientData<T>> {
@ -164,7 +166,7 @@ class LatestValueManagerImpl<T, Key extends string>
*/
export function Latest<T extends object, Key extends string = string>(
initialValue: JsonSerializable<T> & JsonDeserialized<T> & object,
controls?: LatestValueControls,
controls?: BroadcastControlSettings,
): InternalTypes.ManagerFactory<
Key,
InternalTypes.ValueRequiredState<T>,
@ -177,12 +179,6 @@ export function Latest<T extends object, Key extends string = string>(
timestamp: Date.now(),
value: { ...initialValue },
};
const controlSettings = controls
? { ...controls }
: {
allowableUpdateLatency: 60,
forcedRefreshInterval: 0,
};
const factory = (
key: Key,
datastoreHandle: InternalTypes.StateDatastoreHandle<
@ -195,12 +191,7 @@ export function Latest<T extends object, Key extends string = string>(
} => ({
value,
manager: brandIVM<LatestValueManagerImpl<T, Key>, T, InternalTypes.ValueRequiredState<T>>(
new LatestValueManagerImpl(
key,
datastoreFromHandle(datastoreHandle),
value,
controlSettings,
),
new LatestValueManagerImpl(key, datastoreFromHandle(datastoreHandle), value, controls),
),
});
return Object.assign(factory, { instanceBase: LatestValueManagerImpl });

Просмотреть файл

@ -8,6 +8,7 @@ import type { ISessionClient } from "./presence.js";
import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js";
import { brandIVM } from "./valueManager.js";
import type { JsonTypeWith } from "@fluidframework/presence/internal/core-interfaces";
import type { Events, ISubscribable } from "@fluidframework/presence/internal/events";
import { createEmitter } from "@fluidframework/presence/internal/events";
import type { InternalTypes } from "@fluidframework/presence/internal/exposedInternalTypes";
@ -147,17 +148,27 @@ class NotificationsManagerImpl<
broadcast: (name, ...args) => {
this.datastore.localUpdate(
this.key,
// @ts-expect-error TODO
{ rev: 0, timestamp: 0, value: { name, args: [...args] }, ignoreUnmonitored: true },
true,
{
rev: 0,
timestamp: 0,
value: { name, args: [...(args as JsonTypeWith<never>[])] },
ignoreUnmonitored: true,
},
// This is a notification, so we want to send it immediately.
{ allowableUpdateLatencyMs: 0 },
);
},
unicast: (name, targetClient, ...args) => {
this.datastore.localUpdate(
this.key,
// @ts-expect-error TODO
{ rev: 0, timestamp: 0, value: { name, args: [...args] }, ignoreUnmonitored: true },
targetClient,
{
rev: 0,
timestamp: 0,
value: { name, args: [...(args as JsonTypeWith<never>[])] },
ignoreUnmonitored: true,
},
// This is a notification, so we want to send it immediately.
{ allowableUpdateLatencyMs: 0, targetClientId: targetClient.getConnectionId() },
);
},
};

Просмотреть файл

@ -6,6 +6,7 @@
import type { SessionId } from "@fluidframework/id-compressor";
import type { ClientConnectionId } from "./baseTypes.js";
import type { BroadcastControlSettings } from "./broadcastControls.js";
import type {
PresenceNotifications,
PresenceNotificationsSchema,
@ -195,11 +196,13 @@ export interface IPresence {
*
* @param workspaceAddress - Address of the requested PresenceStates Workspace
* @param requestedContent - Requested states for the workspace
* @param controls - Optional settings for default broadcast controls
* @returns A PresenceStates workspace
*/
getStates<StatesSchema extends PresenceStatesSchema>(
workspaceAddress: PresenceWorkspaceAddress,
requestedContent: StatesSchema,
controls?: BroadcastControlSettings,
): PresenceStates<StatesSchema>;
/**

Просмотреть файл

@ -8,10 +8,12 @@ import type { IInboundSignalMessage } from "@fluidframework/runtime-definitions/
import type { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
import type { ClientConnectionId } from "./baseTypes.js";
import type { BroadcastControlSettings } from "./broadcastControls.js";
import type { IEphemeralRuntime } from "./internalTypes.js";
import type { ClientSessionId, ISessionClient } from "./presence.js";
import type {
ClientUpdateEntry,
RuntimeLocalUpdateOptions,
PresenceStatesInternal,
ValueElementMap,
} from "./presenceStates.js";
@ -25,7 +27,7 @@ import type {
import type { IExtensionMessage } from "@fluidframework/presence/internal/container-definitions/internal";
interface PresenceStatesEntry<TSchema extends PresenceStatesSchema> {
interface PresenceWorkspaceEntry<TSchema extends PresenceStatesSchema> {
public: PresenceStates<TSchema>;
internal: PresenceStatesInternal;
}
@ -86,6 +88,7 @@ export interface PresenceDatastoreManager {
getWorkspace<TSchema extends PresenceStatesSchema>(
internalWorkspaceAddress: InternalWorkspaceAddress,
requestedContent: TSchema,
controls?: BroadcastControlSettings,
): PresenceStates<TSchema>;
processSignal(message: IExtensionMessage, local: boolean): void;
}
@ -99,7 +102,10 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager {
private returnedMessages = 0;
private refreshBroadcastRequested = false;
private readonly workspaces = new Map<string, PresenceStatesEntry<PresenceStatesSchema>>();
private readonly workspaces = new Map<
string,
PresenceWorkspaceEntry<PresenceStatesSchema>
>();
public constructor(
private readonly clientSessionId: ClientSessionId,
@ -107,7 +113,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager {
private readonly lookupClient: (clientId: ClientSessionId) => ISessionClient,
private readonly logger: ITelemetryLoggerExt | undefined,
systemWorkspaceDatastore: SystemWorkspaceDatastore,
systemWorkspace: PresenceStatesEntry<PresenceStatesSchema>,
systemWorkspace: PresenceWorkspaceEntry<PresenceStatesSchema>,
) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
this.datastore = { "system:presence": systemWorkspaceDatastore } as PresenceDatastore;
@ -135,10 +141,11 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager {
public getWorkspace<TSchema extends PresenceStatesSchema>(
internalWorkspaceAddress: InternalWorkspaceAddress,
requestedContent: TSchema,
controls?: BroadcastControlSettings,
): PresenceStates<TSchema> {
const existing = this.workspaces.get(internalWorkspaceAddress);
if (existing) {
return existing.internal.ensureContent(requestedContent);
return existing.internal.ensureContent(requestedContent, controls);
}
let workspaceDatastore = this.datastore[internalWorkspaceAddress];
@ -148,7 +155,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager {
const localUpdate = (
states: { [key: string]: ClientUpdateEntry },
forceBroadcast: boolean,
options: RuntimeLocalUpdateOptions,
): void => {
// Check for connectivity before sending updates.
if (!this.runtime.connected) {
@ -164,21 +171,18 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager {
for (const [key, value] of Object.entries(states)) {
updates[key] = { [this.clientSessionId]: value };
}
this.localUpdate(
{
// Always send current connection mapping for some resiliency against
// lost signals. This ensures that client session id found in `updates`
// (which is this client's client session id) is always represented in
// system workspace of recipient clients.
"system:presence": {
clientToSessionId: {
[clientConnectionId]: { ...currentClientToSessionValueState },
},
this.localUpdate({
// Always send current connection mapping for some resiliency against
// lost signals. This ensures that client session id found in `updates`
// (which is this client's client session id) is always represented in
// system workspace of recipient clients.
"system:presence": {
clientToSessionId: {
[clientConnectionId]: { ...currentClientToSessionValueState },
},
[internalWorkspaceAddress]: updates,
},
forceBroadcast,
);
[internalWorkspaceAddress]: updates,
});
};
const entry = createPresenceStates(
@ -189,13 +193,14 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager {
},
workspaceDatastore,
requestedContent,
controls,
);
this.workspaces.set(internalWorkspaceAddress, entry);
return entry.public;
}
private localUpdate(data: DatastoreMessageContent, _forceBroadcast: boolean): void {
private localUpdate(data: DatastoreMessageContent): void {
const content = {
sendTimestamp: Date.now(),
avgLatency: this.averageLatency,

Просмотреть файл

@ -11,6 +11,7 @@ import type {
import { createChildMonitoringContext } from "@fluidframework/telemetry-utils/internal";
import type { ClientConnectionId } from "./baseTypes.js";
import type { BroadcastControlSettings } from "./broadcastControls.js";
import type { IEphemeralRuntime } from "./internalTypes.js";
import type {
ClientSessionId,
@ -115,8 +116,13 @@ class PresenceManager implements IPresence, PresenceExtensionInterface {
public getStates<TSchema extends PresenceStatesSchema>(
workspaceAddress: PresenceWorkspaceAddress,
requestedContent: TSchema,
controls?: BroadcastControlSettings,
): PresenceStates<TSchema> {
return this.datastoreManager.getWorkspace(`s:${workspaceAddress}`, requestedContent);
return this.datastoreManager.getWorkspace(
`s:${workspaceAddress}`,
requestedContent,
controls,
);
}
public getNotifications<TSchema extends PresenceStatesSchema>(

Просмотреть файл

@ -6,15 +6,26 @@
import { assert } from "@fluidframework/core-utils/internal";
import type { ClientConnectionId } from "./baseTypes.js";
import type { BroadcastControlSettings } from "./broadcastControls.js";
import { RequiredBroadcastControl } from "./broadcastControls.js";
import type { InternalTypes } from "./exposedInternalTypes.js";
import type { ClientRecord } from "./internalTypes.js";
import { brandedObjectEntries } from "./internalTypes.js";
import type { ClientSessionId, ISessionClient } from "./presence.js";
import { handleFromDatastore, type StateDatastore } from "./stateDatastore.js";
import type { LocalStateUpdateOptions, StateDatastore } from "./stateDatastore.js";
import { handleFromDatastore } from "./stateDatastore.js";
import type { PresenceStates, PresenceStatesSchema } from "./types.js";
import { unbrandIVM } from "./valueManager.js";
/**
* Extracts `Part` from {@link InternalTypes.ManagerFactory} return type
* matching the {@link PresenceStatesSchema} `Keys` given.
*
* @remarks
* If the `Part` is an optional property, undefined will be included in the
* result. Applying `Required` to the return type prior to extracting `Part`
* does not work as expected. Use Exclude\<, undefined\> can be used as needed.
*
* @internal
*/
export type MapSchemaElement<
@ -23,13 +34,28 @@ export type MapSchemaElement<
Keys extends keyof TSchema = keyof TSchema,
> = ReturnType<TSchema[Keys]>[Part];
/**
* @internal
*/
export interface RuntimeLocalUpdateOptions {
allowableUpdateLatencyMs: number;
/**
* Special option allowed for unicast notifications.
*/
targetClientId?: ClientConnectionId;
}
/**
* @internal
*/
export interface PresenceRuntime {
readonly clientSessionId: ClientSessionId;
lookupClient(clientId: ClientConnectionId): ISessionClient;
localUpdate(states: { [key: string]: ClientUpdateEntry }, forceBroadcast: boolean): void;
localUpdate(
states: { [key: string]: ClientUpdateEntry },
options: RuntimeLocalUpdateOptions,
): void;
}
type PresenceSubSchemaFromWorkspaceSchema<
@ -48,7 +74,7 @@ type MapEntries<TSchema extends PresenceStatesSchema> = PresenceSubSchemaFromWor
* ValueElementMap is a map of key to a map of clientId to ValueState.
* It is not restricted to the schema of the map as it may receive updates from other clients
* with managers that have not been registered locally. Each map node is responsible for keeping
* all sessions state to be able to pick arbitrary client to rebroadcast to others.
* all session's state to be able to pick arbitrary client to rebroadcast to others.
*
* This generic aspect makes some typing difficult. The loose typing is not broadcast to the
* consumers that are expected to maintain their schema over multiple versions of clients.
@ -99,6 +125,7 @@ interface ValueUpdateRecord {
export interface PresenceStatesInternal {
ensureContent<TSchemaAdditional extends PresenceStatesSchema>(
content: TSchemaAdditional,
controls: BroadcastControlSettings | undefined,
): PresenceStates<TSchemaAdditional>;
processUpdate(
received: number,
@ -194,36 +221,65 @@ export function mergeUntrackedDatastore(
}
}
/**
* The default allowable update latency for PresenceStates workspaces in milliseconds.
*/
const defaultAllowableUpdateLatencyMs = 60;
/**
* Produces the value type of a schema element or set of elements.
*/
type SchemaElementValueType<
TSchema extends PresenceStatesSchema,
Keys extends keyof TSchema & string,
> = Exclude<MapSchemaElement<TSchema, "initialData", Keys>, undefined>["value"];
class PresenceStatesImpl<TSchema extends PresenceStatesSchema>
implements
PresenceStatesInternal,
PresenceStates<TSchema>,
StateDatastore<
keyof TSchema & string,
MapSchemaElement<TSchema, "value", keyof TSchema & string>
SchemaElementValueType<TSchema, keyof TSchema & string>
>
{
private readonly nodes: MapEntries<TSchema>;
public readonly props: PresenceStates<TSchema>["props"];
public readonly controls: RequiredBroadcastControl;
public constructor(
private readonly runtime: PresenceRuntime,
private readonly datastore: ValueElementMap<TSchema>,
initialContent: TSchema,
controlsSettings: BroadcastControlSettings | undefined,
) {
this.controls = new RequiredBroadcastControl(defaultAllowableUpdateLatencyMs);
if (controlsSettings?.allowableUpdateLatencyMs !== undefined) {
this.controls.allowableUpdateLatencyMs = controlsSettings.allowableUpdateLatencyMs;
}
// Prepare initial map content from initial state
{
const clientSessionId = this.runtime.clientSessionId;
let anyInitialValues = false;
let cumulativeAllowableUpdateLatencyMs: number | undefined;
// eslint-disable-next-line unicorn/no-array-reduce
const initial = Object.entries(initialContent).reduce(
(acc, [key, nodeFactory]) => {
const newNodeData = nodeFactory(key, handleFromDatastore(this));
acc.nodes[key as keyof TSchema] = newNodeData.manager;
if ("value" in newNodeData) {
if ("initialData" in newNodeData) {
const { value, allowableUpdateLatencyMs } = newNodeData.initialData;
acc.datastore[key] = acc.datastore[key] ?? {};
acc.datastore[key][clientSessionId] = newNodeData.value;
acc.newValues[key] = newNodeData.value;
acc.datastore[key][clientSessionId] = value;
acc.newValues[key] = value;
if (allowableUpdateLatencyMs !== undefined) {
cumulativeAllowableUpdateLatencyMs =
cumulativeAllowableUpdateLatencyMs === undefined
? allowableUpdateLatencyMs
: Math.min(cumulativeAllowableUpdateLatencyMs, allowableUpdateLatencyMs);
}
anyInitialValues = true;
}
return acc;
@ -243,7 +299,10 @@ class PresenceStatesImpl<TSchema extends PresenceStatesSchema>
this.props = this.nodes as unknown as PresenceStates<TSchema>["props"];
if (anyInitialValues) {
this.runtime.localUpdate(initial.newValues, false);
this.runtime.localUpdate(initial.newValues, {
allowableUpdateLatencyMs:
cumulativeAllowableUpdateLatencyMs ?? this.controls.allowableUpdateLatencyMs,
});
}
}
}
@ -252,7 +311,7 @@ class PresenceStatesImpl<TSchema extends PresenceStatesSchema>
key: Key,
): {
self: ClientSessionId | undefined;
states: ClientRecord<MapSchemaElement<TSchema, "value", Key>>;
states: ClientRecord<SchemaElementValueType<TSchema, Key>>;
} {
return {
self: this.runtime.clientSessionId,
@ -262,16 +321,23 @@ class PresenceStatesImpl<TSchema extends PresenceStatesSchema>
public localUpdate<Key extends keyof TSchema & string>(
key: Key,
value: MapSchemaElement<TSchema, "value", Key> & ClientUpdateEntry,
forceBroadcast: boolean,
value: SchemaElementValueType<TSchema, Key> & ClientUpdateEntry,
options: LocalStateUpdateOptions,
): void {
this.runtime.localUpdate({ [key]: value }, forceBroadcast);
this.runtime.localUpdate(
{ [key]: value },
{
...options,
allowableUpdateLatencyMs:
options.allowableUpdateLatencyMs ?? this.controls.allowableUpdateLatencyMs,
},
);
}
public update<Key extends keyof TSchema & string>(
key: Key,
clientId: ClientSessionId,
value: Exclude<MapSchemaElement<TSchema, "value", Key>, undefined>,
value: Exclude<MapSchemaElement<TSchema, "initialData", Key>, undefined>["value"],
): void {
const allKnownState = this.datastore[key];
allKnownState[clientId] = mergeValueDirectory(allKnownState[clientId], value, 0);
@ -294,21 +360,32 @@ class PresenceStatesImpl<TSchema extends PresenceStatesSchema>
assert(!(key in this.nodes), 0xa3c /* Already have entry for key in map */);
const nodeData = nodeFactory(key, handleFromDatastore(this));
this.nodes[key] = nodeData.manager;
if ("value" in nodeData) {
if ("initialData" in nodeData) {
const { value, allowableUpdateLatencyMs } = nodeData.initialData;
if (key in this.datastore) {
// Already have received state from other clients. Kept in `all`.
// TODO: Send current `all` state to state manager.
} else {
this.datastore[key] = {};
}
this.datastore[key][this.runtime.clientSessionId] = nodeData.value;
this.runtime.localUpdate({ [key]: nodeData.value }, false);
this.datastore[key][this.runtime.clientSessionId] = value;
this.runtime.localUpdate(
{ [key]: value },
{
allowableUpdateLatencyMs:
allowableUpdateLatencyMs ?? this.controls.allowableUpdateLatencyMs,
},
);
}
}
public ensureContent<TSchemaAdditional extends PresenceStatesSchema>(
content: TSchemaAdditional,
controls: BroadcastControlSettings | undefined,
): PresenceStates<TSchema & TSchemaAdditional> {
if (controls?.allowableUpdateLatencyMs !== undefined) {
this.controls.allowableUpdateLatencyMs = controls.allowableUpdateLatencyMs;
}
for (const [key, nodeFactory] of Object.entries(content)) {
if (key in this.nodes) {
const node = unbrandIVM(this.nodes[key]);
@ -350,8 +427,9 @@ export function createPresenceStates<TSchema extends PresenceStatesSchema>(
runtime: PresenceRuntime,
datastore: ValueElementMap<PresenceStatesSchema>,
initialContent: TSchema,
controls: BroadcastControlSettings | undefined,
): { public: PresenceStates<TSchema>; internal: PresenceStatesInternal } {
const impl = new PresenceStatesImpl<TSchema>(runtime, datastore, initialContent);
const impl = new PresenceStatesImpl<TSchema>(runtime, datastore, initialContent, controls);
return {
public: impl,

Просмотреть файл

@ -23,19 +23,31 @@ import type { ClientSessionId, ISessionClient } from "./presence.js";
// // [key: string]: StateDatastoreSchemaNode;
// }
/**
* @internal
*/
export interface LocalStateUpdateOptions {
allowableUpdateLatencyMs: number | undefined;
/**
* Special option allowed for unicast notifications.
*/
targetClientId?: ClientConnectionId;
}
/**
* @internal
*/
export interface StateDatastore<
TKey extends string,
TValue extends InternalTypes.ValueDirectoryOrState<any> | undefined,
TValue extends InternalTypes.ValueDirectoryOrState<any>,
> {
localUpdate(
key: TKey,
value: TValue & {
ignoreUnmonitored?: true;
},
forceBroadcast: boolean,
options: LocalStateUpdateOptions,
): void;
update(key: TKey, clientSessionId: ClientSessionId, value: TValue): void;
knownValues(key: TKey): {
@ -55,7 +67,7 @@ export function handleFromDatastore<
// TSchema as `unknown` still provides some type safety.
// TSchema extends StateDatastoreSchema,
TKey extends string /* & keyof TSchema */,
TValue extends InternalTypes.ValueDirectoryOrState<unknown> | undefined,
TValue extends InternalTypes.ValueDirectoryOrState<unknown>,
>(
datastore: StateDatastore<TKey, TValue>,
): InternalTypes.StateDatastoreHandle<TKey, Exclude<TValue, undefined>> {

Просмотреть файл

@ -0,0 +1,83 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import assert from "node:assert";
import type { BroadcastControls, BroadcastControlSettings } from "../broadcastControls.js";
import type { IPresence } from "../presence.js";
import { createPresenceManager } from "../presenceManager.js";
import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js";
const testDefaultAllowableUpdateLatencyMs = 100;
/**
* Adds set of test for common {@link BroadcastControls} implementations.
*
* @param createControls - Function to create the `controls` provider object
*/
export function addControlsTests(
createControls: (
presence: IPresence,
controlSettings?: BroadcastControlSettings,
) => { controls: BroadcastControls },
): void {
describe("controls allowableUpdateLatencyMs", () => {
it("can be specified during create", () => {
// Setup
const presence = createPresenceManager(new MockEphemeralRuntime());
// Act
const controlsProvider = createControls(presence, {
allowableUpdateLatencyMs: testDefaultAllowableUpdateLatencyMs,
});
// Verify
assert.equal(
controlsProvider.controls.allowableUpdateLatencyMs,
testDefaultAllowableUpdateLatencyMs,
);
});
it("can be changed", () => {
// Setup
const presence = createPresenceManager(new MockEphemeralRuntime());
const controlsProvider = createControls(presence, {
allowableUpdateLatencyMs: testDefaultAllowableUpdateLatencyMs,
});
// Act
controlsProvider.controls.allowableUpdateLatencyMs = 200;
// Verify
assert.equal(controlsProvider.controls.allowableUpdateLatencyMs, 200);
});
it("can be reset to system default", () => {
// Setup
// First read value of system default from init without any settings
let presence = createPresenceManager(new MockEphemeralRuntime());
let controlsProvider = createControls(presence, undefined);
const systemDefault = controlsProvider.controls.allowableUpdateLatencyMs;
assert.notEqual(
testDefaultAllowableUpdateLatencyMs,
systemDefault,
"test internal error: Test value matches system default value",
);
// Recreate controls with custom settings specified
presence = createPresenceManager(new MockEphemeralRuntime());
controlsProvider = createControls(presence, {
allowableUpdateLatencyMs: testDefaultAllowableUpdateLatencyMs,
});
// Act
controlsProvider.controls.allowableUpdateLatencyMs = undefined;
// Verify
assert.equal(controlsProvider.controls.allowableUpdateLatencyMs, systemDefault);
});
});
}

Просмотреть файл

@ -3,9 +3,28 @@
* Licensed under the MIT License.
*/
import type { LatestMapItemValueClientData } from "../index.js";
import { LatestMap } from "../index.js";
import type { IPresence } from "../presence.js";
import { addControlsTests } from "./broadcastControlsTests.js";
import type {
BroadcastControlSettings,
IPresence,
LatestMapItemValueClientData,
} from "@fluidframework/presence/alpha";
import { LatestMap } from "@fluidframework/presence/alpha";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function createLatestMapManager(
presence: IPresence,
valueControlSettings?: BroadcastControlSettings,
) {
const states = presence.getStates("name:testWorkspaceA", {
fixedMap: LatestMap(
{ key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } },
valueControlSettings,
),
});
return states.props.fixedMap;
}
describe("Presence", () => {
describe("LatestMapValueManager", () => {
@ -13,6 +32,8 @@ describe("Presence", () => {
* See {@link checkCompiles} below
*/
it("API use compiles", () => {});
addControlsTests(createLatestMapManager);
});
});

Просмотреть файл

@ -3,9 +3,25 @@
* Licensed under the MIT License.
*/
import type { LatestValueClientData } from "../index.js";
import { Latest } from "../index.js";
import type { IPresence } from "../presence.js";
import { addControlsTests } from "./broadcastControlsTests.js";
import type {
BroadcastControlSettings,
IPresence,
LatestValueClientData,
} from "@fluidframework/presence/alpha";
import { Latest } from "@fluidframework/presence/alpha";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function createLatestManager(
presence: IPresence,
valueControlSettings?: BroadcastControlSettings,
) {
const states = presence.getStates("name:testWorkspaceA", {
camera: Latest({ x: 0, y: 0, z: 0 }, valueControlSettings),
});
return states.props.camera;
}
describe("Presence", () => {
describe("LatestValueManager", () => {
@ -13,6 +29,8 @@ describe("Presence", () => {
* See {@link checkCompiles} below
*/
it("API use compiles", () => {});
addControlsTests(createLatestManager);
});
});

Просмотреть файл

@ -5,6 +5,8 @@
import type { IPresence } from "../presence.js";
import { addControlsTests } from "./broadcastControlsTests.js";
import type {
JsonDeserialized,
JsonSerializable,
@ -17,6 +19,10 @@ describe("Presence", () => {
* See {@link checkCompiles} below
*/
it("API use compiles", () => {});
addControlsTests((presence, controlSettings) => {
return presence.getStates("name:testWorkspaceA", {}, controlSettings);
});
});
});

Просмотреть файл

@ -3,6 +3,7 @@
* Licensed under the MIT License.
*/
import type { BroadcastControls } from "./broadcastControls.js";
import type { NotificationsManager } from "./notificationsManager.js";
import type { InternalTypes } from "@fluidframework/presence/internal/exposedInternalTypes";
@ -25,19 +26,19 @@ import type { InternalTypes } from "@fluidframework/presence/internal/exposedInt
*/
export type PresenceWorkspaceAddress = `${string}:${string}`;
// #region PresenceStates
/**
* Single entry in {@link PresenceStatesSchema}.
* Single entry in {@link PresenceStatesSchema} or {@link PresenceNotificationsSchema}.
*
* @alpha
*/
export type PresenceStatesEntry<
export type PresenceWorkspaceEntry<
TKey extends string,
TValue extends InternalTypes.ValueDirectoryOrState<unknown>,
TManager = unknown,
> = InternalTypes.ManagerFactory<TKey, TValue, TManager>;
// #region PresenceStates
/**
* Schema for a {@link PresenceStates} workspace.
*
@ -46,7 +47,7 @@ export type PresenceStatesEntry<
* @alpha
*/
export interface PresenceStatesSchema {
[key: string]: PresenceStatesEntry<typeof key, InternalTypes.ValueDirectoryOrState<any>>;
[key: string]: PresenceWorkspaceEntry<typeof key, InternalTypes.ValueDirectoryOrState<any>>;
}
/**
@ -82,7 +83,7 @@ export interface PresenceStates<
> {
/**
* Registers a new `Value Manager` with the {@link PresenceStates}.
* @param key - new unique key for the `Value Manager`
* @param key - new unique key for the `Value Manager` within the workspace
* @param manager - factory for creating a `Value Manager`
*/
add<
@ -101,6 +102,11 @@ export interface PresenceStates<
* Registry of `Value Manager`s.
*/
readonly props: PresenceStatesEntries<TSchema>;
/**
* Default controls for management of broadcast updates.
*/
readonly controls: BroadcastControls;
}
// #endregion PresenceStates
@ -126,10 +132,36 @@ export interface PresenceNotificationsSchema {
* `PresenceNotifications` maintains a registry of {@link NotificationsManager}s
* that facilitate messages across client members in a session.
*
* @privateRemarks
* This should be kept mostly in sync with {@link PresenceStates}. Notably the
* return type of `add` is limited here and the `controls` property is omitted.
* The `PresenceStatesImpl` class implements `PresenceStates` and therefore
* `PresenceNotifications`, so long as this is proper subset.
*
* @sealed
* @alpha
*/
export type PresenceNotifications<TSchema extends PresenceNotificationsSchema> =
PresenceStates<TSchema, NotificationsManager<any>>;
export interface PresenceNotifications<TSchema extends PresenceNotificationsSchema> {
/**
* Registers a new `Value Manager` with the {@link PresenceNotifications}.
* @param key - new unique key for the `Value Manager` within the workspace
* @param manager - factory for creating a `Value Manager`
*/
add<
TKey extends string,
TValue extends InternalTypes.ValueDirectoryOrState<any>,
TManager extends NotificationsManager<any>,
>(
key: TKey,
manager: InternalTypes.ManagerFactory<TKey, TValue, TManager>,
): asserts this is PresenceNotifications<
TSchema & Record<TKey, InternalTypes.ManagerFactory<TKey, TValue, TManager>>
>;
/**
* Registry of `Value Manager`s.
*/
readonly props: PresenceStatesEntries<TSchema>;
}
// #endregion PresenceNotifications