@netless/window-manager 0.4.0-canary.10 → 0.4.0-canary.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/typings.d.ts CHANGED
@@ -2,7 +2,7 @@ import type Emittery from "emittery";
2
2
  import type { AnimationMode, Displayer, DisplayerState, Player, Room, SceneDefinition, SceneState, View } from "white-web-sdk";
3
3
  import type { AppContext } from "./AppContext";
4
4
  import type { ReadonlyTeleBox, TeleBoxRect } from "@netless/telebox-insider";
5
- export interface NetlessApp<Attributes = any, SetupResult = any, AppOptions = any> {
5
+ export interface NetlessApp<Attributes = any, MagixEventPayloads = any, AppOptions = any, SetupResult = any> {
6
6
  kind: string;
7
7
  config?: {
8
8
  /** Box width relative to whiteboard. 0~1. Default 0.5. */
@@ -16,7 +16,7 @@ export interface NetlessApp<Attributes = any, SetupResult = any, AppOptions = an
16
16
  /** App only single instance. */
17
17
  singleton?: boolean;
18
18
  };
19
- setup: (context: AppContext<Attributes, AppOptions>) => SetupResult;
19
+ setup: (context: AppContext<Attributes, MagixEventPayloads, AppOptions>) => SetupResult;
20
20
  }
21
21
  export declare type AppEmitterEvent<T = any> = {
22
22
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netless/window-manager",
3
- "version": "0.4.0-canary.10",
3
+ "version": "0.4.0-canary.11",
4
4
  "description": "",
5
5
  "main": "dist/index.es.js",
6
6
  "module": "dist/index.es.js",
@@ -0,0 +1,66 @@
1
+ import type {
2
+ MagixEventListenerOptions as WhiteMagixListenerOptions,
3
+ Event as WhiteEvent,
4
+ EventPhase as WhiteEventPhase,
5
+ Scope as WhiteScope,
6
+ } from "white-web-sdk";
7
+
8
+ export interface MagixEventListenerOptions extends WhiteMagixListenerOptions {
9
+ /**
10
+ * Rapid emitted callbacks will be slowed down to this interval (in ms).
11
+ */
12
+ fireInterval?: number;
13
+ /**
14
+ * If `true`, sent events will reach self-listeners after committed to server.
15
+ * Otherwise the events will reach self-listeners immediately.
16
+ */
17
+ fireSelfEventAfterCommit?: boolean;
18
+ }
19
+
20
+ export interface MagixEventMessage<
21
+ TPayloads = any,
22
+ TEvent extends MagixEventTypes<TPayloads> = MagixEventTypes<TPayloads>
23
+ > extends Omit<WhiteEvent, "scope" | "phase"> {
24
+ /** Event name */
25
+ event: TEvent;
26
+ /** Event Payload */
27
+ payload: TPayloads[TEvent];
28
+ /** Whiteboard ID of the client who dispatched the event. It will be AdminObserverId for system events. */
29
+ authorId: number;
30
+ scope: `${WhiteScope}`;
31
+ phase: `${WhiteEventPhase}`;
32
+ }
33
+
34
+ export type MagixEventTypes<TPayloads = any> = Extract<keyof TPayloads, string>;
35
+
36
+ export type MagixEventPayload<
37
+ TPayloads = any,
38
+ TEvent extends MagixEventTypes<TPayloads> = MagixEventTypes<TPayloads>
39
+ > = TPayloads[TEvent];
40
+
41
+ export type MagixEventDispatcher<TPayloads = any> = <
42
+ TEvent extends MagixEventTypes<TPayloads> = MagixEventTypes<TPayloads>
43
+ >(
44
+ event: TEvent,
45
+ payload: TPayloads[TEvent]
46
+ ) => void;
47
+
48
+ export type MagixEventHandler<
49
+ TPayloads = any,
50
+ TEvent extends MagixEventTypes<TPayloads> = MagixEventTypes<TPayloads>
51
+ > = (message: MagixEventMessage<TPayloads, TEvent>) => void;
52
+
53
+ export type MagixEventAddListener<TPayloads = any> = <
54
+ TEvent extends MagixEventTypes<TPayloads> = MagixEventTypes<TPayloads>
55
+ >(
56
+ event: TEvent,
57
+ handler: MagixEventHandler<TPayloads, TEvent>,
58
+ options?: MagixEventListenerOptions | undefined
59
+ ) => void;
60
+
61
+ export type MagixEventRemoveListener<TPayloads = any> = <
62
+ TEvent extends MagixEventTypes<TPayloads> = MagixEventTypes<TPayloads>
63
+ >(
64
+ event: TEvent,
65
+ handler?: MagixEventHandler<TPayloads, TEvent>
66
+ ) => void;
@@ -1,5 +1,5 @@
1
1
  import type { AkkoObjectUpdatedProperty } from "white-web-sdk";
2
- import { get, has, isObject } from "lodash";
2
+ import { get, has, mapValues, isObject, size, noop } from "lodash";
3
3
  import { SideEffectManager } from "side-effect-manager";
4
4
  import type { AppContext } from "../../AppContext";
5
5
  import { safeListenPropsUpdated } from "../../Utils/Reactive";
@@ -9,12 +9,12 @@ import { StorageEvent } from "./StorageEvent";
9
9
 
10
10
  export * from './typings';
11
11
 
12
- const STORAGE_NS = "_WM-STORAGE_";
12
+ export const STORAGE_NS = "_WM-STORAGE_";
13
13
 
14
- export class Storage<TState = any> implements Storage<TState> {
15
- readonly id: string;
14
+ export class Storage<TState extends Record<string, any> = any> implements Storage<TState> {
15
+ readonly id: string | null;
16
16
 
17
- private readonly _context: AppContext<{ [STORAGE_NS]: TState }>;
17
+ private readonly _context: AppContext;
18
18
  private readonly _sideEffect = new SideEffectManager();
19
19
  private _state: TState;
20
20
  private _destroyed = false;
@@ -26,54 +26,52 @@ export class Storage<TState = any> implements Storage<TState> {
26
26
  */
27
27
  private _lastValue = new Map<string | number | symbol, TState[Extract<keyof TState, string>]>();
28
28
 
29
- constructor(context: AppContext<any>, id: string, defaultState?: TState) {
30
- if (id == null) {
31
- throw new Error("Cannot create Storage with empty id.");
32
- }
33
-
29
+ constructor(context: AppContext, id?: string, defaultState?: TState) {
34
30
  if (defaultState && !isObject(defaultState)) {
35
31
  throw new Error(`Default state for Storage ${id} is not an object.`);
36
32
  }
37
33
 
38
34
  this._context = context;
39
- this.id = id;
35
+ this.id = id || null;
40
36
 
41
- const attrs = context.getAttributes();
42
37
  this._state = {} as TState;
43
- const rawState = get<TState>(attrs, [STORAGE_NS, id], this._state);
38
+ const rawState = this._getRawState(this._state);
44
39
 
45
- if (this._context.getIsWritable()) {
46
- if (!isObject(rawState) || rawState === this._state) {
47
- if (!attrs[STORAGE_NS]) {
40
+ if (this.id !== null && this._context.getIsWritable()) {
41
+ if (rawState === this._state || !isObject(rawState)) {
42
+ if (!get(this._context.getAttributes(), [STORAGE_NS])) {
48
43
  this._context.updateAttributes([STORAGE_NS], {});
49
44
  }
50
45
  this._context.updateAttributes([STORAGE_NS, this.id], this._state);
51
- if (defaultState) {
52
- this.setState(defaultState);
53
- }
54
- } else {
55
- // strip mobx
56
- plainObjectKeys(rawState).forEach(key => {
57
- try {
58
- const rawValue = isObject(rawState[key]) ? JSON.parse(JSON.stringify(rawState[key])) : rawState[key];
59
- if (isRef<TState[Extract<keyof TState, string>]>(rawValue)) {
60
- this._state[key] = rawValue.v;
61
- if (isObject(rawValue.v)) {
62
- this._refMap.set(rawValue.v, rawValue);
63
- }
64
- } else {
65
- this._state[key] = rawValue;
66
- }
67
- } catch (e) {
68
- console.error(e);
69
- }
70
- });
46
+ }
47
+ if (defaultState) {
48
+ this.setState(defaultState);
71
49
  }
72
50
  }
73
51
 
52
+ // strip mobx
53
+ plainObjectKeys(rawState).forEach(key => {
54
+ if (this.id === null && key === STORAGE_NS) {
55
+ return;
56
+ }
57
+ try {
58
+ const rawValue = isObject(rawState[key]) ? JSON.parse(JSON.stringify(rawState[key])) : rawState[key];
59
+ if (isRef<TState[Extract<keyof TState, string>]>(rawValue)) {
60
+ this._state[key] = rawValue.v;
61
+ if (isObject(rawValue.v)) {
62
+ this._refMap.set(rawValue.v, rawValue);
63
+ }
64
+ } else {
65
+ this._state[key] = rawValue;
66
+ }
67
+ } catch (e) {
68
+ console.error(e);
69
+ }
70
+ });
71
+
74
72
  this._sideEffect.addDisposer(
75
73
  safeListenPropsUpdated(
76
- () => get(context.getAttributes(), [STORAGE_NS, this.id]),
74
+ () => this.id === null ? context.getAttributes() : get(context.getAttributes(), [STORAGE_NS, this.id]),
77
75
  this._updateProperties.bind(this),
78
76
  this.destroy.bind(this)
79
77
  )
@@ -122,7 +120,7 @@ export class Storage<TState = any> implements Storage<TState> {
122
120
  if (value === void 0) {
123
121
  this._lastValue.set(key, this._state[key]);
124
122
  delete this._state[key];
125
- this._context.updateAttributes([STORAGE_NS, this.id, key], value);
123
+ this._setRawState(key, value);
126
124
  } else {
127
125
  this._lastValue.set(key, this._state[key]);
128
126
  this._state[key] = value as TState[Extract<keyof TState, string>];
@@ -137,13 +135,20 @@ export class Storage<TState = any> implements Storage<TState> {
137
135
  payload = refValue;
138
136
  }
139
137
 
140
- this._context.updateAttributes([STORAGE_NS, this.id, key], payload);
138
+ this._setRawState(key, payload)
141
139
  }
142
140
  });
143
141
  }
144
142
  }
145
143
 
146
- emptyStore(): void {
144
+ /**
145
+ * Empty storage data.
146
+ */
147
+ emptyStorage(): void {
148
+ if (size(this._state) <= 0) {
149
+ return;
150
+ }
151
+
147
152
  if (this._destroyed) {
148
153
  console.error(new Error(`Cannot empty destroyed Storage "${this.id}".`));
149
154
  return;
@@ -154,10 +159,17 @@ export class Storage<TState = any> implements Storage<TState> {
154
159
  return;
155
160
  }
156
161
 
157
- this._context.updateAttributes([STORAGE_NS, this.id], {});
162
+ this.setState(mapValues(this._state, noop as () => undefined));
158
163
  }
159
164
 
160
- deleteStore(): void {
165
+ /**
166
+ * Delete storage index with all of its data and destroy the Storage instance.
167
+ */
168
+ deleteStorage(): void {
169
+ if (this.id === null) {
170
+ throw new Error(`Cannot delete main Storage`);
171
+ }
172
+
161
173
  if (!this._context.getIsWritable()) {
162
174
  console.error(new Error(`Cannot delete Storage "${this.id}" without writable access.`));
163
175
  return;
@@ -172,11 +184,35 @@ export class Storage<TState = any> implements Storage<TState> {
172
184
  return this._destroyed;
173
185
  }
174
186
 
187
+ /**
188
+ * Destroy the Storage instance. The data will be kept.
189
+ */
175
190
  destroy() {
176
191
  this._destroyed = true;
177
192
  this._sideEffect.flushAll();
178
193
  }
179
194
 
195
+ private _getRawState(): TState | undefined
196
+ private _getRawState(defaultValue: TState): TState
197
+ private _getRawState(defaultValue?: TState): TState | undefined {
198
+ if (this.id === null) {
199
+ return get(this._context.getAttributes(), [], defaultValue);
200
+ } else {
201
+ return get(this._context.getAttributes(), [STORAGE_NS, this.id], defaultValue);
202
+ }
203
+ }
204
+
205
+ private _setRawState(key: string, value: any): void {
206
+ if (this.id === null) {
207
+ if (key === STORAGE_NS) {
208
+ throw new Error(`Cannot set attribute internal filed "${STORAGE_NS}"`)
209
+ }
210
+ return this._context.updateAttributes([key], value);
211
+ } else {
212
+ return this._context.updateAttributes([STORAGE_NS, this.id, key], value);
213
+ }
214
+ }
215
+
180
216
  private _updateProperties(actions: ReadonlyArray<AkkoObjectUpdatedProperty<TState, string>>): void {
181
217
  if (this._destroyed) {
182
218
  console.error(new Error(`Cannot call _updateProperties on destroyed Storage "${this.id}".`));
@@ -190,6 +226,11 @@ export class Storage<TState = any> implements Storage<TState> {
190
226
  try {
191
227
  const action = actions[i]
192
228
  const key = action.key as Extract<keyof TState, string>;
229
+
230
+ if (this.id === null && key === STORAGE_NS) {
231
+ continue
232
+ }
233
+
193
234
  const value = isObject(action.value) ? JSON.parse(JSON.stringify(action.value)) : action.value;
194
235
  let oldValue: TState[Extract<keyof TState, string>] | undefined;
195
236
  if (this._lastValue.has(key)) {
package/src/AppContext.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  unlistenDisposed,
7
7
  unlistenUpdated,
8
8
  toJS
9
- } from 'white-web-sdk';
9
+ } from 'white-web-sdk';
10
10
  import { BoxNotCreatedError } from './Utils/error';
11
11
  import type { Room, SceneDefinition, View } from "white-web-sdk";
12
12
  import type { ReadonlyTeleBox } from "@netless/telebox-insider";
@@ -16,9 +16,10 @@ import type { AppEmitterEvent } from "./index";
16
16
  import type { AppManager } from "./AppManager";
17
17
  import type { AppProxy } from "./AppProxy";
18
18
  import { Storage } from './App/Storage';
19
+ import type { MagixEventAddListener, MagixEventDispatcher, MagixEventRemoveListener } from './App/MagixEvent';
19
20
 
20
- export class AppContext<TAttrs extends Record<string, any> = any, AppOptions = any> {
21
- public readonly emitter: Emittery<AppEmitterEvent<TAttrs>>;
21
+ export class AppContext<TAttributes = any, TMagixEventPayloads = any, TAppOptions = any> {
22
+ public readonly emitter: Emittery<AppEmitterEvent<TAttributes>>;
22
23
  public readonly mobxUtils = {
23
24
  autorun,
24
25
  reaction,
@@ -40,21 +41,22 @@ export class AppContext<TAttrs extends Record<string, any> = any, AppOptions = a
40
41
  private boxManager: BoxManager,
41
42
  public appId: string,
42
43
  private appProxy: AppProxy,
43
- private appOptions?: AppOptions | (() => AppOptions),
44
+ private appOptions?: TAppOptions | (() => TAppOptions),
44
45
  ) {
45
46
  this.emitter = appProxy.appEmitter;
46
47
  this.isAddApp = appProxy.isAddApp;
47
48
  }
48
49
 
49
- public getDisplayer() {
50
+ public getDisplayer = () => {
50
51
  return this.manager.displayer;
51
52
  }
52
53
 
53
- public getAttributes(): TAttrs | undefined {
54
+ /** @deprecated Use context.storage.state instead. */
55
+ public getAttributes = (): TAttributes | undefined => {
54
56
  return this.appProxy.attributes;
55
57
  }
56
58
 
57
- public getScenes(): SceneDefinition[] | undefined {
59
+ public getScenes = (): SceneDefinition[] | undefined => {
58
60
  const appAttr = this.store.getAppAttributes(this.appId);
59
61
  if (appAttr?.isDynamicPPT) {
60
62
  const appProxy = this.manager.appProxies.get(this.appId);
@@ -66,19 +68,21 @@ export class AppContext<TAttrs extends Record<string, any> = any, AppOptions = a
66
68
  }
67
69
  }
68
70
 
69
- public getView(): View | undefined {
71
+ public getView = (): View | undefined => {
70
72
  return this.appProxy.view;
71
73
  }
72
74
 
73
- public getInitScenePath() {
75
+ public getInitScenePath = () => {
74
76
  return this.manager.getAppInitPath(this.appId);
75
77
  }
76
78
 
77
- public getIsWritable(): boolean {
79
+ /** Get App writable status. */
80
+ public getIsWritable = (): boolean => {
78
81
  return this.manager.canOperate;
79
82
  }
80
83
 
81
- public getBox(): ReadonlyTeleBox {
84
+ /** Get the App Window UI box. */
85
+ public getBox = (): ReadonlyTeleBox => {
82
86
  const box = this.boxManager.getBox(this.appId);
83
87
  if (box) {
84
88
  return box;
@@ -87,26 +91,28 @@ export class AppContext<TAttrs extends Record<string, any> = any, AppOptions = a
87
91
  }
88
92
  }
89
93
 
90
- public getRoom(): Room | undefined {
94
+ public getRoom = (): Room | undefined => {
91
95
  return this.manager.room;
92
96
  }
93
97
 
94
- public setAttributes(attributes: TAttrs) {
98
+ /** @deprecated Use context.storage.setState instead. */
99
+ public setAttributes = (attributes: TAttributes) => {
95
100
  this.manager.safeSetAttributes({ [this.appId]: attributes });
96
101
  }
97
102
 
98
- public updateAttributes(keys: string[], value: any) {
103
+ /** @deprecated Use context.storage.setState instead. */
104
+ public updateAttributes = (keys: string[], value: any) => {
99
105
  if (this.manager.attributes[this.appId]) {
100
106
  this.manager.safeUpdateAttributes([this.appId, ...keys], value);
101
107
  }
102
108
  }
103
109
 
104
- public async setScenePath(scenePath: string): Promise<void> {
110
+ public setScenePath = async (scenePath: string): Promise<void> => {
105
111
  if (!this.appProxy.box) return;
106
112
  this.appProxy.setFullPath(scenePath);
107
113
  }
108
114
 
109
- public mountView(dom: HTMLDivElement): void {
115
+ public mountView = (dom: HTMLDivElement): void => {
110
116
  const view = this.getView();
111
117
  if (view) {
112
118
  view.divElement = dom;
@@ -117,15 +123,41 @@ export class AppContext<TAttrs extends Record<string, any> = any, AppOptions = a
117
123
  }
118
124
  }
119
125
 
120
- public getAppOptions(): AppOptions | undefined {
121
- return typeof this.appOptions === 'function' ? (this.appOptions as () => AppOptions)() : this.appOptions
126
+ /** Get the local App options. */
127
+ public getAppOptions = (): TAppOptions | undefined => {
128
+ return typeof this.appOptions === 'function' ? (this.appOptions as () => TAppOptions)() : this.appOptions
122
129
  }
123
130
 
124
- public createStorage<TState>(storeId: string, defaultState?: TState): Storage<TState> {
131
+ private _storage?: Storage<TAttributes>
132
+
133
+ /** Main Storage for attributes. */
134
+ public get storage(): Storage<TAttributes> {
135
+ if (!this._storage) {
136
+ this._storage = new Storage(this);
137
+ }
138
+ return this._storage;
139
+ }
140
+
141
+ /**
142
+ * Create separated storages for flexible state management.
143
+ * @param storeId Namespace for the storage. Storages of the same namespace share the same data.
144
+ * @param defaultState Default state for initial storage creation.
145
+ * @returns
146
+ */
147
+ public createStorage = <TState>(storeId: string, defaultState?: TState): Storage<TState> => {
125
148
  const storage = new Storage(this, storeId, defaultState);
126
149
  this.emitter.on("destroy", () => {
127
150
  storage.destroy();
128
151
  });
129
152
  return storage;
130
153
  }
154
+
155
+ /** Dispatch events to other clients (and self). */
156
+ public dispatchMagixEvent: MagixEventDispatcher<TMagixEventPayloads> = (this.manager.displayer as Room).dispatchMagixEvent.bind(this.manager.displayer)
157
+
158
+ /** Listen to events from others clients (and self messages). */
159
+ public addMagixEventListener: MagixEventAddListener<TMagixEventPayloads> = this.manager.displayer.addMagixEventListener.bind(this.manager.displayer)
160
+
161
+ /** Remove a Magix event listener. */
162
+ public removeMagixEventListener = this.manager.displayer.removeMagixEventListener.bind(this.manager.displayer) as MagixEventRemoveListener<TMagixEventPayloads>
131
163
  }
package/src/AppManager.ts CHANGED
@@ -30,6 +30,7 @@ export class AppManager {
30
30
  public boxManager?: BoxManager;
31
31
 
32
32
  private _prevSceneIndex: number | undefined;
33
+ private _prevFocused: string | undefined;
33
34
 
34
35
  constructor(public windowManger: WindowManager) {
35
36
  this.displayer = windowManger.displayer;
@@ -106,6 +107,15 @@ export class AppManager {
106
107
  }
107
108
  });
108
109
  });
110
+ this.refresher?.add("focusedChange", () => {
111
+ return autorun(() => {
112
+ const focused = get(this.attributes, "focus");
113
+ if (this._prevFocused !== focused) {
114
+ callbacks.emit("focusedChange", focused);
115
+ this._prevFocused = focused;
116
+ }
117
+ });
118
+ })
109
119
  if (!this.attributes.apps || Object.keys(this.attributes.apps).length === 0) {
110
120
  const mainScenePath = this.store.getMainViewScenePath();
111
121
  if (!mainScenePath) return;
@@ -116,6 +126,7 @@ export class AppManager {
116
126
  }
117
127
  this.displayerWritableListener(!this.room?.isWritable);
118
128
  this.displayer.callbacks.on("onEnableWriteNowChanged", this.displayerWritableListener);
129
+ this._prevFocused = this.attributes.focus;
119
130
  }
120
131
 
121
132
  /**
package/src/index.ts CHANGED
@@ -151,6 +151,7 @@ export type PublicEvent = {
151
151
  cameraStateChange: CameraState;
152
152
  mainViewScenePathChange: string;
153
153
  mainViewSceneIndexChange: number;
154
+ focusedChange: string | undefined;
154
155
  };
155
156
 
156
157
  export type MountParams = {
package/src/typings.ts CHANGED
@@ -12,7 +12,7 @@ import type {
12
12
  import type { AppContext } from "./AppContext";
13
13
  import type { ReadonlyTeleBox, TeleBoxRect } from "@netless/telebox-insider";
14
14
 
15
- export interface NetlessApp<Attributes = any, SetupResult = any, AppOptions = any> {
15
+ export interface NetlessApp<Attributes = any, MagixEventPayloads = any, AppOptions = any, SetupResult = any> {
16
16
  kind: string;
17
17
  config?: {
18
18
  /** Box width relative to whiteboard. 0~1. Default 0.5. */
@@ -28,7 +28,7 @@ export interface NetlessApp<Attributes = any, SetupResult = any, AppOptions = an
28
28
  /** App only single instance. */
29
29
  singleton?: boolean;
30
30
  };
31
- setup: (context: AppContext<Attributes, AppOptions>) => SetupResult;
31
+ setup: (context: AppContext<Attributes, MagixEventPayloads, AppOptions>) => SetupResult;
32
32
  }
33
33
 
34
34
  export type AppEmitterEvent<T = any> = {