Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit c200b45

Browse files
authored
Merge branch 'main' into fix/avoid-cross-geo-calls
2 parents c9f877b + 145acb8 commit c200b45

File tree

5 files changed

+218
-7
lines changed

5 files changed

+218
-7
lines changed

Composer/packages/client/src/App.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22
// Licensed under the MIT License.
33

44
import React, { Fragment, useEffect, useState } from 'react';
5-
import { useRecoilValue } from 'recoil';
5+
import { useRecoilValue, useRecoilCallback, CallbackInterface } from 'recoil';
6+
import { useMount, useUnmount } from '@fluentui/react-hooks';
67

78
import { Header } from './components/Header';
89
import { Announcement } from './components/AppComponents/Announcement';
910
import { MainContainer } from './components/AppComponents/MainContainer';
10-
import { dispatcherState, userSettingsState } from './recoilModel';
11+
import { dispatcherState, userSettingsState, lgFileState } from './recoilModel';
1112
import { loadLocale } from './utils/fileUtil';
1213
import { useInitializeLogger } from './telemetry/useInitializeLogger';
1314
import { setupIcons } from './setupIcons';
1415
import { setOneAuthEnabled } from './utils/oneAuthUtil';
1516
import { LoadingSpinner } from './components/LoadingSpinner';
17+
import lgWorker from './recoilModel/parsers/lgWorker';
18+
import { LgEventType } from './recoilModel/parsers/types';
1619

1720
setupIcons();
1821

