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:
Родитель
233bcc4c08
Коммит
e3c4816e56
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче