@netless/window-manager 0.4.0-canary.5 → 0.4.0-canary.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netless/window-manager",
3
- "version": "0.4.0-canary.5",
3
+ "version": "0.4.0-canary.6",
4
4
  "description": "",
5
5
  "main": "dist/index.es.js",
6
6
  "module": "dist/index.es.js",
@@ -0,0 +1,21 @@
1
+ export type StorageEventListener<T> = (event: T) => void;
2
+
3
+ export class StorageEvent<TMessage> {
4
+ listeners = new Set<StorageEventListener<TMessage>>();
5
+
6
+ get length(): number {
7
+ return this.listeners.size;
8
+ }
9
+
10
+ dispatch(message: TMessage): void {
11
+ this.listeners.forEach(callback => callback(message));
12
+ }
13
+
14
+ addListener(listener: StorageEventListener<TMessage>): void {
15
+ this.listeners.add(listener);
16
+ }
17
+
18
+ removeListener(listener: StorageEventListener<TMessage>): void {
19
+ this.listeners.delete(listener);
20
+ }
21
+ }
@@ -0,0 +1,243 @@
1
+ import type { AkkoObjectUpdatedProperty } from "white-web-sdk";
2
+ import { get, has, isObject } from "lodash";
3
+ import { SideEffectManager } from "side-effect-manager";
4
+ import type { AppContext } from "../../AppContext";
5
+ import { safeListenPropsUpdated } from "../../Utils/Reactive";
6
+ import { isRef, makeRef, plainObjectKeys } from "./utils";
7
+ import type { Diff, MaybeRefValue, RefValue } from "./typings";
8
+ import { StorageEvent } from "./StorageEvent";
9
+
10
+ export * from './typings';
11
+
12
+ const STORAGE_NS = "_WM-STORAGE_";
13
+
14
+ export class Storage<TState = any> implements Storage<TState> {
15
+ readonly id: string;
16
+
17
+ private readonly _context: AppContext<{ [STORAGE_NS]: TState }>;
18
+ private readonly _sideEffect = new SideEffectManager();
19
+ private _state: TState;
20
+ private _destroyed = false;
21
+
22
+ private _refMap = new WeakMap<any, RefValue>();
23
+
24
+ /**
25
+ * `setState` alters local state immediately before sending to server. This will cache the old value for onStateChanged diffing.
26
+ */
27
+ private _lastValue = new Map<string | number | symbol, TState[Extract<keyof TState, string>]>();
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
+
34
+ if (defaultState && !isObject(defaultState)) {
35
+ throw new Error(`Default state for Storage ${id} is not an object.`);
36
+ }
37
+
38
+ this._context = context;
39
+ this.id = id;
40
+
41
+ const attrs = context.getAttributes();
42
+ this._state = {} as TState;
43
+ const rawState = get<TState>(attrs, [STORAGE_NS, id], this._state);
44
+
45
+ if (this._context.getIsWritable()) {
46
+ if (!isObject(rawState) || rawState === this._state) {
47
+ if (!attrs[STORAGE_NS]) {
48
+ this._context.updateAttributes([STORAGE_NS], {});
49
+ }
50
+ 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
+ });
71
+ }
72
+ }
73
+
74
+ this._sideEffect.addDisposer(
75
+ safeListenPropsUpdated(
76
+ () => get(context.getAttributes(), [STORAGE_NS, this.id]),
77
+ this._updateProperties.bind(this),
78
+ this.destroy.bind(this)
79
+ )
80
+ );
81
+ }
82
+
83
+ get state(): Readonly<TState> {
84
+ if (this._destroyed) {
85
+ console.warn(`Accessing state on destroyed Storage "${this.id}"`)
86
+ }
87
+ return this._state;
88
+ }
89
+
90
+ readonly onStateChanged = new StorageEvent<Diff<TState>>();
91
+
92
+ ensureState(state: Partial<TState>): void {
93
+ return this.setState(
94
+ plainObjectKeys(state).reduce((payload, key) => {
95
+ if (!has(this._state, key)) {
96
+ payload[key] = state[key];
97
+ }
98
+ return payload;
99
+ }, {} as Partial<TState>)
100
+ );
101
+ }
102
+
103
+ setState(state: Partial<TState>): void {
104
+ if (this._destroyed) {
105
+ console.error(new Error(`Cannot call setState on destroyed Storage "${this.id}".`));
106
+ return;
107
+ }
108
+
109
+ if (!this._context.getIsWritable()) {
110
+ console.error(new Error(`Cannot setState on Storage "${this.id}" without writable access`), state);
111
+ return;
112
+ }
113
+
114
+ const keys = plainObjectKeys(state);
115
+ if (keys.length > 0) {
116
+ keys.forEach(key => {
117
+ const value = state[key];
118
+ if (value === this._state[key]) {
119
+ return;
120
+ }
121
+
122
+ if (value === void 0) {
123
+ this._lastValue.set(key, this._state[key]);
124
+ delete this._state[key];
125
+ this._context.updateAttributes([STORAGE_NS, this.id, key], value);
126
+ } else {
127
+ this._lastValue.set(key, this._state[key]);
128
+ this._state[key] = value as TState[Extract<keyof TState, string>];
129
+
130
+ let payload: MaybeRefValue<typeof value> = value;
131
+ if (isObject(value)) {
132
+ let refValue = this._refMap.get(value);
133
+ if (!refValue) {
134
+ refValue = makeRef(value);
135
+ this._refMap.set(value, refValue);
136
+ }
137
+ payload = refValue;
138
+ }
139
+
140
+ this._context.updateAttributes([STORAGE_NS, this.id, key], payload);
141
+ }
142
+ });
143
+ }
144
+ }
145
+
146
+ emptyStore(): void {
147
+ if (this._destroyed) {
148
+ console.error(new Error(`Cannot empty destroyed Storage "${this.id}".`));
149
+ return;
150
+ }
151
+
152
+ if (!this._context.getIsWritable()) {
153
+ console.error(new Error(`Cannot empty Storage "${this.id}" without writable access.`));
154
+ return;
155
+ }
156
+
157
+ this._context.updateAttributes([STORAGE_NS, this.id], {});
158
+ }
159
+
160
+ deleteStore(): void {
161
+ if (!this._context.getIsWritable()) {
162
+ console.error(new Error(`Cannot delete Storage "${this.id}" without writable access.`));
163
+ return;
164
+ }
165
+
166
+ this.destroy();
167
+
168
+ this._context.updateAttributes([STORAGE_NS, this.id], void 0);
169
+ }
170
+
171
+ get destroyed(): boolean {
172
+ return this._destroyed;
173
+ }
174
+
175
+ destroy() {
176
+ this._destroyed = true;
177
+ this._sideEffect.flushAll();
178
+ }
179
+
180
+ private _updateProperties(actions: ReadonlyArray<AkkoObjectUpdatedProperty<TState, string>>): void {
181
+ if (this._destroyed) {
182
+ console.error(new Error(`Cannot call _updateProperties on destroyed Storage "${this.id}".`));
183
+ return;
184
+ }
185
+
186
+ if (actions.length > 0) {
187
+ const diffs: Diff<TState> = {};
188
+
189
+ for (let i = 0; i < actions.length; i++) {
190
+ try {
191
+ const action = actions[i]
192
+ const key = action.key as Extract<keyof TState, string>;
193
+ const value = isObject(action.value) ? JSON.parse(JSON.stringify(action.value)) : action.value;
194
+ let oldValue: TState[Extract<keyof TState, string>] | undefined;
195
+ if (this._lastValue.has(key)) {
196
+ oldValue = this._lastValue.get(key);
197
+ this._lastValue.delete(key);
198
+ }
199
+
200
+ switch (action.kind) {
201
+ case 2: {
202
+ // Removed
203
+ if (has(this._state, key)) {
204
+ oldValue = this._state[key];
205
+ delete this._state[key];
206
+ }
207
+ diffs[key] = { oldValue };
208
+ break;
209
+ }
210
+ default: {
211
+ let newValue = value;
212
+
213
+ if (isRef<TState[Extract<keyof TState, string>]>(value)) {
214
+ const { k, v } = value;
215
+ const curValue = this._state[key];
216
+ if (isObject(curValue) && this._refMap.get(curValue)?.k === k) {
217
+ newValue = curValue;
218
+ } else {
219
+ newValue = v;
220
+ if (isObject(v)) {
221
+ this._refMap.set(v, value);
222
+ }
223
+ }
224
+ }
225
+
226
+ if (newValue !== this._state[key]) {
227
+ oldValue = this._state[key];
228
+ this._state[key] = newValue;
229
+ }
230
+
231
+ diffs[key] = { newValue, oldValue };
232
+ break;
233
+ }
234
+ }
235
+ } catch (e) {
236
+ console.error(e)
237
+ }
238
+ }
239
+
240
+ this.onStateChanged.dispatch(diffs);
241
+ }
242
+ }
243
+ }
@@ -0,0 +1,15 @@
1
+ export type RefValue<TValue = any> = { k: string; v: TValue; __isRef: true };
2
+
3
+ export type ExtractRawValue<TValue> = TValue extends RefValue<infer TRefValue> ? TRefValue : TValue;
4
+
5
+ export type AutoRefValue<TValue> = RefValue<ExtractRawValue<TValue>>;
6
+
7
+ export type MaybeRefValue<TValue> = TValue | AutoRefValue<TValue>;
8
+
9
+ export type DiffOne<T> = { oldValue?: T; newValue?: T };
10
+
11
+ export type Diff<T> = { [K in keyof T]?: DiffOne<T[K]> };
12
+
13
+ export type StorageOnSetStatePayload<TState = unknown> = {
14
+ [K in keyof TState]?: MaybeRefValue<TState[K]>;
15
+ };
@@ -0,0 +1,17 @@
1
+ import { has } from "lodash";
2
+ import { genUID } from "side-effect-manager";
3
+ import type { AutoRefValue, ExtractRawValue, RefValue } from "./typings";
4
+
5
+ export const plainObjectKeys = Object.keys as <T>(o: T) => Array<Extract<keyof T, string>>;
6
+
7
+ export function isRef<TValue = unknown>(e: unknown): e is RefValue<TValue> {
8
+ return Boolean(has(e, '__isRef'));
9
+ }
10
+
11
+ export function makeRef<TValue>(v: TValue): RefValue<TValue> {
12
+ return { k: genUID(), v, __isRef: true };
13
+ }
14
+
15
+ export function makeAutoRef<TValue>(v: TValue): AutoRefValue<TValue> {
16
+ return isRef<ExtractRawValue<TValue>>(v) ? v : makeRef(v as ExtractRawValue<TValue>);
17
+ }
package/src/AppContext.ts CHANGED
@@ -15,6 +15,7 @@ import type { BoxManager } from "./BoxManager";
15
15
  import type { AppEmitterEvent } from "./index";
16
16
  import type { AppManager } from "./AppManager";
17
17
  import type { AppProxy } from "./AppProxy";
18
+ import { Storage } from './App/Storage';
18
19
 
19
20
  export class AppContext<TAttrs extends Record<string, any>, AppOptions = any> {
20
21
  public readonly emitter: Emittery<AppEmitterEvent<TAttrs>>;
@@ -120,4 +121,12 @@ export class AppContext<TAttrs extends Record<string, any>, AppOptions = any> {
120
121
  public getAppOptions(): AppOptions | undefined {
121
122
  return typeof this.appOptions === 'function' ? (this.appOptions as () => AppOptions)() : this.appOptions
122
123
  }
124
+
125
+ public createStorage<TState>(storeId: string, defaultState?: TState): Storage<TState> {
126
+ const storage = new Storage(this, storeId, defaultState);
127
+ this.emitter.on("destroy", () => {
128
+ storage.destroy();
129
+ });
130
+ return storage;
131
+ }
123
132
  }
package/src/AppManager.ts CHANGED
@@ -282,7 +282,7 @@ export class AppManager {
282
282
  emitter.emit("observerIdChange", this.displayer.observerId);
283
283
  };
284
284
 
285
- private displayerWritableListener = (isReadonly: boolean) => {
285
+ public displayerWritableListener = (isReadonly: boolean) => {
286
286
  const isWritable = !isReadonly;
287
287
  const isManualWritable =
288
288
  this.windowManger.readonly === undefined || this.windowManger.readonly === false;
@@ -1,5 +1,4 @@
1
1
  import { debounce, isFunction } from "lodash";
2
- import { emitter } from "./index";
3
2
  import { log } from "./Utils/log";
4
3
  import { RoomPhase } from "white-web-sdk";
5
4
  import type { Room } from "white-web-sdk";
@@ -1,5 +1,6 @@
1
1
  import { listenUpdated, unlistenUpdated, reaction, UpdateEventKind } from "white-web-sdk";
2
2
  import type { AkkoObjectUpdatedProperty , AkkoObjectUpdatedListener } from "white-web-sdk";
3
+ import { isObject } from "lodash";
3
4
 
4
5
  // 兼容 13 和 14 版本 SDK
5
6
  export const onObjectByEvent = (event: UpdateEventKind) => {
@@ -30,7 +31,8 @@ export const onObjectByEvent = (event: UpdateEventKind) => {
30
31
 
31
32
  export const safeListenPropsUpdated = <T>(
32
33
  getProps: () => T,
33
- callback: AkkoObjectUpdatedListener<T>
34
+ callback: AkkoObjectUpdatedListener<T>,
35
+ onDestroyed?: (props: unknown) => void
34
36
  ) => {
35
37
  let disposeListenUpdated: (() => void) | null = null;
36
38
  const disposeReaction = reaction(
@@ -41,8 +43,12 @@ export const safeListenPropsUpdated = <T>(
41
43
  disposeListenUpdated = null;
42
44
  }
43
45
  const props = getProps();
44
- disposeListenUpdated = () => unlistenUpdated(props, callback);
45
- listenUpdated(props, callback);
46
+ if (isObject(props)) {
47
+ disposeListenUpdated = () => unlistenUpdated(props, callback);
48
+ listenUpdated(props, callback);
49
+ } else {
50
+ onDestroyed?.(props);
51
+ }
46
52
  },
47
53
  { fireImmediately: true }
48
54
  );
package/src/index.ts CHANGED
@@ -186,7 +186,7 @@ export class WindowManager extends InvisiblePlugin<WindowMangerAttributes> {
186
186
  public static containerSizeRatio = DEFAULT_CONTAINER_RATIO;
187
187
  private static isCreated = false;
188
188
 
189
- public version = "0.4.0-canary.5";
189
+ public version = "0.4.0-canary.6";
190
190
 
191
191
  public appListeners?: AppListeners;
192
192
 
@@ -479,10 +479,8 @@ export class WindowManager extends InvisiblePlugin<WindowMangerAttributes> {
479
479
  * 设置所有 app 的 readonly 模式
480
480
  */
481
481
  public setReadonly(readonly: boolean): void {
482
- if (this.room?.isWritable) {
483
- this.readonly = readonly;
484
- this.appManager?.boxManager?.setReadonly(readonly);
485
- }
482
+ this.readonly = readonly;
483
+ this.boxManager?.setReadonly(readonly);
486
484
  }
487
485
 
488
486
  /**