@@ -26,6 +29,7 @@ export const App: React.FC = () => {
2629
const { appLocale } = useRecoilValue(userSettingsState);
2730

2831
const [isClosing, setIsClosing] = useState(false);
32+
const [listener, setListener] = useState<{ destroy(): boolean }>({} as any);
2933

3034
const {
3135
fetchExtensions,
@@ -34,6 +38,19 @@ export const App: React.FC = () => {
3438
performAppCleanupOnQuit,
3539
setMachineInfo,
3640
} = useRecoilValue(dispatcherState);
41+
const updateFile = useRecoilCallback((callbackHelpers: CallbackInterface) => async ({ projectId, value }) => {
42+
callbackHelpers.set(lgFileState({ projectId, lgFileId: value.id }), value);
43+
});
44+
45+
useMount(() => {
46+
const listener = lgWorker.listen(LgEventType.OnUpdateLgFile, (msg) => {
47+
const { projectId, payload } = msg.data;
48+
updateFile({ projectId, value: payload });
49+
});
50+
setListener(listener);
51+
});
52+
53+
useUnmount(() => listener.destroy());
3754

3855
useEffect(() => {
3956
loadLocale(appLocale);

Composer/packages/client/src/recoilModel/parsers/lgWorker.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Worker from './workers/lgParser.worker.ts';
66
import { BaseWorker } from './baseWorker';
77
import {
88
LgActionType,
9+
LgEventType,
910
LgParsePayload,
1011
LgUpdateTemplatePayload,
1112
LgCreateTemplatePayload,
@@ -20,6 +21,39 @@ import {
2021

2122
// Wrapper class
2223
class LgWorker extends BaseWorker<LgActionType> {
24+
private listeners = new Map<LgEventType, ((msg: MessageEvent) => void)[]>();
25+
26+
constructor(worker: Worker) {
27+
super(worker);
28+
29+
worker.onmessage = (msg) => {
30+
const { type } = msg.data;
31+
32+
if (type === LgEventType.OnUpdateLgFile) {
33+
this.listeners.get(type)?.forEach((cb) => cb(msg));
34+
} else {
35+
this.handleMsg(msg);
36+
}
37+
};
38+
}
39+
40+
listen(action: LgEventType, callback: (msg: MessageEvent) => void) {
41+
if (this.listeners.has(action)) {
42+
this.listeners.get(action)!.push(callback);
43+
} else {
44+
this.listeners.set(action, [callback]);
45+
}
46+
47+
return {
48+
destroy: () => this.listeners.delete(action),
49+
};
50+
}
51+
52+
flush(): Promise<boolean> {
53+
this.listeners.clear();
54+
return super.flush();
55+
}
56+
2357
addProject(projectId: string) {
2458
return this.sendMsg<LgNewCachePayload>(LgActionType.NewCache, { projectId });
2559
}

Composer/packages/client/src/recoilModel/parsers/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ export enum LgActionType {
156156
ParseAll = 'parse-all',
157157
}
158158

159+
export enum LgEventType {
160+
OnUpdateLgFile = 'on-update-lgfile',
161+
}
162+
159163
export enum IndexerActionType {
160164
Index = 'index',
161165
}

Composer/packages/client/src/recoilModel/parsers/workers/lgParser.worker.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { lgImportResolverGenerator, LgFile } from '@bfc/shared';
55

66
import {
77
LgActionType,
8+
LgEventType,
89
LgParsePayload,
910
LgUpdateTemplatePayload,
1011
LgCreateTemplatePayload,
@@ -16,6 +17,7 @@ import {
1617
LgCleanCachePayload,
1718
LgParseAllPayload,
1819
} from '../types';
20+
import { MapOptimizer } from '../../utils/mapOptimizer';
1921

2022
const ctx: Worker = self as any;
2123

@@ -197,6 +199,11 @@ export const handleMessage = (msg: LgMessageEvent) => {
197199
case LgActionType.Parse: {
198200
const { id, content, lgFiles, projectId } = msg.payload;
199201

202+
const cachedFile = cache.get(projectId, id);
203+
if (cachedFile?.isContentUnparsed === false && cachedFile?.content === content) {
204+
return filterParseResult(cachedFile);
205+
}
206+
200207
const lgFile = lgUtil.parse(id, content, lgFiles);
201208
cache.set(projectId, lgFile);
202209
payload = filterParseResult(lgFile);
@@ -206,12 +213,20 @@ export const handleMessage = (msg: LgMessageEvent) => {
206213
case LgActionType.ParseAll: {
207214
const { lgResources, projectId } = msg.payload;
208215
// We'll do the parsing when the file is required. Save empty LG instead.
209-
payload = lgResources.map(({ id, content }) => {
210-
const emptyLg = emptyLgFile(id, content);
211-
cache.set(projectId, emptyLg);
212-
return filterParseResult(emptyLg);
216+
payload = lgResources.map(({ id, content }) => [id, emptyLgFile(id, content)]);
217+
const resources = new Map<string, LgFile>(payload);
218+
cache.projects.set(projectId, resources);
219+
220+
const optimizer = new MapOptimizer(10, resources);
221+
optimizer.onUpdate((_, value, ctx) => {
222+
const refs = value.parseResult?.references?.map(({ name }) => name);
223+
ctx.setReferences(refs);
224+
});
225+
optimizer.onDelete((_, value) => {
226+
const lgFile = emptyLgFile(value.id, value.content);
227+
cache.set(projectId, lgFile);
228+
ctx.postMessage({ type: LgEventType.OnUpdateLgFile, projectId, payload: lgFile });
213229
});
214-
215230
break;
216231
}
217232

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* Internal tree structure to track the oldest elements and their references.
6+
*/
7+
interface MapOptimizerTree<Key> {
8+
timestamp: number;
9+
references: Key[];
10+
}
11+
12+
/**
13+
* Context for the MapOptimizer.onUpdate event.
14+
*/
15+
interface OnUpdateMapOptimizerContext<Key> {
16+
/**
17+
* Sets the related Map keys references of an element, these references are taken into account on the delete event.
18+
* @param references The Map keys of a related element.
19+
*/
20+
setReferences(references: Key[]): void;
21+
}
22+
23+
/**
24+
* Class to optimize a Map object by deleting the oldest elements of the collection based on a capacity limit.
25+
*/
26+
export class MapOptimizer<Key, Value> {
27+
public tree = new Map<Key, MapOptimizerTree<Key>>();
28+
private skipOptimize = new Set<Key>();
29+
30+
onUpdateCallback?: (key: Key, value: Value, ctx: OnUpdateMapOptimizerContext<Key>) => void;
31+
onDeleteCallback?: (key: Key, value: Value) => void;
32+
33+
/**
34+
* Initializes a new instance of the MapOptimizer class.
35+
* @param capacity The capacity limit to trigger the optimization steps.
36+
* @param list The Map object to optimize.
37+
*/
38+
constructor(private capacity: number, public list: Map<Key, Value>) {
39+
this.attach();
40+
}
41+
42+
/**
43+
* Event triggered when an element is added or updated in the Map object.
44+
* @param callback Exposes the element's Key, Value and Context to perform operations.
45+
*/
46+
onUpdate(callback: (key: Key, value: Value, ctx: OnUpdateMapOptimizerContext<Key>) => void) {
47+
this.onUpdateCallback = callback;
48+
}
49+
50+
/**
51+
* Event triggered when an element is marked for deletion.
52+
* @param callback Exposes the element's Key, Value.
53+
*/
54+
onDelete(callback: (key: Key, value: Value) => void) {
55+
this.onDeleteCallback = callback;
56+
}
57+
58+
/**
59+
* @private
60+
* Attaches the "set" method to the Map object to listen and trigger the optimization.
61+
*/
62+
private attach() {
63+
const set = this.list.set;
64+
this.list.set = (key, value) => {
65+
if (!this.skipOptimize.has(key)) {
66+
this.optimize(key, value);
67+
}
68+
const result = set.apply(this.list, [key, value]);
69+
return result;
70+
};
71+
}
72+
73+
/**
74+
* @private
75+
* Optimizes the Map object by performing the onDelete event callback on the oldest element in the collection.
76+
*/
77+
private optimize(keyToAdd: Key, valueToAdd: Value) {
78+
const exists = this.tree.has(keyToAdd);
79+
const context: MapOptimizerTree<Key> = { timestamp: Date.now(), references: [] };
80+
this.onUpdateCallback?.(keyToAdd, valueToAdd, {
81+
setReferences: (references) => (context.references = references || []),
82+
});
83+
this.tree.set(keyToAdd, context);
84+
85+
if (exists) {
86+
return;
87+
}
88+
89+
let processed: [Key, MapOptimizerTree<Key>][] = [];
90+
const itemsToRemove = Array.from(this.tree.entries())
91+
.filter(([key]) => key !== keyToAdd)
92+
.sort(([, v1], [, v2]) => v2.timestamp - v1.timestamp);
93+
94+
while (this.capacity < this.tree.size) {
95+
const itemToRemove = itemsToRemove.pop();
96+
if (!itemToRemove) {
97+
break;
98+
}
99+
100+
const [key, { references }] = itemToRemove;
101+
const ids = this.identify([key, ...references]);
102+
103+
// Re-process previous items if an item gets deleted.
104+
processed.push(itemToRemove);
105+
if (ids.length > 0) {
106+
itemsToRemove.push(...processed);
107+
processed = [];
108+
}
109+
110+
for (const id of ids) {
111+
this.tree.delete(id);
112+
const listItem = this.list.get(id)!;
113+
this.skipOptimize.add(id);
114+
this.onDeleteCallback ? this.onDeleteCallback(id, listItem) : this.list.delete(id);
115+
this.skipOptimize.delete(id);
116+
}
117+
}
118+
}
119+
120+
/**
121+
* @private
122+
* Identifies all the keys that are available to delete.
123+
*/
124+
private identify(references: Key[], memo: Key[] = []) {
125+
for (const reference of references) {
126+
const found = this.tree.get(reference);
127+
const existsOnMemo = () => memo.some((e) => found!.references.includes(e));
128+
const existsOnReferences = () =>
129+
Array.from(this.tree.values()).some(({ references }) => references.includes(reference));
130+
131+
if (!found || existsOnMemo() || existsOnReferences()) {
132+
continue;
133+
}
134+
135+
memo.push(reference);
136+
this.identify(found.references, memo);
137+
}
138+
139+
return memo;
140+
}
141+
}

0 commit comments

Comments
 (0)