diff options
-rw-r--r-- | subprojects/frontend/package.json | 18 | ||||
-rw-r--r-- | subprojects/frontend/src/utils/ConditionVariable.ts | 64 | ||||
-rw-r--r-- | subprojects/frontend/src/utils/PendingTask.ts | 20 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/ContentAssistService.ts | 8 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/HighlightingService.ts | 12 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/OccurrencesService.ts | 119 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/UpdateService.ts | 296 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/ValidationService.ts | 12 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/XtextClient.ts | 6 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/xtextServiceResults.ts | 5 | ||||
-rw-r--r-- | yarn.lock | 232 |
11 files changed, 417 insertions, 375 deletions
diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json index 448b9710..7cef17fe 100644 --- a/subprojects/frontend/package.json +++ b/subprojects/frontend/package.json | |||
@@ -31,17 +31,16 @@ | |||
31 | "@codemirror/view": "^6.2.0", | 31 | "@codemirror/view": "^6.2.0", |
32 | "@emotion/react": "^11.10.0", | 32 | "@emotion/react": "^11.10.0", |
33 | "@emotion/styled": "^11.10.0", | 33 | "@emotion/styled": "^11.10.0", |
34 | "@fontsource/inter": "^4.5.12", | ||
35 | "@fontsource/jetbrains-mono": "^4.5.10", | 34 | "@fontsource/jetbrains-mono": "^4.5.10", |
36 | "@fontsource/roboto": "^4.5.8", | 35 | "@fontsource/roboto": "^4.5.8", |
37 | "@lezer/common": "^1.0.0", | 36 | "@lezer/common": "^1.0.0", |
38 | "@lezer/highlight": "^1.0.0", | 37 | "@lezer/highlight": "^1.0.0", |
39 | "@lezer/lr": "^1.2.3", | 38 | "@lezer/lr": "^1.2.3", |
40 | "@material-icons/svg": "^1.0.32", | 39 | "@material-icons/svg": "^1.0.32", |
41 | "@mui/icons-material": "5.8.4", | 40 | "@mui/icons-material": "5.10.2", |
42 | "@mui/material": "5.10.1", | 41 | "@mui/material": "5.10.2", |
43 | "@types/lodash-es": "^4.17.6", | ||
44 | "ansi-styles": "^6.1.0", | 42 | "ansi-styles": "^6.1.0", |
43 | "async-mutex": "^0.3.2", | ||
45 | "escape-string-regexp": "^5.0.0", | 44 | "escape-string-regexp": "^5.0.0", |
46 | "lodash-es": "^4.17.21", | 45 | "lodash-es": "^4.17.21", |
47 | "loglevel": "^1.8.0", | 46 | "loglevel": "^1.8.0", |
@@ -58,19 +57,20 @@ | |||
58 | "@lezer/generator": "^1.1.1", | 57 | "@lezer/generator": "^1.1.1", |
59 | "@types/eslint": "^8.4.6", | 58 | "@types/eslint": "^8.4.6", |
60 | "@types/html-minifier-terser": "^7.0.0", | 59 | "@types/html-minifier-terser": "^7.0.0", |
61 | "@types/node": "^18.7.8", | 60 | "@types/lodash-es": "^4.17.6", |
61 | "@types/node": "^18.7.13", | ||
62 | "@types/prettier": "^2.7.0", | 62 | "@types/prettier": "^2.7.0", |
63 | "@types/react": "^18.0.17", | 63 | "@types/react": "^18.0.17", |
64 | "@types/react-dom": "^18.0.6", | 64 | "@types/react-dom": "^18.0.6", |
65 | "@typescript-eslint/eslint-plugin": "^5.33.1", | 65 | "@typescript-eslint/eslint-plugin": "^5.35.1", |
66 | "@typescript-eslint/parser": "^5.33.1", | 66 | "@typescript-eslint/parser": "^5.35.1", |
67 | "@vitejs/plugin-react": "^2.0.1", | 67 | "@vitejs/plugin-react": "^2.0.1", |
68 | "cross-env": "^7.0.3", | 68 | "cross-env": "^7.0.3", |
69 | "eslint": "^8.22.0", | 69 | "eslint": "^8.22.0", |
70 | "eslint-config-airbnb": "^19.0.4", | 70 | "eslint-config-airbnb": "^19.0.4", |
71 | "eslint-config-airbnb-typescript": "^17.0.0", | 71 | "eslint-config-airbnb-typescript": "^17.0.0", |
72 | "eslint-config-prettier": "^8.5.0", | 72 | "eslint-config-prettier": "^8.5.0", |
73 | "eslint-import-resolver-typescript": "^3.4.2", | 73 | "eslint-import-resolver-typescript": "^3.5.0", |
74 | "eslint-plugin-import": "^2.26.0", | 74 | "eslint-plugin-import": "^2.26.0", |
75 | "eslint-plugin-jsx-a11y": "^6.6.1", | 75 | "eslint-plugin-jsx-a11y": "^6.6.1", |
76 | "eslint-plugin-prettier": "^4.2.1", | 76 | "eslint-plugin-prettier": "^4.2.1", |
@@ -80,7 +80,7 @@ | |||
80 | "prettier": "^2.7.1", | 80 | "prettier": "^2.7.1", |
81 | "typescript": "~4.7.4", | 81 | "typescript": "~4.7.4", |
82 | "vite": "^3.0.9", | 82 | "vite": "^3.0.9", |
83 | "vite-plugin-inject-preload": "^1.0.1", | 83 | "vite-plugin-inject-preload": "^1.1.0", |
84 | "vite-plugin-pwa": "^0.12.3", | 84 | "vite-plugin-pwa": "^0.12.3", |
85 | "workbox-window": "^6.5.4" | 85 | "workbox-window": "^6.5.4" |
86 | } | 86 | } |
diff --git a/subprojects/frontend/src/utils/ConditionVariable.ts b/subprojects/frontend/src/utils/ConditionVariable.ts deleted file mode 100644 index 1d3431f7..00000000 --- a/subprojects/frontend/src/utils/ConditionVariable.ts +++ /dev/null | |||
@@ -1,64 +0,0 @@ | |||
1 | import PendingTask from './PendingTask'; | ||
2 | import getLogger from './getLogger'; | ||
3 | |||
4 | const log = getLogger('utils.ConditionVariable'); | ||
5 | |||
6 | export type Condition = () => boolean; | ||
7 | |||
8 | export default class ConditionVariable { | ||
9 | private readonly condition: Condition; | ||
10 | |||
11 | private readonly defaultTimeout: number; | ||
12 | |||
13 | private listeners: PendingTask<void>[] = []; | ||
14 | |||
15 | constructor(condition: Condition, defaultTimeout = 0) { | ||
16 | this.condition = condition; | ||
17 | this.defaultTimeout = defaultTimeout; | ||
18 | } | ||
19 | |||
20 | async waitFor(timeoutMs?: number | undefined): Promise<void> { | ||
21 | if (this.condition()) { | ||
22 | return; | ||
23 | } | ||
24 | const timeoutOrDefault = timeoutMs ?? this.defaultTimeout; | ||
25 | let nowMs = Date.now(); | ||
26 | const endMs = nowMs + timeoutOrDefault; | ||
27 | while (!this.condition() && nowMs < endMs) { | ||
28 | const remainingMs = endMs - nowMs; | ||
29 | const promise = new Promise<void>((resolve, reject) => { | ||
30 | if (this.condition()) { | ||
31 | resolve(); | ||
32 | return; | ||
33 | } | ||
34 | const task = new PendingTask(resolve, reject, remainingMs); | ||
35 | this.listeners.push(task); | ||
36 | }); | ||
37 | // We must keep waiting until the update has completed, | ||
38 | // so the tasks can't be started in parallel. | ||
39 | // eslint-disable-next-line no-await-in-loop | ||
40 | await promise; | ||
41 | nowMs = Date.now(); | ||
42 | } | ||
43 | if (!this.condition()) { | ||
44 | log.error('Condition still does not hold after', timeoutOrDefault, 'ms'); | ||
45 | throw new Error('Failed to wait for condition'); | ||
46 | } | ||
47 | } | ||
48 | |||
49 | notifyAll(): void { | ||
50 | this.clearListenersWith((listener) => listener.resolve()); | ||
51 | } | ||
52 | |||
53 | rejectAll(error: unknown): void { | ||
54 | this.clearListenersWith((listener) => listener.reject(error)); | ||
55 | } | ||
56 | |||
57 | private clearListenersWith(callback: (listener: PendingTask<void>) => void) { | ||
58 | // Copy `listeners` so that we don't get into a race condition | ||
59 | // if one of the listeners adds another listener. | ||
60 | const { listeners } = this; | ||
61 | this.listeners = []; | ||
62 | listeners.forEach(callback); | ||
63 | } | ||
64 | } | ||
diff --git a/subprojects/frontend/src/utils/PendingTask.ts b/subprojects/frontend/src/utils/PendingTask.ts index 3976bdf9..205c8452 100644 --- a/subprojects/frontend/src/utils/PendingTask.ts +++ b/subprojects/frontend/src/utils/PendingTask.ts | |||
@@ -14,21 +14,19 @@ export default class PendingTask<T> { | |||
14 | constructor( | 14 | constructor( |
15 | resolveCallback: (value: T) => void, | 15 | resolveCallback: (value: T) => void, |
16 | rejectCallback: (reason?: unknown) => void, | 16 | rejectCallback: (reason?: unknown) => void, |
17 | timeoutMs?: number | undefined, | 17 | timeoutMs: number | undefined, |
18 | timeoutCallback?: () => void | undefined, | 18 | timeoutCallback: () => void | undefined, |
19 | ) { | 19 | ) { |
20 | this.resolveCallback = resolveCallback; | 20 | this.resolveCallback = resolveCallback; |
21 | this.rejectCallback = rejectCallback; | 21 | this.rejectCallback = rejectCallback; |
22 | if (timeoutMs) { | 22 | this.timeout = setTimeout(() => { |
23 | this.timeout = setTimeout(() => { | 23 | if (!this.resolved) { |
24 | if (!this.resolved) { | 24 | this.reject(new Error('Request timed out')); |
25 | this.reject(new Error('Request timed out')); | 25 | if (timeoutCallback) { |
26 | if (timeoutCallback) { | 26 | timeoutCallback(); |
27 | timeoutCallback(); | ||
28 | } | ||
29 | } | 27 | } |
30 | }, timeoutMs); | 28 | } |
31 | } | 29 | }, timeoutMs); |
32 | } | 30 | } |
33 | 31 | ||
34 | resolve(value: T): void { | 32 | resolve(value: T): void { |
diff --git a/subprojects/frontend/src/xtext/ContentAssistService.ts b/subprojects/frontend/src/xtext/ContentAssistService.ts index 39042812..9e41f57b 100644 --- a/subprojects/frontend/src/xtext/ContentAssistService.ts +++ b/subprojects/frontend/src/xtext/ContentAssistService.ts | |||
@@ -104,13 +104,9 @@ function createCompletion(entry: ContentAssistEntry): Completion { | |||
104 | } | 104 | } |
105 | 105 | ||
106 | export default class ContentAssistService { | 106 | export default class ContentAssistService { |
107 | private readonly updateService: UpdateService; | ||
108 | |||
109 | private lastCompletion: CompletionResult | undefined; | 107 | private lastCompletion: CompletionResult | undefined; |
110 | 108 | ||
111 | constructor(updateService: UpdateService) { | 109 | constructor(private readonly updateService: UpdateService) {} |
112 | this.updateService = updateService; | ||
113 | } | ||
114 | 110 | ||
115 | onTransaction(transaction: Transaction): void { | 111 | onTransaction(transaction: Transaction): void { |
116 | if (this.shouldInvalidateCachedCompletion(transaction)) { | 112 | if (this.shouldInvalidateCachedCompletion(transaction)) { |
@@ -159,8 +155,6 @@ export default class ContentAssistService { | |||
159 | this.lastCompletion = undefined; | 155 | this.lastCompletion = undefined; |
160 | const entries = await this.updateService.fetchContentAssist( | 156 | const entries = await this.updateService.fetchContentAssist( |
161 | { | 157 | { |
162 | resource: this.updateService.resourceName, | ||
163 | serviceType: 'assist', | ||
164 | caretOffset: context.pos, | 158 | caretOffset: context.pos, |
165 | proposalsLimit: PROPOSALS_LIMIT, | 159 | proposalsLimit: PROPOSALS_LIMIT, |
166 | }, | 160 | }, |
diff --git a/subprojects/frontend/src/xtext/HighlightingService.ts b/subprojects/frontend/src/xtext/HighlightingService.ts index cf618b96..f9ab7b7e 100644 --- a/subprojects/frontend/src/xtext/HighlightingService.ts +++ b/subprojects/frontend/src/xtext/HighlightingService.ts | |||
@@ -5,14 +5,10 @@ import type UpdateService from './UpdateService'; | |||
5 | import { highlightingResult } from './xtextServiceResults'; | 5 | import { highlightingResult } from './xtextServiceResults'; |
6 | 6 | ||
7 | export default class HighlightingService { | 7 | export default class HighlightingService { |
8 | private readonly store: EditorStore; | 8 | constructor( |
9 | 9 | private readonly store: EditorStore, | |
10 | private readonly updateService: UpdateService; | 10 | private readonly updateService: UpdateService, |
11 | 11 | ) {} | |
12 | constructor(store: EditorStore, updateService: UpdateService) { | ||
13 | this.store = store; | ||
14 | this.updateService = updateService; | ||
15 | } | ||
16 | 12 | ||
17 | onPush(push: unknown): void { | 13 | onPush(push: unknown): void { |
18 | const { regions } = highlightingResult.parse(push); | 14 | const { regions } = highlightingResult.parse(push); |
diff --git a/subprojects/frontend/src/xtext/OccurrencesService.ts b/subprojects/frontend/src/xtext/OccurrencesService.ts index 35913f43..c8d6fd7b 100644 --- a/subprojects/frontend/src/xtext/OccurrencesService.ts +++ b/subprojects/frontend/src/xtext/OccurrencesService.ts | |||
@@ -1,20 +1,15 @@ | |||
1 | import { Transaction } from '@codemirror/state'; | 1 | import { Transaction } from '@codemirror/state'; |
2 | import { debounce } from 'lodash-es'; | ||
2 | 3 | ||
3 | import type EditorStore from '../editor/EditorStore'; | 4 | import type EditorStore from '../editor/EditorStore'; |
4 | import { | 5 | import { |
5 | type IOccurrence, | 6 | type IOccurrence, |
6 | isCursorWithinOccurence, | 7 | isCursorWithinOccurence, |
7 | } from '../editor/findOccurrences'; | 8 | } from '../editor/findOccurrences'; |
8 | import Timer from '../utils/Timer'; | ||
9 | import getLogger from '../utils/getLogger'; | 9 | import getLogger from '../utils/getLogger'; |
10 | 10 | ||
11 | import type UpdateService from './UpdateService'; | 11 | import type UpdateService from './UpdateService'; |
12 | import type XtextWebSocketClient from './XtextWebSocketClient'; | 12 | import type { TextRegion } from './xtextServiceResults'; |
13 | import { | ||
14 | isConflictResult, | ||
15 | OccurrencesResult, | ||
16 | type TextRegion, | ||
17 | } from './xtextServiceResults'; | ||
18 | 13 | ||
19 | const FIND_OCCURRENCES_TIMEOUT_MS = 1000; | 14 | const FIND_OCCURRENCES_TIMEOUT_MS = 1000; |
20 | 15 | ||
@@ -34,38 +29,23 @@ function transformOccurrences(regions: TextRegion[]): IOccurrence[] { | |||
34 | } | 29 | } |
35 | 30 | ||
36 | export default class OccurrencesService { | 31 | export default class OccurrencesService { |
37 | private readonly store: EditorStore; | ||
38 | |||
39 | private readonly webSocketClient: XtextWebSocketClient; | ||
40 | |||
41 | private readonly updateService: UpdateService; | ||
42 | |||
43 | private hasOccurrences = false; | 32 | private hasOccurrences = false; |
44 | 33 | ||
45 | private readonly findOccurrencesTimer = new Timer(() => { | 34 | private readonly findOccurrencesLater = debounce( |
46 | this.handleFindOccurrences(); | 35 | () => this.findOccurrences(), |
47 | }, FIND_OCCURRENCES_TIMEOUT_MS); | 36 | FIND_OCCURRENCES_TIMEOUT_MS, |
48 | 37 | ); | |
49 | private readonly clearOccurrencesTimer = new Timer(() => { | ||
50 | this.clearOccurrences(); | ||
51 | }); | ||
52 | 38 | ||
53 | constructor( | 39 | constructor( |
54 | store: EditorStore, | 40 | private readonly store: EditorStore, |
55 | webSocketClient: XtextWebSocketClient, | 41 | private readonly updateService: UpdateService, |
56 | updateService: UpdateService, | 42 | ) {} |
57 | ) { | ||
58 | this.store = store; | ||
59 | this.webSocketClient = webSocketClient; | ||
60 | this.updateService = updateService; | ||
61 | } | ||
62 | 43 | ||
63 | onTransaction(transaction: Transaction): void { | 44 | onTransaction(transaction: Transaction): void { |
64 | if (transaction.docChanged) { | 45 | if (transaction.docChanged) { |
65 | // Must clear occurrences asynchronously from `onTransaction`, | 46 | // Must clear occurrences asynchronously from `onTransaction`, |
66 | // because we must not emit a conflicting transaction when handling the pending transaction. | 47 | // because we must not emit a conflicting transaction when handling the pending transaction. |
67 | this.clearOccurrencesTimer.schedule(); | 48 | this.clearAndFindOccurrencesLater(); |
68 | this.findOccurrencesTimer.reschedule(); | ||
69 | return; | 49 | return; |
70 | } | 50 | } |
71 | if (!transaction.isUserEvent('select')) { | 51 | if (!transaction.isUserEvent('select')) { |
@@ -73,11 +53,10 @@ export default class OccurrencesService { | |||
73 | } | 53 | } |
74 | if (this.needsOccurrences) { | 54 | if (this.needsOccurrences) { |
75 | if (!isCursorWithinOccurence(this.store.state)) { | 55 | if (!isCursorWithinOccurence(this.store.state)) { |
76 | this.clearOccurrencesTimer.schedule(); | 56 | this.clearAndFindOccurrencesLater(); |
77 | this.findOccurrencesTimer.reschedule(); | ||
78 | } | 57 | } |
79 | } else { | 58 | } else { |
80 | this.clearOccurrencesTimer.schedule(); | 59 | this.clearOccurrencesLater(); |
81 | } | 60 | } |
82 | } | 61 | } |
83 | 62 | ||
@@ -85,8 +64,26 @@ export default class OccurrencesService { | |||
85 | return this.store.state.selection.main.empty; | 64 | return this.store.state.selection.main.empty; |
86 | } | 65 | } |
87 | 66 | ||
88 | private handleFindOccurrences() { | 67 | private clearAndFindOccurrencesLater(): void { |
89 | this.clearOccurrencesTimer.cancel(); | 68 | this.clearOccurrencesLater(); |
69 | this.findOccurrencesLater(); | ||
70 | } | ||
71 | |||
72 | /** | ||
73 | * Clears the occurences from a new immediate task to let the current editor transaction finish. | ||
74 | */ | ||
75 | private clearOccurrencesLater() { | ||
76 | setTimeout(() => this.clearOccurrences(), 0); | ||
77 | } | ||
78 | |||
79 | private clearOccurrences() { | ||
80 | if (this.hasOccurrences) { | ||
81 | this.store.updateOccurrences([], []); | ||
82 | this.hasOccurrences = false; | ||
83 | } | ||
84 | } | ||
85 | |||
86 | private findOccurrences() { | ||
90 | this.updateOccurrences().catch((error) => { | 87 | this.updateOccurrences().catch((error) => { |
91 | log.error('Unexpected error while updating occurrences', error); | 88 | log.error('Unexpected error while updating occurrences', error); |
92 | this.clearOccurrences(); | 89 | this.clearOccurrences(); |
@@ -98,43 +95,26 @@ export default class OccurrencesService { | |||
98 | this.clearOccurrences(); | 95 | this.clearOccurrences(); |
99 | return; | 96 | return; |
100 | } | 97 | } |
101 | await this.updateService.update(); | 98 | const fetchResult = await this.updateService.fetchOccurrences(() => { |
102 | const result = await this.webSocketClient.send({ | 99 | return this.needsOccurrences |
103 | resource: this.updateService.resourceName, | 100 | ? { |
104 | serviceType: 'occurrences', | 101 | cancelled: false, |
105 | expectedStateId: this.updateService.xtextStateId, | 102 | data: this.store.state.selection.main.head, |
106 | caretOffset: this.store.state.selection.main.head, | 103 | } |
104 | : { cancelled: true }; | ||
107 | }); | 105 | }); |
108 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); | 106 | if (fetchResult.cancelled) { |
109 | if (!allChanges.empty || isConflictResult(result, 'canceled')) { | ||
110 | // Stale occurrences result, the user already made some changes. | 107 | // Stale occurrences result, the user already made some changes. |
111 | // We can safely ignore the occurrences and schedule a new find occurrences call. | 108 | // We can safely ignore the occurrences and schedule a new find occurrences call. |
112 | this.clearOccurrences(); | 109 | this.clearOccurrences(); |
113 | this.findOccurrencesTimer.schedule(); | 110 | if (this.needsOccurrences) { |
114 | return; | 111 | this.findOccurrencesLater(); |
115 | } | 112 | } |
116 | const parsedOccurrencesResult = OccurrencesResult.safeParse(result); | ||
117 | if (!parsedOccurrencesResult.success) { | ||
118 | log.error( | ||
119 | 'Unexpected occurences result', | ||
120 | result, | ||
121 | 'not an OccurrencesResult: ', | ||
122 | parsedOccurrencesResult.error, | ||
123 | ); | ||
124 | this.clearOccurrences(); | ||
125 | return; | ||
126 | } | ||
127 | const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data; | ||
128 | if (stateId !== this.updateService.xtextStateId) { | ||
129 | log.error( | ||
130 | 'Unexpected state id, expected:', | ||
131 | this.updateService.xtextStateId, | ||
132 | 'got:', | ||
133 | stateId, | ||
134 | ); | ||
135 | this.clearOccurrences(); | ||
136 | return; | 113 | return; |
137 | } | 114 | } |
115 | const { | ||
116 | data: { writeRegions, readRegions }, | ||
117 | } = fetchResult; | ||
138 | const write = transformOccurrences(writeRegions); | 118 | const write = transformOccurrences(writeRegions); |
139 | const read = transformOccurrences(readRegions); | 119 | const read = transformOccurrences(readRegions); |
140 | this.hasOccurrences = write.length > 0 || read.length > 0; | 120 | this.hasOccurrences = write.length > 0 || read.length > 0; |
@@ -147,11 +127,4 @@ export default class OccurrencesService { | |||
147 | ); | 127 | ); |
148 | this.store.updateOccurrences(write, read); | 128 | this.store.updateOccurrences(write, read); |
149 | } | 129 | } |
150 | |||
151 | private clearOccurrences() { | ||
152 | if (this.hasOccurrences) { | ||
153 | this.store.updateOccurrences([], []); | ||
154 | this.hasOccurrences = false; | ||
155 | } | ||
156 | } | ||
157 | } | 130 | } |
diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts index f8b71160..3b4ae259 100644 --- a/subprojects/frontend/src/xtext/UpdateService.ts +++ b/subprojects/frontend/src/xtext/UpdateService.ts | |||
@@ -5,11 +5,11 @@ import { | |||
5 | StateEffect, | 5 | StateEffect, |
6 | type Transaction, | 6 | type Transaction, |
7 | } from '@codemirror/state'; | 7 | } from '@codemirror/state'; |
8 | import { E_CANCELED, E_TIMEOUT, Mutex, withTimeout } from 'async-mutex'; | ||
9 | import { debounce } from 'lodash-es'; | ||
8 | import { nanoid } from 'nanoid'; | 10 | import { nanoid } from 'nanoid'; |
9 | 11 | ||
10 | import type EditorStore from '../editor/EditorStore'; | 12 | import type EditorStore from '../editor/EditorStore'; |
11 | import ConditionVariable from '../utils/ConditionVariable'; | ||
12 | import Timer from '../utils/Timer'; | ||
13 | import getLogger from '../utils/getLogger'; | 13 | import getLogger from '../utils/getLogger'; |
14 | 14 | ||
15 | import type XtextWebSocketClient from './XtextWebSocketClient'; | 15 | import type XtextWebSocketClient from './XtextWebSocketClient'; |
@@ -19,12 +19,15 @@ import { | |||
19 | DocumentStateResult, | 19 | DocumentStateResult, |
20 | FormattingResult, | 20 | FormattingResult, |
21 | isConflictResult, | 21 | isConflictResult, |
22 | OccurrencesResult, | ||
22 | } from './xtextServiceResults'; | 23 | } from './xtextServiceResults'; |
23 | 24 | ||
24 | const UPDATE_TIMEOUT_MS = 500; | 25 | const UPDATE_TIMEOUT_MS = 500; |
25 | 26 | ||
26 | const WAIT_FOR_UPDATE_TIMEOUT_MS = 1000; | 27 | const WAIT_FOR_UPDATE_TIMEOUT_MS = 1000; |
27 | 28 | ||
29 | const FORMAT_TEXT_RETRIES = 5; | ||
30 | |||
28 | const log = getLogger('xtext.UpdateService'); | 31 | const log = getLogger('xtext.UpdateService'); |
29 | 32 | ||
30 | /** | 33 | /** |
@@ -38,10 +41,20 @@ const log = getLogger('xtext.UpdateService'); | |||
38 | */ | 41 | */ |
39 | const setDirtyChanges = StateEffect.define<ChangeSet>(); | 42 | const setDirtyChanges = StateEffect.define<ChangeSet>(); |
40 | 43 | ||
41 | export interface IAbortSignal { | 44 | export interface AbortSignal { |
42 | aborted: boolean; | 45 | aborted: boolean; |
43 | } | 46 | } |
44 | 47 | ||
48 | export interface ContentAssistParams { | ||
49 | caretOffset: number; | ||
50 | |||
51 | proposalsLimit: number; | ||
52 | } | ||
53 | |||
54 | export type CancellableResult<T> = | ||
55 | | { cancelled: false; data: T } | ||
56 | | { cancelled: true }; | ||
57 | |||
45 | interface StateUpdateResult<T> { | 58 | interface StateUpdateResult<T> { |
46 | newStateId: string; | 59 | newStateId: string; |
47 | 60 | ||
@@ -57,15 +70,27 @@ interface Delta { | |||
57 | } | 70 | } |
58 | 71 | ||
59 | export default class UpdateService { | 72 | export default class UpdateService { |
60 | resourceName: string; | 73 | readonly resourceName: string; |
61 | 74 | ||
62 | xtextStateId: string | undefined; | 75 | xtextStateId: string | undefined; |
63 | 76 | ||
64 | private readonly store: EditorStore; | 77 | private readonly store: EditorStore; |
65 | 78 | ||
79 | private readonly mutex = withTimeout(new Mutex(), WAIT_FOR_UPDATE_TIMEOUT_MS); | ||
80 | |||
66 | /** | 81 | /** |
67 | * The changes being synchronized to the server if a full or delta text update is running, | 82 | * The changes being synchronized to the server if a full or delta text update is running |
68 | * `undefined` otherwise. | 83 | * withing a `withUpdateExclusive` block, `undefined` otherwise. |
84 | * | ||
85 | * Must be `undefined` before and after entering the critical section of `mutex` | ||
86 | * and may only be changes in the critical section of `mutex`. | ||
87 | * | ||
88 | * Methods named with an `Exclusive` suffix in this class assume that the mutex is held | ||
89 | * and may call `withUpdateExclusive` or `doFallbackUpdateFullTextExclusive` | ||
90 | * to mutate this field. | ||
91 | * | ||
92 | * Methods named with a `do` suffix assume that they are called in a `withUpdateExclusive` | ||
93 | * block and require this field to be non-`undefined`. | ||
69 | */ | 94 | */ |
70 | private pendingUpdate: ChangeSet | undefined; | 95 | private pendingUpdate: ChangeSet | undefined; |
71 | 96 | ||
@@ -76,15 +101,11 @@ export default class UpdateService { | |||
76 | 101 | ||
77 | private readonly webSocketClient: XtextWebSocketClient; | 102 | private readonly webSocketClient: XtextWebSocketClient; |
78 | 103 | ||
79 | private readonly updatedCondition = new ConditionVariable( | 104 | private readonly idleUpdateLater = debounce( |
80 | () => this.pendingUpdate === undefined && this.xtextStateId !== undefined, | 105 | () => this.idleUpdate(), |
81 | WAIT_FOR_UPDATE_TIMEOUT_MS, | 106 | UPDATE_TIMEOUT_MS, |
82 | ); | 107 | ); |
83 | 108 | ||
84 | private readonly idleUpdateTimer = new Timer(() => { | ||
85 | this.handleIdleUpdate(); | ||
86 | }, UPDATE_TIMEOUT_MS); | ||
87 | |||
88 | constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) { | 109 | constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) { |
89 | this.resourceName = `${nanoid(7)}.problem`; | 110 | this.resourceName = `${nanoid(7)}.problem`; |
90 | this.store = store; | 111 | this.store = store; |
@@ -95,6 +116,13 @@ export default class UpdateService { | |||
95 | onReconnect(): void { | 116 | onReconnect(): void { |
96 | this.xtextStateId = undefined; | 117 | this.xtextStateId = undefined; |
97 | this.updateFullText().catch((error) => { | 118 | this.updateFullText().catch((error) => { |
119 | // Let E_TIMEOUT errors propagate, since if the first update times out, | ||
120 | // we can't use the connection. | ||
121 | if (error === E_CANCELED) { | ||
122 | // Content assist will perform a full-text update anyways. | ||
123 | log.debug('Full text update cancelled'); | ||
124 | return; | ||
125 | } | ||
98 | log.error('Unexpected error during initial update', error); | 126 | log.error('Unexpected error during initial update', error); |
99 | }); | 127 | }); |
100 | } | 128 | } |
@@ -106,6 +134,8 @@ export default class UpdateService { | |||
106 | if (setDirtyChangesEffect) { | 134 | if (setDirtyChangesEffect) { |
107 | const { value } = setDirtyChangesEffect; | 135 | const { value } = setDirtyChangesEffect; |
108 | if (this.pendingUpdate !== undefined) { | 136 | if (this.pendingUpdate !== undefined) { |
137 | // Do not clear `pendingUpdate`, because that would indicate an update failure | ||
138 | // to `withUpdateExclusive`. | ||
109 | this.pendingUpdate = ChangeSet.empty(value.length); | 139 | this.pendingUpdate = ChangeSet.empty(value.length); |
110 | } | 140 | } |
111 | this.dirtyChanges = value; | 141 | this.dirtyChanges = value; |
@@ -113,7 +143,7 @@ export default class UpdateService { | |||
113 | } | 143 | } |
114 | if (transaction.docChanged) { | 144 | if (transaction.docChanged) { |
115 | this.dirtyChanges = this.dirtyChanges.compose(transaction.changes); | 145 | this.dirtyChanges = this.dirtyChanges.compose(transaction.changes); |
116 | this.idleUpdateTimer.reschedule(); | 146 | this.idleUpdateLater(); |
117 | } | 147 | } |
118 | } | 148 | } |
119 | 149 | ||
@@ -132,34 +162,42 @@ export default class UpdateService { | |||
132 | ); | 162 | ); |
133 | } | 163 | } |
134 | 164 | ||
135 | private handleIdleUpdate(): void { | 165 | private idleUpdate(): void { |
136 | if (!this.webSocketClient.isOpen || this.dirtyChanges.empty) { | 166 | if (!this.webSocketClient.isOpen || this.dirtyChanges.empty) { |
137 | return; | 167 | return; |
138 | } | 168 | } |
139 | if (this.pendingUpdate === undefined) { | 169 | if (!this.mutex.isLocked()) { |
140 | this.update().catch((error) => { | 170 | this.update().catch((error) => { |
171 | if (error === E_CANCELED || error === E_TIMEOUT) { | ||
172 | log.debug('Idle update cancelled'); | ||
173 | return; | ||
174 | } | ||
141 | log.error('Unexpected error during scheduled update', error); | 175 | log.error('Unexpected error during scheduled update', error); |
142 | }); | 176 | }); |
143 | } | 177 | } |
144 | this.idleUpdateTimer.reschedule(); | 178 | this.idleUpdateLater(); |
145 | } | 179 | } |
146 | 180 | ||
147 | private newEmptyChangeSet(): ChangeSet { | 181 | private newEmptyChangeSet(): ChangeSet { |
148 | return ChangeSet.of([], this.store.state.doc.length); | 182 | return ChangeSet.of([], this.store.state.doc.length); |
149 | } | 183 | } |
150 | 184 | ||
151 | async updateFullText(): Promise<void> { | 185 | private updateFullText(): Promise<void> { |
152 | await this.withUpdate(() => this.doUpdateFullText()); | 186 | return this.runExclusive(() => this.updateFullTextExclusive()); |
153 | } | 187 | } |
154 | 188 | ||
155 | private async doUpdateFullText(): Promise<StateUpdateResult<void>> { | 189 | private async updateFullTextExclusive(): Promise<void> { |
190 | await this.withVoidUpdateExclusive(() => this.doUpdateFullTextExclusive()); | ||
191 | } | ||
192 | |||
193 | private async doUpdateFullTextExclusive(): Promise<string> { | ||
156 | const result = await this.webSocketClient.send({ | 194 | const result = await this.webSocketClient.send({ |
157 | resource: this.resourceName, | 195 | resource: this.resourceName, |
158 | serviceType: 'update', | 196 | serviceType: 'update', |
159 | fullText: this.store.state.doc.sliceString(0), | 197 | fullText: this.store.state.doc.sliceString(0), |
160 | }); | 198 | }); |
161 | const { stateId } = DocumentStateResult.parse(result); | 199 | const { stateId } = DocumentStateResult.parse(result); |
162 | return { newStateId: stateId, data: undefined }; | 200 | return stateId; |
163 | } | 201 | } |
164 | 202 | ||
165 | /** | 203 | /** |
@@ -171,14 +209,26 @@ export default class UpdateService { | |||
171 | * | 209 | * |
172 | * @returns a promise resolving when the update is completed | 210 | * @returns a promise resolving when the update is completed |
173 | */ | 211 | */ |
174 | async update(): Promise<void> { | 212 | private async update(): Promise<void> { |
175 | await this.prepareForDeltaUpdate(); | 213 | // We may check here for the delta to avoid locking, |
214 | // but we'll need to recompute the delta in the critical section, | ||
215 | // because it may have changed by the time we can acquire the lock. | ||
216 | if (this.dirtyChanges.empty) { | ||
217 | return; | ||
218 | } | ||
219 | await this.runExclusive(() => this.updateExclusive()); | ||
220 | } | ||
221 | |||
222 | private async updateExclusive(): Promise<void> { | ||
223 | if (this.xtextStateId === undefined) { | ||
224 | await this.updateFullTextExclusive(); | ||
225 | } | ||
176 | const delta = this.computeDelta(); | 226 | const delta = this.computeDelta(); |
177 | if (delta === undefined) { | 227 | if (delta === undefined) { |
178 | return; | 228 | return; |
179 | } | 229 | } |
180 | log.trace('Editor delta', delta); | 230 | log.trace('Editor delta', delta); |
181 | await this.withUpdate(async () => { | 231 | await this.withVoidUpdateExclusive(async () => { |
182 | const result = await this.webSocketClient.send({ | 232 | const result = await this.webSocketClient.send({ |
183 | resource: this.resourceName, | 233 | resource: this.resourceName, |
184 | serviceType: 'update', | 234 | serviceType: 'update', |
@@ -187,34 +237,98 @@ export default class UpdateService { | |||
187 | }); | 237 | }); |
188 | const parsedDocumentStateResult = DocumentStateResult.safeParse(result); | 238 | const parsedDocumentStateResult = DocumentStateResult.safeParse(result); |
189 | if (parsedDocumentStateResult.success) { | 239 | if (parsedDocumentStateResult.success) { |
190 | return { | 240 | return parsedDocumentStateResult.data.stateId; |
191 | newStateId: parsedDocumentStateResult.data.stateId, | ||
192 | data: undefined, | ||
193 | }; | ||
194 | } | 241 | } |
195 | if (isConflictResult(result, 'invalidStateId')) { | 242 | if (isConflictResult(result, 'invalidStateId')) { |
196 | return this.doFallbackToUpdateFullText(); | 243 | return this.doFallbackUpdateFullTextExclusive(); |
197 | } | 244 | } |
198 | throw parsedDocumentStateResult.error; | 245 | throw parsedDocumentStateResult.error; |
199 | }); | 246 | }); |
200 | } | 247 | } |
201 | 248 | ||
202 | private doFallbackToUpdateFullText(): Promise<StateUpdateResult<void>> { | 249 | async fetchOccurrences( |
203 | if (this.pendingUpdate === undefined) { | 250 | getCaretOffset: () => CancellableResult<number>, |
204 | throw new Error('Only a pending update can be extended'); | 251 | ): Promise<CancellableResult<OccurrencesResult>> { |
252 | try { | ||
253 | await this.update(); | ||
254 | } catch (error) { | ||
255 | if (error === E_CANCELED || error === E_TIMEOUT) { | ||
256 | return { cancelled: true }; | ||
257 | } | ||
258 | throw error; | ||
205 | } | 259 | } |
206 | log.warn('Delta update failed, performing full text update'); | 260 | if (!this.dirtyChanges.empty || this.mutex.isLocked()) { |
207 | this.xtextStateId = undefined; | 261 | // Just give up if another update is in progress. |
208 | this.pendingUpdate = this.pendingUpdate.compose(this.dirtyChanges); | 262 | return { cancelled: true }; |
209 | this.dirtyChanges = this.newEmptyChangeSet(); | 263 | } |
210 | return this.doUpdateFullText(); | 264 | const caretOffsetResult = getCaretOffset(); |
265 | if (caretOffsetResult.cancelled) { | ||
266 | return { cancelled: true }; | ||
267 | } | ||
268 | const expectedStateId = this.xtextStateId; | ||
269 | const data = await this.webSocketClient.send({ | ||
270 | resource: this.resourceName, | ||
271 | serviceType: 'occurrences', | ||
272 | caretOffset: caretOffsetResult.data, | ||
273 | expectedStateId, | ||
274 | }); | ||
275 | if ( | ||
276 | // The query must have reached the server without being conflicted with an update | ||
277 | // or cancelled server-side. | ||
278 | isConflictResult(data) || | ||
279 | // And no state update should have occurred since then. | ||
280 | this.xtextStateId !== expectedStateId || | ||
281 | // And there should be no change to the editor text since then. | ||
282 | !this.dirtyChanges.empty || | ||
283 | // And there should be no state update in progress. | ||
284 | this.mutex.isLocked() | ||
285 | ) { | ||
286 | return { cancelled: true }; | ||
287 | } | ||
288 | const parsedOccurrencesResult = OccurrencesResult.safeParse(data); | ||
289 | if (!parsedOccurrencesResult.success) { | ||
290 | log.error( | ||
291 | 'Unexpected occurences result', | ||
292 | data, | ||
293 | 'not an OccurrencesResult:', | ||
294 | parsedOccurrencesResult.error, | ||
295 | ); | ||
296 | throw parsedOccurrencesResult.error; | ||
297 | } | ||
298 | if (parsedOccurrencesResult.data.stateId !== expectedStateId) { | ||
299 | return { cancelled: true }; | ||
300 | } | ||
301 | return { cancelled: false, data: parsedOccurrencesResult.data }; | ||
211 | } | 302 | } |
212 | 303 | ||
213 | async fetchContentAssist( | 304 | async fetchContentAssist( |
214 | params: Record<string, unknown>, | 305 | params: ContentAssistParams, |
215 | signal: IAbortSignal, | 306 | signal: AbortSignal, |
307 | ): Promise<ContentAssistEntry[]> { | ||
308 | if (!this.mutex.isLocked && this.xtextStateId !== undefined) { | ||
309 | return this.fetchContentAssistFetchOnly(params, this.xtextStateId); | ||
310 | } | ||
311 | // Content assist updates should have priority over other updates. | ||
312 | this.mutex.cancel(); | ||
313 | try { | ||
314 | return await this.runExclusive(() => | ||
315 | this.fetchContentAssistExclusive(params, signal), | ||
316 | ); | ||
317 | } catch (error) { | ||
318 | if ((error === E_CANCELED || error === E_TIMEOUT) && signal.aborted) { | ||
319 | return []; | ||
320 | } | ||
321 | throw error; | ||
322 | } | ||
323 | } | ||
324 | |||
325 | private async fetchContentAssistExclusive( | ||
326 | params: ContentAssistParams, | ||
327 | signal: AbortSignal, | ||
216 | ): Promise<ContentAssistEntry[]> { | 328 | ): Promise<ContentAssistEntry[]> { |
217 | await this.prepareForDeltaUpdate(); | 329 | if (this.xtextStateId === undefined) { |
330 | await this.updateFullTextExclusive(); | ||
331 | } | ||
218 | if (signal.aborted) { | 332 | if (signal.aborted) { |
219 | return []; | 333 | return []; |
220 | } | 334 | } |
@@ -222,8 +336,8 @@ export default class UpdateService { | |||
222 | if (delta !== undefined) { | 336 | if (delta !== undefined) { |
223 | log.trace('Editor delta', delta); | 337 | log.trace('Editor delta', delta); |
224 | // Try to fetch while also performing a delta update. | 338 | // Try to fetch while also performing a delta update. |
225 | const fetchUpdateEntries = await this.withUpdate(() => | 339 | const fetchUpdateEntries = await this.withUpdateExclusive(() => |
226 | this.doFetchContentAssistWithDelta(params, delta), | 340 | this.doFetchContentAssistWithDeltaExclusive(params, delta), |
227 | ); | 341 | ); |
228 | if (fetchUpdateEntries !== undefined) { | 342 | if (fetchUpdateEntries !== undefined) { |
229 | return fetchUpdateEntries; | 343 | return fetchUpdateEntries; |
@@ -235,15 +349,17 @@ export default class UpdateService { | |||
235 | if (this.xtextStateId === undefined) { | 349 | if (this.xtextStateId === undefined) { |
236 | throw new Error('failed to obtain Xtext state id'); | 350 | throw new Error('failed to obtain Xtext state id'); |
237 | } | 351 | } |
238 | return this.doFetchContentAssistFetchOnly(params, this.xtextStateId); | 352 | return this.fetchContentAssistFetchOnly(params, this.xtextStateId); |
239 | } | 353 | } |
240 | 354 | ||
241 | private async doFetchContentAssistWithDelta( | 355 | private async doFetchContentAssistWithDeltaExclusive( |
242 | params: Record<string, unknown>, | 356 | params: ContentAssistParams, |
243 | delta: Delta, | 357 | delta: Delta, |
244 | ): Promise<StateUpdateResult<ContentAssistEntry[] | undefined>> { | 358 | ): Promise<StateUpdateResult<ContentAssistEntry[] | undefined>> { |
245 | const fetchUpdateResult = await this.webSocketClient.send({ | 359 | const fetchUpdateResult = await this.webSocketClient.send({ |
246 | ...params, | 360 | ...params, |
361 | resource: this.resourceName, | ||
362 | serviceType: 'assist', | ||
247 | requiredStateId: this.xtextStateId, | 363 | requiredStateId: this.xtextStateId, |
248 | ...delta, | 364 | ...delta, |
249 | }); | 365 | }); |
@@ -256,7 +372,7 @@ export default class UpdateService { | |||
256 | } | 372 | } |
257 | if (isConflictResult(fetchUpdateResult, 'invalidStateId')) { | 373 | if (isConflictResult(fetchUpdateResult, 'invalidStateId')) { |
258 | log.warn('Server state invalid during content assist'); | 374 | log.warn('Server state invalid during content assist'); |
259 | const { newStateId } = await this.doFallbackToUpdateFullText(); | 375 | const newStateId = await this.doFallbackUpdateFullTextExclusive(); |
260 | // We must finish this state update transaction to prepare for any push events | 376 | // We must finish this state update transaction to prepare for any push events |
261 | // before querying for content assist, so we just return `undefined` and will query | 377 | // before querying for content assist, so we just return `undefined` and will query |
262 | // the content assist service later. | 378 | // the content assist service later. |
@@ -265,14 +381,16 @@ export default class UpdateService { | |||
265 | throw parsedContentAssistResult.error; | 381 | throw parsedContentAssistResult.error; |
266 | } | 382 | } |
267 | 383 | ||
268 | private async doFetchContentAssistFetchOnly( | 384 | private async fetchContentAssistFetchOnly( |
269 | params: Record<string, unknown>, | 385 | params: ContentAssistParams, |
270 | requiredStateId: string, | 386 | requiredStateId: string, |
271 | ): Promise<ContentAssistEntry[]> { | 387 | ): Promise<ContentAssistEntry[]> { |
272 | // Fallback to fetching without a delta update. | 388 | // Fallback to fetching without a delta update. |
273 | const fetchOnlyResult = await this.webSocketClient.send({ | 389 | const fetchOnlyResult = await this.webSocketClient.send({ |
274 | ...params, | 390 | ...params, |
275 | requiredStateId: this.xtextStateId, | 391 | resource: this.resourceName, |
392 | serviceType: 'assist', | ||
393 | requiredStateId, | ||
276 | }); | 394 | }); |
277 | const { stateId, entries: fetchOnlyEntries } = | 395 | const { stateId, entries: fetchOnlyEntries } = |
278 | ContentAssistResult.parse(fetchOnlyResult); | 396 | ContentAssistResult.parse(fetchOnlyResult); |
@@ -285,14 +403,32 @@ export default class UpdateService { | |||
285 | } | 403 | } |
286 | 404 | ||
287 | async formatText(): Promise<void> { | 405 | async formatText(): Promise<void> { |
288 | await this.update(); | 406 | let retries = 0; |
407 | while (retries < FORMAT_TEXT_RETRIES) { | ||
408 | try { | ||
409 | // eslint-disable-next-line no-await-in-loop -- Use a loop for sequential retries. | ||
410 | await this.runExclusive(() => this.formatTextExclusive()); | ||
411 | return; | ||
412 | } catch (error) { | ||
413 | // Let timeout errors propagate to give up formatting on a flaky connection. | ||
414 | if (error === E_CANCELED && retries < FORMAT_TEXT_RETRIES) { | ||
415 | retries += 1; | ||
416 | } else { | ||
417 | throw error; | ||
418 | } | ||
419 | } | ||
420 | } | ||
421 | } | ||
422 | |||
423 | private async formatTextExclusive(): Promise<void> { | ||
424 | await this.updateExclusive(); | ||
289 | let { from, to } = this.store.state.selection.main; | 425 | let { from, to } = this.store.state.selection.main; |
290 | if (to <= from) { | 426 | if (to <= from) { |
291 | from = 0; | 427 | from = 0; |
292 | to = this.store.state.doc.length; | 428 | to = this.store.state.doc.length; |
293 | } | 429 | } |
294 | log.debug('Formatting from', from, 'to', to); | 430 | log.debug('Formatting from', from, 'to', to); |
295 | await this.withUpdate<void>(async () => { | 431 | await this.withVoidUpdateExclusive(async () => { |
296 | const result = await this.webSocketClient.send({ | 432 | const result = await this.webSocketClient.send({ |
297 | resource: this.resourceName, | 433 | resource: this.resourceName, |
298 | serviceType: 'format', | 434 | serviceType: 'format', |
@@ -305,7 +441,7 @@ export default class UpdateService { | |||
305 | to, | 441 | to, |
306 | insert: formattedText, | 442 | insert: formattedText, |
307 | }); | 443 | }); |
308 | return { newStateId: stateId, data: undefined }; | 444 | return stateId; |
309 | }); | 445 | }); |
310 | } | 446 | } |
311 | 447 | ||
@@ -345,6 +481,28 @@ export default class UpdateService { | |||
345 | }); | 481 | }); |
346 | } | 482 | } |
347 | 483 | ||
484 | private runExclusive<T>(callback: () => Promise<T>): Promise<T> { | ||
485 | return this.mutex.runExclusive(async () => { | ||
486 | if (this.pendingUpdate !== undefined) { | ||
487 | throw new Error('Update is pending before entering critical section'); | ||
488 | } | ||
489 | const result = await callback(); | ||
490 | if (this.pendingUpdate !== undefined) { | ||
491 | throw new Error('Update is pending after entering critical section'); | ||
492 | } | ||
493 | return result; | ||
494 | }); | ||
495 | } | ||
496 | |||
497 | private withVoidUpdateExclusive( | ||
498 | callback: () => Promise<string>, | ||
499 | ): Promise<void> { | ||
500 | return this.withUpdateExclusive<void>(async () => { | ||
501 | const newStateId = await callback(); | ||
502 | return { newStateId, data: undefined }; | ||
503 | }); | ||
504 | } | ||
505 | |||
348 | /** | 506 | /** |
349 | * Executes an asynchronous callback that updates the state on the server. | 507 | * Executes an asynchronous callback that updates the state on the server. |
350 | * | 508 | * |
@@ -366,20 +524,18 @@ export default class UpdateService { | |||
366 | * @param callback the asynchronous callback that updates the server state | 524 | * @param callback the asynchronous callback that updates the server state |
367 | * @returns a promise resolving to the second value returned by `callback` | 525 | * @returns a promise resolving to the second value returned by `callback` |
368 | */ | 526 | */ |
369 | private async withUpdate<T>( | 527 | private async withUpdateExclusive<T>( |
370 | callback: () => Promise<StateUpdateResult<T>>, | 528 | callback: () => Promise<StateUpdateResult<T>>, |
371 | ): Promise<T> { | 529 | ): Promise<T> { |
372 | if (this.pendingUpdate !== undefined) { | 530 | if (this.pendingUpdate !== undefined) { |
373 | throw new Error('Another update is pending, will not perform update'); | 531 | throw new Error('Delta updates are not reentrant'); |
374 | } | 532 | } |
375 | this.pendingUpdate = this.dirtyChanges; | 533 | this.pendingUpdate = this.dirtyChanges; |
376 | this.dirtyChanges = this.newEmptyChangeSet(); | 534 | this.dirtyChanges = this.newEmptyChangeSet(); |
535 | let data: T; | ||
377 | try { | 536 | try { |
378 | const { newStateId, data } = await callback(); | 537 | ({ newStateId: this.xtextStateId, data } = await callback()); |
379 | this.xtextStateId = newStateId; | ||
380 | this.pendingUpdate = undefined; | 538 | this.pendingUpdate = undefined; |
381 | this.updatedCondition.notifyAll(); | ||
382 | return data; | ||
383 | } catch (e) { | 539 | } catch (e) { |
384 | log.error('Error while update', e); | 540 | log.error('Error while update', e); |
385 | if (this.pendingUpdate === undefined) { | 541 | if (this.pendingUpdate === undefined) { |
@@ -389,25 +545,19 @@ export default class UpdateService { | |||
389 | } | 545 | } |
390 | this.pendingUpdate = undefined; | 546 | this.pendingUpdate = undefined; |
391 | this.webSocketClient.forceReconnectOnError(); | 547 | this.webSocketClient.forceReconnectOnError(); |
392 | this.updatedCondition.rejectAll(e); | ||
393 | throw e; | 548 | throw e; |
394 | } | 549 | } |
550 | return data; | ||
395 | } | 551 | } |
396 | 552 | ||
397 | /** | 553 | private doFallbackUpdateFullTextExclusive(): Promise<string> { |
398 | * Ensures that there is some state available on the server (`xtextStateId`) | 554 | if (this.pendingUpdate === undefined) { |
399 | * and that there is no pending update. | 555 | throw new Error('Only a pending update can be extended'); |
400 | * | ||
401 | * After this function resolves, a delta text update is possible. | ||
402 | * | ||
403 | * @returns a promise resolving when there is a valid state id but no pending update | ||
404 | */ | ||
405 | private async prepareForDeltaUpdate(): Promise<void> { | ||
406 | // If no update is pending, but the full text hasn't been uploaded to the server yet, | ||
407 | // we must start a full text upload. | ||
408 | if (this.pendingUpdate === undefined && this.xtextStateId === undefined) { | ||
409 | await this.updateFullText(); | ||
410 | } | 556 | } |
411 | await this.updatedCondition.waitFor(); | 557 | log.warn('Delta update failed, performing full text update'); |
558 | this.xtextStateId = undefined; | ||
559 | this.pendingUpdate = this.pendingUpdate.compose(this.dirtyChanges); | ||
560 | this.dirtyChanges = this.newEmptyChangeSet(); | ||
561 | return this.doUpdateFullTextExclusive(); | ||
412 | } | 562 | } |
413 | } | 563 | } |
diff --git a/subprojects/frontend/src/xtext/ValidationService.ts b/subprojects/frontend/src/xtext/ValidationService.ts index a0b27251..e78318f7 100644 --- a/subprojects/frontend/src/xtext/ValidationService.ts +++ b/subprojects/frontend/src/xtext/ValidationService.ts | |||
@@ -6,14 +6,10 @@ import type UpdateService from './UpdateService'; | |||
6 | import { ValidationResult } from './xtextServiceResults'; | 6 | import { ValidationResult } from './xtextServiceResults'; |
7 | 7 | ||
8 | export default class ValidationService { | 8 | export default class ValidationService { |
9 | private readonly store: EditorStore; | 9 | constructor( |
10 | 10 | private readonly store: EditorStore, | |
11 | private readonly updateService: UpdateService; | 11 | private readonly updateService: UpdateService, |
12 | 12 | ) {} | |
13 | constructor(store: EditorStore, updateService: UpdateService) { | ||
14 | this.store = store; | ||
15 | this.updateService = updateService; | ||
16 | } | ||
17 | 13 | ||
18 | onPush(push: unknown): void { | 14 | onPush(push: unknown): void { |
19 | const { issues } = ValidationResult.parse(push); | 15 | const { issues } = ValidationResult.parse(push); |
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts index 7297c674..6351c9fd 100644 --- a/subprojects/frontend/src/xtext/XtextClient.ts +++ b/subprojects/frontend/src/xtext/XtextClient.ts | |||
@@ -43,11 +43,7 @@ export default class XtextClient { | |||
43 | this.updateService, | 43 | this.updateService, |
44 | ); | 44 | ); |
45 | this.validationService = new ValidationService(store, this.updateService); | 45 | this.validationService = new ValidationService(store, this.updateService); |
46 | this.occurrencesService = new OccurrencesService( | 46 | this.occurrencesService = new OccurrencesService(store, this.updateService); |
47 | store, | ||
48 | this.webSocketClient, | ||
49 | this.updateService, | ||
50 | ); | ||
51 | } | 47 | } |
52 | 48 | ||
53 | onTransaction(transaction: Transaction): void { | 49 | onTransaction(transaction: Transaction): void { |
diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts index 4cfb9c33..e93c6714 100644 --- a/subprojects/frontend/src/xtext/xtextServiceResults.ts +++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts | |||
@@ -26,12 +26,13 @@ export type ServiceConflictResult = z.infer<typeof ServiceConflictResult>; | |||
26 | 26 | ||
27 | export function isConflictResult( | 27 | export function isConflictResult( |
28 | result: unknown, | 28 | result: unknown, |
29 | conflictType: Conflict, | 29 | conflictType?: Conflict | undefined, |
30 | ): boolean { | 30 | ): boolean { |
31 | const parsedConflictResult = ServiceConflictResult.safeParse(result); | 31 | const parsedConflictResult = ServiceConflictResult.safeParse(result); |
32 | return ( | 32 | return ( |
33 | parsedConflictResult.success && | 33 | parsedConflictResult.success && |
34 | parsedConflictResult.data.conflict === conflictType | 34 | (conflictType === undefined || |
35 | parsedConflictResult.data.conflict === conflictType) | ||
35 | ); | 36 | ); |
36 | } | 37 | } |
37 | 38 | ||
@@ -1440,16 +1440,16 @@ __metadata: | |||
1440 | languageName: node | 1440 | languageName: node |
1441 | linkType: hard | 1441 | linkType: hard |
1442 | 1442 | ||
1443 | "@emotion/cache@npm:^11.10.0, @emotion/cache@npm:^11.9.3": | 1443 | "@emotion/cache@npm:^11.10.0, @emotion/cache@npm:^11.10.1": |
1444 | version: 11.10.0 | 1444 | version: 11.10.3 |
1445 | resolution: "@emotion/cache@npm:11.10.0" | 1445 | resolution: "@emotion/cache@npm:11.10.3" |
1446 | dependencies: | 1446 | dependencies: |
1447 | "@emotion/memoize": ^0.8.0 | 1447 | "@emotion/memoize": ^0.8.0 |
1448 | "@emotion/sheet": ^1.2.0 | 1448 | "@emotion/sheet": ^1.2.0 |
1449 | "@emotion/utils": ^1.2.0 | 1449 | "@emotion/utils": ^1.2.0 |
1450 | "@emotion/weak-memoize": ^0.3.0 | 1450 | "@emotion/weak-memoize": ^0.3.0 |
1451 | stylis: 4.0.13 | 1451 | stylis: 4.0.13 |
1452 | checksum: 60786e3108c35d3f79bd434119f1b6e34120fccc92f4a129bb67596944b703c3396efa51ed373f0ee3d71de780d78f4d767c264f7d76b073d06d435a2d3c1edf | 1452 | checksum: d31291eff1b270d8db6f471b2b9b3bc5d786c296838631f101837747ff5afa8e8890655279457c68ce2cee23256ab02a25c177f5487b5061da82c7354c1bdce5 |
1453 | languageName: node | 1453 | languageName: node |
1454 | linkType: hard | 1454 | linkType: hard |
1455 | 1455 | ||
@@ -1460,7 +1460,7 @@ __metadata: | |||
1460 | languageName: node | 1460 | languageName: node |
1461 | linkType: hard | 1461 | linkType: hard |
1462 | 1462 | ||
1463 | "@emotion/is-prop-valid@npm:^1.1.3, @emotion/is-prop-valid@npm:^1.2.0": | 1463 | "@emotion/is-prop-valid@npm:^1.2.0": |
1464 | version: 1.2.0 | 1464 | version: 1.2.0 |
1465 | resolution: "@emotion/is-prop-valid@npm:1.2.0" | 1465 | resolution: "@emotion/is-prop-valid@npm:1.2.0" |
1466 | dependencies: | 1466 | dependencies: |
@@ -1586,13 +1586,6 @@ __metadata: | |||
1586 | languageName: node | 1586 | languageName: node |
1587 | linkType: hard | 1587 | linkType: hard |
1588 | 1588 | ||
1589 | "@fontsource/inter@npm:^4.5.12": | ||
1590 | version: 4.5.12 | ||
1591 | resolution: "@fontsource/inter@npm:4.5.12" | ||
1592 | checksum: 7637978d4355f52cf348bbe40a2e1bfa535c290057bef27c022343234b00c07b138c86a2b52260825c1ecc5cc9f59c68fd6ff08311111c70c04f82ace592920b | ||
1593 | languageName: node | ||
1594 | linkType: hard | ||
1595 | |||
1596 | "@fontsource/jetbrains-mono@npm:^4.5.10": | 1589 | "@fontsource/jetbrains-mono@npm:^4.5.10": |
1597 | version: 4.5.10 | 1590 | version: 4.5.10 |
1598 | resolution: "@fontsource/jetbrains-mono@npm:4.5.10" | 1591 | resolution: "@fontsource/jetbrains-mono@npm:4.5.10" |
@@ -1745,12 +1738,12 @@ __metadata: | |||
1745 | languageName: node | 1738 | languageName: node |
1746 | linkType: hard | 1739 | linkType: hard |
1747 | 1740 | ||
1748 | "@mui/base@npm:5.0.0-alpha.93": | 1741 | "@mui/base@npm:5.0.0-alpha.94": |
1749 | version: 5.0.0-alpha.93 | 1742 | version: 5.0.0-alpha.94 |
1750 | resolution: "@mui/base@npm:5.0.0-alpha.93" | 1743 | resolution: "@mui/base@npm:5.0.0-alpha.94" |
1751 | dependencies: | 1744 | dependencies: |
1752 | "@babel/runtime": ^7.17.2 | 1745 | "@babel/runtime": ^7.17.2 |
1753 | "@emotion/is-prop-valid": ^1.1.3 | 1746 | "@emotion/is-prop-valid": ^1.2.0 |
1754 | "@mui/types": ^7.1.5 | 1747 | "@mui/types": ^7.1.5 |
1755 | "@mui/utils": ^5.9.3 | 1748 | "@mui/utils": ^5.9.3 |
1756 | "@popperjs/core": ^2.11.6 | 1749 | "@popperjs/core": ^2.11.6 |
@@ -1764,20 +1757,20 @@ __metadata: | |||
1764 | peerDependenciesMeta: | 1757 | peerDependenciesMeta: |
1765 | "@types/react": | 1758 | "@types/react": |
1766 | optional: true | 1759 | optional: true |
1767 | checksum: 8e04ac3d7e453d8acea73884fea899db4561218ba48644b04a11dfb905dbf630a84847a28ca62bc03d678c8db749e76e150d9cf99d1fbb5ff892e6b734527c29 | 1760 | checksum: 877c7c1b53a555265332e1e677945de0415db187a7c5a7f088eaacb4f85aa20086a668915ad1885e77fa7ef4df5d35250e55f4aef2b4c5aa82f014406bbdaef3 |
1768 | languageName: node | 1761 | languageName: node |
1769 | linkType: hard | 1762 | linkType: hard |
1770 | 1763 | ||
1771 | "@mui/core-downloads-tracker@npm:^5.10.1": | 1764 | "@mui/core-downloads-tracker@npm:^5.10.2": |
1772 | version: 5.10.1 | 1765 | version: 5.10.2 |
1773 | resolution: "@mui/core-downloads-tracker@npm:5.10.1" | 1766 | resolution: "@mui/core-downloads-tracker@npm:5.10.2" |
1774 | checksum: 275fbabd9beb6d4cbbe5d6cf2a7621fb827bb7ad8be2fdd218ac3b1cc7c2ee9f69cbb71022b8a6f9ecaadbc21225015b33232fc3827dabe8db5c12fbf642cfc9 | 1767 | checksum: 237d25f2cc8f63e4ee5f1cbe7944cb9818784535ed861ab3b8564ec4c7cbfdeb3c044596a2447c49feff987f268da30538fcb2ad194e3f7a0095d2e6edcdfdbd |
1775 | languageName: node | 1768 | languageName: node |
1776 | linkType: hard | 1769 | linkType: hard |
1777 | 1770 | ||
1778 | "@mui/icons-material@npm:5.8.4": | 1771 | "@mui/icons-material@npm:5.10.2": |
1779 | version: 5.8.4 | 1772 | version: 5.10.2 |
1780 | resolution: "@mui/icons-material@npm:5.8.4" | 1773 | resolution: "@mui/icons-material@npm:5.10.2" |
1781 | dependencies: | 1774 | dependencies: |
1782 | "@babel/runtime": ^7.17.2 | 1775 | "@babel/runtime": ^7.17.2 |
1783 | peerDependencies: | 1776 | peerDependencies: |
@@ -1787,18 +1780,18 @@ __metadata: | |||
1787 | peerDependenciesMeta: | 1780 | peerDependenciesMeta: |
1788 | "@types/react": | 1781 | "@types/react": |
1789 | optional: true | 1782 | optional: true |
1790 | checksum: 1df0ffb08670628968b803c7048fc645df5e2870860759626d80a355fdef62498e60e4ebed2d47ca8b3600e6ecb6db1829bec5acf3a766ff41e518007c7e2f5b | 1783 | checksum: 9e694babaa69c52139b7cdd12f449a3464707ae12c3cdfd8af43bcbb1f9040dce08c6cde95ea7812e7b946f49fddbf4df05bc6cf44a30eaeb1c8701786fd309d |
1791 | languageName: node | 1784 | languageName: node |
1792 | linkType: hard | 1785 | linkType: hard |
1793 | 1786 | ||
1794 | "@mui/material@npm:5.10.1": | 1787 | "@mui/material@npm:5.10.2": |
1795 | version: 5.10.1 | 1788 | version: 5.10.2 |
1796 | resolution: "@mui/material@npm:5.10.1" | 1789 | resolution: "@mui/material@npm:5.10.2" |
1797 | dependencies: | 1790 | dependencies: |
1798 | "@babel/runtime": ^7.17.2 | 1791 | "@babel/runtime": ^7.17.2 |
1799 | "@mui/base": 5.0.0-alpha.93 | 1792 | "@mui/base": 5.0.0-alpha.94 |
1800 | "@mui/core-downloads-tracker": ^5.10.1 | 1793 | "@mui/core-downloads-tracker": ^5.10.2 |
1801 | "@mui/system": ^5.10.1 | 1794 | "@mui/system": ^5.10.2 |
1802 | "@mui/types": ^7.1.5 | 1795 | "@mui/types": ^7.1.5 |
1803 | "@mui/utils": ^5.9.3 | 1796 | "@mui/utils": ^5.9.3 |
1804 | "@types/react-transition-group": ^4.4.5 | 1797 | "@types/react-transition-group": ^4.4.5 |
@@ -1820,7 +1813,7 @@ __metadata: | |||
1820 | optional: true | 1813 | optional: true |
1821 | "@types/react": | 1814 | "@types/react": |
1822 | optional: true | 1815 | optional: true |
1823 | checksum: 47c8157757df28863f4788f551ddb55e8268a246b2691fe5f0351d83040d874314a3b483d33359824f918abb861e3c6dca91562a5999ebda456c743890e6978a | 1816 | checksum: 2d44933c28e25b112a18cdcee4ea4c71d6eeba6b91ee33e3b66ec636c05daf3a9b03715d0a86c34a5670a3e25e39d13b10fc57aac524d52631134db0daca88de |
1824 | languageName: node | 1817 | languageName: node |
1825 | linkType: hard | 1818 | linkType: hard |
1826 | 1819 | ||
@@ -1841,12 +1834,12 @@ __metadata: | |||
1841 | languageName: node | 1834 | languageName: node |
1842 | linkType: hard | 1835 | linkType: hard |
1843 | 1836 | ||
1844 | "@mui/styled-engine@npm:^5.10.1": | 1837 | "@mui/styled-engine@npm:^5.10.2": |
1845 | version: 5.10.1 | 1838 | version: 5.10.2 |
1846 | resolution: "@mui/styled-engine@npm:5.10.1" | 1839 | resolution: "@mui/styled-engine@npm:5.10.2" |
1847 | dependencies: | 1840 | dependencies: |
1848 | "@babel/runtime": ^7.17.2 | 1841 | "@babel/runtime": ^7.17.2 |
1849 | "@emotion/cache": ^11.9.3 | 1842 | "@emotion/cache": ^11.10.1 |
1850 | csstype: ^3.1.0 | 1843 | csstype: ^3.1.0 |
1851 | prop-types: ^15.8.1 | 1844 | prop-types: ^15.8.1 |
1852 | peerDependencies: | 1845 | peerDependencies: |
@@ -1858,17 +1851,17 @@ __metadata: | |||
1858 | optional: true | 1851 | optional: true |
1859 | "@emotion/styled": | 1852 | "@emotion/styled": |
1860 | optional: true | 1853 | optional: true |
1861 | checksum: 27c4003bdb1f8a76f30ed6c458789631a93efee832631edeb63940ca3ede70f8aee8e39987b1da1b8717fcbd907d00554d2b5fecc83266eee70c3637c9b66712 | 1854 | checksum: 337455da69990ef83b10f7e0ee14720b991ae2dca9c616664931edbc17f209f3f9fc0abcad97c01d70cd72afbb0760f44c9aefe27c458fb6104d4a3f0078b812 |
1862 | languageName: node | 1855 | languageName: node |
1863 | linkType: hard | 1856 | linkType: hard |
1864 | 1857 | ||
1865 | "@mui/system@npm:^5.10.1": | 1858 | "@mui/system@npm:^5.10.2": |
1866 | version: 5.10.1 | 1859 | version: 5.10.2 |
1867 | resolution: "@mui/system@npm:5.10.1" | 1860 | resolution: "@mui/system@npm:5.10.2" |
1868 | dependencies: | 1861 | dependencies: |
1869 | "@babel/runtime": ^7.17.2 | 1862 | "@babel/runtime": ^7.17.2 |
1870 | "@mui/private-theming": ^5.9.3 | 1863 | "@mui/private-theming": ^5.9.3 |
1871 | "@mui/styled-engine": ^5.10.1 | 1864 | "@mui/styled-engine": ^5.10.2 |
1872 | "@mui/types": ^7.1.5 | 1865 | "@mui/types": ^7.1.5 |
1873 | "@mui/utils": ^5.9.3 | 1866 | "@mui/utils": ^5.9.3 |
1874 | clsx: ^1.2.1 | 1867 | clsx: ^1.2.1 |
@@ -1886,7 +1879,7 @@ __metadata: | |||
1886 | optional: true | 1879 | optional: true |
1887 | "@types/react": | 1880 | "@types/react": |
1888 | optional: true | 1881 | optional: true |
1889 | checksum: 0acb163dec856af3813ff043b4d8441d44431add7cb234cf16d386b2b70e30cba6261b51ac936f886d746f4b3ab3475b58e33e09f76cd92a6f6aadd61dca3927 | 1882 | checksum: 07bdac55e8a29d5397579025a47258b885bd54b60cd9f7bc7214526960ba076ab975ba0a217b2420fd48960aaaec4bceea37ae404f7452cc2048c677fc91790a |
1890 | languageName: node | 1883 | languageName: node |
1891 | linkType: hard | 1884 | linkType: hard |
1892 | 1885 | ||
@@ -1998,7 +1991,6 @@ __metadata: | |||
1998 | "@codemirror/view": ^6.2.0 | 1991 | "@codemirror/view": ^6.2.0 |
1999 | "@emotion/react": ^11.10.0 | 1992 | "@emotion/react": ^11.10.0 |
2000 | "@emotion/styled": ^11.10.0 | 1993 | "@emotion/styled": ^11.10.0 |
2001 | "@fontsource/inter": ^4.5.12 | ||
2002 | "@fontsource/jetbrains-mono": ^4.5.10 | 1994 | "@fontsource/jetbrains-mono": ^4.5.10 |
2003 | "@fontsource/roboto": ^4.5.8 | 1995 | "@fontsource/roboto": ^4.5.8 |
2004 | "@lezer/common": ^1.0.0 | 1996 | "@lezer/common": ^1.0.0 |
@@ -2006,26 +1998,27 @@ __metadata: | |||
2006 | "@lezer/highlight": ^1.0.0 | 1998 | "@lezer/highlight": ^1.0.0 |
2007 | "@lezer/lr": ^1.2.3 | 1999 | "@lezer/lr": ^1.2.3 |
2008 | "@material-icons/svg": ^1.0.32 | 2000 | "@material-icons/svg": ^1.0.32 |
2009 | "@mui/icons-material": 5.8.4 | 2001 | "@mui/icons-material": 5.10.2 |
2010 | "@mui/material": 5.10.1 | 2002 | "@mui/material": 5.10.2 |
2011 | "@types/eslint": ^8.4.6 | 2003 | "@types/eslint": ^8.4.6 |
2012 | "@types/html-minifier-terser": ^7.0.0 | 2004 | "@types/html-minifier-terser": ^7.0.0 |
2013 | "@types/lodash-es": ^4.17.6 | 2005 | "@types/lodash-es": ^4.17.6 |
2014 | "@types/node": ^18.7.8 | 2006 | "@types/node": ^18.7.13 |
2015 | "@types/prettier": ^2.7.0 | 2007 | "@types/prettier": ^2.7.0 |
2016 | "@types/react": ^18.0.17 | 2008 | "@types/react": ^18.0.17 |
2017 | "@types/react-dom": ^18.0.6 | 2009 | "@types/react-dom": ^18.0.6 |
2018 | "@typescript-eslint/eslint-plugin": ^5.33.1 | 2010 | "@typescript-eslint/eslint-plugin": ^5.35.1 |
2019 | "@typescript-eslint/parser": ^5.33.1 | 2011 | "@typescript-eslint/parser": ^5.35.1 |
2020 | "@vitejs/plugin-react": ^2.0.1 | 2012 | "@vitejs/plugin-react": ^2.0.1 |
2021 | ansi-styles: ^6.1.0 | 2013 | ansi-styles: ^6.1.0 |
2014 | async-mutex: ^0.3.2 | ||
2022 | cross-env: ^7.0.3 | 2015 | cross-env: ^7.0.3 |
2023 | escape-string-regexp: ^5.0.0 | 2016 | escape-string-regexp: ^5.0.0 |
2024 | eslint: ^8.22.0 | 2017 | eslint: ^8.22.0 |
2025 | eslint-config-airbnb: ^19.0.4 | 2018 | eslint-config-airbnb: ^19.0.4 |
2026 | eslint-config-airbnb-typescript: ^17.0.0 | 2019 | eslint-config-airbnb-typescript: ^17.0.0 |
2027 | eslint-config-prettier: ^8.5.0 | 2020 | eslint-config-prettier: ^8.5.0 |
2028 | eslint-import-resolver-typescript: ^3.4.2 | 2021 | eslint-import-resolver-typescript: ^3.5.0 |
2029 | eslint-plugin-import: ^2.26.0 | 2022 | eslint-plugin-import: ^2.26.0 |
2030 | eslint-plugin-jsx-a11y: ^6.6.1 | 2023 | eslint-plugin-jsx-a11y: ^6.6.1 |
2031 | eslint-plugin-prettier: ^4.2.1 | 2024 | eslint-plugin-prettier: ^4.2.1 |
@@ -2044,7 +2037,7 @@ __metadata: | |||
2044 | react-dom: ^18.2.0 | 2037 | react-dom: ^18.2.0 |
2045 | typescript: ~4.7.4 | 2038 | typescript: ~4.7.4 |
2046 | vite: ^3.0.9 | 2039 | vite: ^3.0.9 |
2047 | vite-plugin-inject-preload: ^1.0.1 | 2040 | vite-plugin-inject-preload: ^1.1.0 |
2048 | vite-plugin-pwa: ^0.12.3 | 2041 | vite-plugin-pwa: ^0.12.3 |
2049 | workbox-window: ^6.5.4 | 2042 | workbox-window: ^6.5.4 |
2050 | zod: ^3.18.0 | 2043 | zod: ^3.18.0 |
@@ -2198,10 +2191,10 @@ __metadata: | |||
2198 | languageName: node | 2191 | languageName: node |
2199 | linkType: hard | 2192 | linkType: hard |
2200 | 2193 | ||
2201 | "@types/node@npm:*, @types/node@npm:^18.7.8": | 2194 | "@types/node@npm:*, @types/node@npm:^18.7.13": |
2202 | version: 18.7.8 | 2195 | version: 18.7.13 |
2203 | resolution: "@types/node@npm:18.7.8" | 2196 | resolution: "@types/node@npm:18.7.13" |
2204 | checksum: e0125efefa896083c05f549d93166109959ffdd68cb626aad0d660c0ce9de888fe405b4763b4a3c3e0968560409c272413e0ad07204522543c688e162a617ecb | 2197 | checksum: 45431e7e89ecaf85c7d2c180d801c132a7c59e2f8ad578726b6d71cc74e3267c18f9ccdcad738bc0479790c078f0c79efb0e58da2c6be535c15995dbb19050c9 |
2205 | languageName: node | 2198 | languageName: node |
2206 | linkType: hard | 2199 | linkType: hard |
2207 | 2200 | ||
@@ -2287,13 +2280,13 @@ __metadata: | |||
2287 | languageName: node | 2280 | languageName: node |
2288 | linkType: hard | 2281 | linkType: hard |
2289 | 2282 | ||
2290 | "@typescript-eslint/eslint-plugin@npm:^5.33.1": | 2283 | "@typescript-eslint/eslint-plugin@npm:^5.35.1": |
2291 | version: 5.33.1 | 2284 | version: 5.35.1 |
2292 | resolution: "@typescript-eslint/eslint-plugin@npm:5.33.1" | 2285 | resolution: "@typescript-eslint/eslint-plugin@npm:5.35.1" |
2293 | dependencies: | 2286 | dependencies: |
2294 | "@typescript-eslint/scope-manager": 5.33.1 | 2287 | "@typescript-eslint/scope-manager": 5.35.1 |
2295 | "@typescript-eslint/type-utils": 5.33.1 | 2288 | "@typescript-eslint/type-utils": 5.35.1 |
2296 | "@typescript-eslint/utils": 5.33.1 | 2289 | "@typescript-eslint/utils": 5.35.1 |
2297 | debug: ^4.3.4 | 2290 | debug: ^4.3.4 |
2298 | functional-red-black-tree: ^1.0.1 | 2291 | functional-red-black-tree: ^1.0.1 |
2299 | ignore: ^5.2.0 | 2292 | ignore: ^5.2.0 |
@@ -2306,42 +2299,42 @@ __metadata: | |||
2306 | peerDependenciesMeta: | 2299 | peerDependenciesMeta: |
2307 | typescript: | 2300 | typescript: |
2308 | optional: true | 2301 | optional: true |
2309 | checksum: d9b6b038f70e4959ad211c84f50a38de2d00b54f0636ad76eea414fb070fa616933690da80de6668e62c8fbbeb227086322001b7d7ad1924421a232547c97936 | 2302 | checksum: 073f4dffd863881f1c87e1c217ac13bda44aaa2db12ef260032b5e8eb6ffd6b9cf6f62c85132dbf84152f353c435c66dd4f75c3bcb86eb23e926737aa4fb66fa |
2310 | languageName: node | 2303 | languageName: node |
2311 | linkType: hard | 2304 | linkType: hard |
2312 | 2305 | ||
2313 | "@typescript-eslint/parser@npm:^5.33.1": | 2306 | "@typescript-eslint/parser@npm:^5.35.1": |
2314 | version: 5.33.1 | 2307 | version: 5.35.1 |
2315 | resolution: "@typescript-eslint/parser@npm:5.33.1" | 2308 | resolution: "@typescript-eslint/parser@npm:5.35.1" |
2316 | dependencies: | 2309 | dependencies: |
2317 | "@typescript-eslint/scope-manager": 5.33.1 | 2310 | "@typescript-eslint/scope-manager": 5.35.1 |
2318 | "@typescript-eslint/types": 5.33.1 | 2311 | "@typescript-eslint/types": 5.35.1 |
2319 | "@typescript-eslint/typescript-estree": 5.33.1 | 2312 | "@typescript-eslint/typescript-estree": 5.35.1 |
2320 | debug: ^4.3.4 | 2313 | debug: ^4.3.4 |
2321 | peerDependencies: | 2314 | peerDependencies: |
2322 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 | 2315 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 |
2323 | peerDependenciesMeta: | 2316 | peerDependenciesMeta: |
2324 | typescript: | 2317 | typescript: |
2325 | optional: true | 2318 | optional: true |
2326 | checksum: fb3a4e000ce6d9583656fc3b3fb80f127a0ec1b7c3872ea469164516d993a588859ded4ec1338e6bbf2151168380d8aa29ec31027af23b50f5107949f8e7b438 | 2319 | checksum: 57ea1a1da60b370f8d5c11c86155f7339359a90f2c59e34c89f626f1a79cb440248f07bd307a27ebbbcc997d2731cb9754cdbc37639770940521a938dd89870c |
2327 | languageName: node | 2320 | languageName: node |
2328 | linkType: hard | 2321 | linkType: hard |
2329 | 2322 | ||
2330 | "@typescript-eslint/scope-manager@npm:5.33.1": | 2323 | "@typescript-eslint/scope-manager@npm:5.35.1": |
2331 | version: 5.33.1 | 2324 | version: 5.35.1 |
2332 | resolution: "@typescript-eslint/scope-manager@npm:5.33.1" | 2325 | resolution: "@typescript-eslint/scope-manager@npm:5.35.1" |
2333 | dependencies: | 2326 | dependencies: |
2334 | "@typescript-eslint/types": 5.33.1 | 2327 | "@typescript-eslint/types": 5.35.1 |
2335 | "@typescript-eslint/visitor-keys": 5.33.1 | 2328 | "@typescript-eslint/visitor-keys": 5.35.1 |
2336 | checksum: b9918d8320ea59081d19070ce952b56984e72fb2c113215e5e6a0f97deac9aae5aa67ec7a07cddb010c0f75cdf8df096ab45e9241e4b7b611acfa6d4cdfb6516 | 2329 | checksum: 5a969a081309bac5962f99ee6dfdfd9c68ea677bc79d9796592dce82a36217f67aa55c7bf421b2c97b46c5149d6a9401bb4c57829595e8c19f47cfa9e8c2dd86 |
2337 | languageName: node | 2330 | languageName: node |
2338 | linkType: hard | 2331 | linkType: hard |
2339 | 2332 | ||
2340 | "@typescript-eslint/type-utils@npm:5.33.1": | 2333 | "@typescript-eslint/type-utils@npm:5.35.1": |
2341 | version: 5.33.1 | 2334 | version: 5.35.1 |
2342 | resolution: "@typescript-eslint/type-utils@npm:5.33.1" | 2335 | resolution: "@typescript-eslint/type-utils@npm:5.35.1" |
2343 | dependencies: | 2336 | dependencies: |
2344 | "@typescript-eslint/utils": 5.33.1 | 2337 | "@typescript-eslint/utils": 5.35.1 |
2345 | debug: ^4.3.4 | 2338 | debug: ^4.3.4 |
2346 | tsutils: ^3.21.0 | 2339 | tsutils: ^3.21.0 |
2347 | peerDependencies: | 2340 | peerDependencies: |
@@ -2349,23 +2342,23 @@ __metadata: | |||
2349 | peerDependenciesMeta: | 2342 | peerDependenciesMeta: |
2350 | typescript: | 2343 | typescript: |
2351 | optional: true | 2344 | optional: true |
2352 | checksum: ddf88835bc87b3ad946aaeb29b770a49a8e1c3c5e294ee9cb93b1936f432a1016efb97803f197eea1be61545cbc79b5526cc05e9339ca9beada22fc83801ddea | 2345 | checksum: af317ba156f2767f76a7f97193873a00468370e157fdcc6ac19f664bc6c4c0a6836bd25028d17fdd54d339b6842fda68b82f1ce4142a222de6953625ea6c0a9c |
2353 | languageName: node | 2346 | languageName: node |
2354 | linkType: hard | 2347 | linkType: hard |
2355 | 2348 | ||
2356 | "@typescript-eslint/types@npm:5.33.1": | 2349 | "@typescript-eslint/types@npm:5.35.1": |
2357 | version: 5.33.1 | 2350 | version: 5.35.1 |
2358 | resolution: "@typescript-eslint/types@npm:5.33.1" | 2351 | resolution: "@typescript-eslint/types@npm:5.35.1" |
2359 | checksum: 122891bd4ab4b930b1d33f3ce43a010825c1e61b9879520a0f3dc34cf92df71e2a873410845ab8d746333511c455c115eaafdec149298a161cef713829dfdb77 | 2352 | checksum: a4e1001867f43f3364b109fc5a07b91ae7a34b78ab191c6c5c4695dac9bb2b80b0a602651c0b807c1c7c1fc3656d2bbd47c637afa08a09e7b1c39eae3c489e00 |
2360 | languageName: node | 2353 | languageName: node |
2361 | linkType: hard | 2354 | linkType: hard |
2362 | 2355 | ||
2363 | "@typescript-eslint/typescript-estree@npm:5.33.1": | 2356 | "@typescript-eslint/typescript-estree@npm:5.35.1": |
2364 | version: 5.33.1 | 2357 | version: 5.35.1 |
2365 | resolution: "@typescript-eslint/typescript-estree@npm:5.33.1" | 2358 | resolution: "@typescript-eslint/typescript-estree@npm:5.35.1" |
2366 | dependencies: | 2359 | dependencies: |
2367 | "@typescript-eslint/types": 5.33.1 | 2360 | "@typescript-eslint/types": 5.35.1 |
2368 | "@typescript-eslint/visitor-keys": 5.33.1 | 2361 | "@typescript-eslint/visitor-keys": 5.35.1 |
2369 | debug: ^4.3.4 | 2362 | debug: ^4.3.4 |
2370 | globby: ^11.1.0 | 2363 | globby: ^11.1.0 |
2371 | is-glob: ^4.0.3 | 2364 | is-glob: ^4.0.3 |
@@ -2374,33 +2367,33 @@ __metadata: | |||
2374 | peerDependenciesMeta: | 2367 | peerDependenciesMeta: |
2375 | typescript: | 2368 | typescript: |
2376 | optional: true | 2369 | optional: true |
2377 | checksum: 1418e409b141c2f012bc2dd5c40d95dfd8aa572dd3e9523ed23e4371e4459d10ecd074fda75dc770ce980686b25ffc44725eebf165c494818ed4131d1ac0239f | 2370 | checksum: a917ca4753a3f92c8d8555c96f5414383a9742761625476fa36a019401543aa74996159afa0f7fc7fae05fe0f904e3c6f4153a55412070c8a94e8171e81084c7 |
2378 | languageName: node | 2371 | languageName: node |
2379 | linkType: hard | 2372 | linkType: hard |
2380 | 2373 | ||
2381 | "@typescript-eslint/utils@npm:5.33.1": | 2374 | "@typescript-eslint/utils@npm:5.35.1": |
2382 | version: 5.33.1 | 2375 | version: 5.35.1 |
2383 | resolution: "@typescript-eslint/utils@npm:5.33.1" | 2376 | resolution: "@typescript-eslint/utils@npm:5.35.1" |
2384 | dependencies: | 2377 | dependencies: |
2385 | "@types/json-schema": ^7.0.9 | 2378 | "@types/json-schema": ^7.0.9 |
2386 | "@typescript-eslint/scope-manager": 5.33.1 | 2379 | "@typescript-eslint/scope-manager": 5.35.1 |
2387 | "@typescript-eslint/types": 5.33.1 | 2380 | "@typescript-eslint/types": 5.35.1 |
2388 | "@typescript-eslint/typescript-estree": 5.33.1 | 2381 | "@typescript-eslint/typescript-estree": 5.35.1 |
2389 | eslint-scope: ^5.1.1 | 2382 | eslint-scope: ^5.1.1 |
2390 | eslint-utils: ^3.0.0 | 2383 | eslint-utils: ^3.0.0 |
2391 | peerDependencies: | 2384 | peerDependencies: |
2392 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 | 2385 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 |
2393 | checksum: c550504d62fc72f29bf3d7a651bd3a81f49fb1fccaf47583721c2ab1abd2ef78a1e4bc392cb4be4a61a45a4f24fc14a59d67b98aac8a16a207a7cace86538cab | 2386 | checksum: 2b04092583c3139dd090727c24fb9d7fdb1fb9f20f2e3f0141cab5b98b6a1934b0fc8cab948f7faae55588385b0f1fb7bbf91f52c705ce4528036a527c3119c6 |
2394 | languageName: node | 2387 | languageName: node |
2395 | linkType: hard | 2388 | linkType: hard |
2396 | 2389 | ||
2397 | "@typescript-eslint/visitor-keys@npm:5.33.1": | 2390 | "@typescript-eslint/visitor-keys@npm:5.35.1": |
2398 | version: 5.33.1 | 2391 | version: 5.35.1 |
2399 | resolution: "@typescript-eslint/visitor-keys@npm:5.33.1" | 2392 | resolution: "@typescript-eslint/visitor-keys@npm:5.35.1" |
2400 | dependencies: | 2393 | dependencies: |
2401 | "@typescript-eslint/types": 5.33.1 | 2394 | "@typescript-eslint/types": 5.35.1 |
2402 | eslint-visitor-keys: ^3.3.0 | 2395 | eslint-visitor-keys: ^3.3.0 |
2403 | checksum: 0d32a433450f61e97b5fa6b1e167f06ed395c200b16b4dbd4490a1c4941de420689b622f8a2486f5398806fb24f57b9fab901b4cbc8fdb8853f568264b3a182a | 2396 | checksum: ef3c8377aac89935b5cc2fcf37bb3e42aa5f98848e7c22bdcbe5bb06c0fe8a1373a6897fd21109be8929b4708ad06c8874d2ef7bba17ff64911964203457330d |
2404 | languageName: node | 2397 | languageName: node |
2405 | linkType: hard | 2398 | linkType: hard |
2406 | 2399 | ||
@@ -2616,6 +2609,15 @@ __metadata: | |||
2616 | languageName: node | 2609 | languageName: node |
2617 | linkType: hard | 2610 | linkType: hard |
2618 | 2611 | ||
2612 | "async-mutex@npm:^0.3.2": | ||
2613 | version: 0.3.2 | ||
2614 | resolution: "async-mutex@npm:0.3.2" | ||
2615 | dependencies: | ||
2616 | tslib: ^2.3.1 | ||
2617 | checksum: 620b771dfdea1cad0a6b712915c31a1e3ca880a8cf1eae92b4590f435995e0260929c6ebaae0b9126b1456790ea498064b5bb9a506948cda760f48d3d0dcc4c8 | ||
2618 | languageName: node | ||
2619 | linkType: hard | ||
2620 | |||
2619 | "async@npm:^3.2.3": | 2621 | "async@npm:^3.2.3": |
2620 | version: 3.2.4 | 2622 | version: 3.2.4 |
2621 | resolution: "async@npm:3.2.4" | 2623 | resolution: "async@npm:3.2.4" |
@@ -3610,21 +3612,21 @@ __metadata: | |||
3610 | languageName: node | 3612 | languageName: node |
3611 | linkType: hard | 3613 | linkType: hard |
3612 | 3614 | ||
3613 | "eslint-import-resolver-typescript@npm:^3.4.2": | 3615 | "eslint-import-resolver-typescript@npm:^3.5.0": |
3614 | version: 3.4.2 | 3616 | version: 3.5.0 |
3615 | resolution: "eslint-import-resolver-typescript@npm:3.4.2" | 3617 | resolution: "eslint-import-resolver-typescript@npm:3.5.0" |
3616 | dependencies: | 3618 | dependencies: |
3617 | debug: ^4.3.4 | 3619 | debug: ^4.3.4 |
3618 | enhanced-resolve: ^5.10.0 | 3620 | enhanced-resolve: ^5.10.0 |
3619 | get-tsconfig: ^4.2.0 | 3621 | get-tsconfig: ^4.2.0 |
3620 | globby: ^13.1.2 | 3622 | globby: ^13.1.2 |
3621 | is-core-module: ^2.9.0 | 3623 | is-core-module: ^2.10.0 |
3622 | is-glob: ^4.0.3 | 3624 | is-glob: ^4.0.3 |
3623 | synckit: ^0.8.3 | 3625 | synckit: ^0.8.3 |
3624 | peerDependencies: | 3626 | peerDependencies: |
3625 | eslint: "*" | 3627 | eslint: "*" |
3626 | eslint-plugin-import: "*" | 3628 | eslint-plugin-import: "*" |
3627 | checksum: e54d8c02542cc85fc9703a064111a25585a7189b61445a209792ae50b718d273e5522be44c61f149f59292570eef6a08036d988c8477da9f6d8f3c6177801319 | 3629 | checksum: 9719d1f68b7bb0eaf8939cff2d3b02b526949f73db744877de781640650dd4d0a17d934222b9ac69e27d9f363ee4569c1aa1a2a2aab6500257517f9bf7d25976 |
3628 | languageName: node | 3630 | languageName: node |
3629 | linkType: hard | 3631 | linkType: hard |
3630 | 3632 | ||
@@ -4492,12 +4494,12 @@ __metadata: | |||
4492 | languageName: node | 4494 | languageName: node |
4493 | linkType: hard | 4495 | linkType: hard |
4494 | 4496 | ||
4495 | "is-core-module@npm:^2.2.0, is-core-module@npm:^2.8.1, is-core-module@npm:^2.9.0": | 4497 | "is-core-module@npm:^2.10.0, is-core-module@npm:^2.2.0, is-core-module@npm:^2.8.1, is-core-module@npm:^2.9.0": |
4496 | version: 2.9.0 | 4498 | version: 2.10.0 |
4497 | resolution: "is-core-module@npm:2.9.0" | 4499 | resolution: "is-core-module@npm:2.10.0" |
4498 | dependencies: | 4500 | dependencies: |
4499 | has: ^1.0.3 | 4501 | has: ^1.0.3 |
4500 | checksum: b27034318b4b462f1c8f1dfb1b32baecd651d891a4e2d1922135daeff4141dfced2b82b07aef83ef54275c4a3526aa38da859223664d0868ca24182badb784ce | 4502 | checksum: 0f3f77811f430af3256fa7bbc806f9639534b140f8ee69476f632c3e1eb4e28a38be0b9d1b8ecf596179c841b53576129279df95e7051d694dac4ceb6f967593 |
4501 | languageName: node | 4503 | languageName: node |
4502 | linkType: hard | 4504 | linkType: hard |
4503 | 4505 | ||
@@ -6410,7 +6412,7 @@ __metadata: | |||
6410 | languageName: node | 6412 | languageName: node |
6411 | linkType: hard | 6413 | linkType: hard |
6412 | 6414 | ||
6413 | "tslib@npm:^2.0.3, tslib@npm:^2.4.0": | 6415 | "tslib@npm:^2.0.3, tslib@npm:^2.3.1, tslib@npm:^2.4.0": |
6414 | version: 2.4.0 | 6416 | version: 2.4.0 |
6415 | resolution: "tslib@npm:2.4.0" | 6417 | resolution: "tslib@npm:2.4.0" |
6416 | checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113 | 6418 | checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113 |
@@ -6592,14 +6594,14 @@ __metadata: | |||
6592 | languageName: node | 6594 | languageName: node |
6593 | linkType: hard | 6595 | linkType: hard |
6594 | 6596 | ||
6595 | "vite-plugin-inject-preload@npm:^1.0.1": | 6597 | "vite-plugin-inject-preload@npm:^1.1.0": |
6596 | version: 1.0.1 | 6598 | version: 1.1.0 |
6597 | resolution: "vite-plugin-inject-preload@npm:1.0.1" | 6599 | resolution: "vite-plugin-inject-preload@npm:1.1.0" |
6598 | dependencies: | 6600 | dependencies: |
6599 | mime-types: ^2.1.35 | 6601 | mime-types: ^2.1.35 |
6600 | peerDependencies: | 6602 | peerDependencies: |
6601 | vite: ^2.9.0 || ^3.0.0-0 | 6603 | vite: ^2.9.0 || ^3.0.0-0 |
6602 | checksum: 0504be885942752933240f99192b677c27f0f56297546e9db289312784f71f60fa30dff896f89677af7a2c84eed10bc8af33c41bd284c63a3b59379030f383fd | 6604 | checksum: 51123f1e5bb5cf1d7830e964e0b81b4d64e92a1176b86fea6ec33ea28601673a2315a63a66fc8aa79f6dc3e8c18a847f5dfabbefee19db507aa548318f12913e |
6603 | languageName: node | 6605 | languageName: node |
6604 | linkType: hard | 6606 | linkType: hard |
6605 | 6607 | ||