@antelopejs/dms-database 0.0.14 → 0.1.1

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.
Files changed (173) hide show
  1. package/README.md +30 -5
  2. package/dist/db/models/query_history.model.d.ts +1 -0
  3. package/dist/db/models/query_history.model.js +6 -0
  4. package/dist/db/models/query_history.model.js.map +1 -1
  5. package/dist/db/tables/query_history.table.d.ts +3 -0
  6. package/dist/db/tables/query_history.table.js +12 -0
  7. package/dist/db/tables/query_history.table.js.map +1 -1
  8. package/dist/db/tables/saved_queries.table.d.ts +1 -0
  9. package/dist/db/tables/saved_queries.table.js +4 -0
  10. package/dist/db/tables/saved_queries.table.js.map +1 -1
  11. package/dist/i18n/messages.d.ts +39 -0
  12. package/dist/i18n/messages.js +64 -0
  13. package/dist/i18n/messages.js.map +1 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/module.d.ts +11 -0
  18. package/dist/module.js +55 -0
  19. package/dist/module.js.map +1 -0
  20. package/dist/pages/data.d.ts +1 -1
  21. package/dist/pages/data.js +9 -6
  22. package/dist/pages/data.js.map +1 -1
  23. package/dist/pages/diagram.d.ts +9 -0
  24. package/dist/pages/diagram.js +45 -0
  25. package/dist/pages/diagram.js.map +1 -0
  26. package/dist/pages/index.d.ts +1 -0
  27. package/dist/pages/index.js +1 -0
  28. package/dist/pages/index.js.map +1 -1
  29. package/dist/pages/overview.d.ts +4 -1
  30. package/dist/pages/overview.js +113 -6
  31. package/dist/pages/overview.js.map +1 -1
  32. package/dist/pages/query.d.ts +1 -1
  33. package/dist/pages/query.js +22 -6
  34. package/dist/pages/query.js.map +1 -1
  35. package/dist/pages/schemas.d.ts +2 -1
  36. package/dist/pages/schemas.js +143 -6
  37. package/dist/pages/schemas.js.map +1 -1
  38. package/dist/routes/data.d.ts +8 -0
  39. package/dist/routes/data.js +84 -21
  40. package/dist/routes/data.js.map +1 -1
  41. package/dist/routes/index.d.ts +1 -0
  42. package/dist/routes/index.js +1 -0
  43. package/dist/routes/index.js.map +1 -1
  44. package/dist/routes/overview.d.ts +12 -0
  45. package/dist/routes/overview.js +55 -0
  46. package/dist/routes/overview.js.map +1 -0
  47. package/dist/routes/query.d.ts +5 -1
  48. package/dist/routes/query.js +162 -27
  49. package/dist/routes/query.js.map +1 -1
  50. package/dist/routes/tables.d.ts +15 -12
  51. package/dist/routes/tables.js +95 -55
  52. package/dist/routes/tables.js.map +1 -1
  53. package/dist/service/catalog.d.ts +3 -0
  54. package/dist/service/catalog.js +26 -0
  55. package/dist/service/catalog.js.map +1 -0
  56. package/dist/service/health.d.ts +3 -3
  57. package/dist/service/health.js +17 -14
  58. package/dist/service/health.js.map +1 -1
  59. package/dist/service/introspect.d.ts +3 -1
  60. package/dist/service/introspect.js +131 -43
  61. package/dist/service/introspect.js.map +1 -1
  62. package/dist/service/overview.d.ts +9 -0
  63. package/dist/service/overview.js +113 -0
  64. package/dist/service/overview.js.map +1 -0
  65. package/dist/service/queryInspection.d.ts +15 -0
  66. package/dist/service/queryInspection.js +107 -0
  67. package/dist/service/queryInspection.js.map +1 -0
  68. package/dist/service/references.d.ts +9 -0
  69. package/dist/service/references.js +52 -0
  70. package/dist/service/references.js.map +1 -0
  71. package/dist/service/schemaFilters.d.ts +26 -0
  72. package/dist/service/schemaFilters.js +42 -0
  73. package/dist/service/schemaFilters.js.map +1 -0
  74. package/dist/service/stagedSerialization.d.ts +2 -0
  75. package/dist/service/stagedSerialization.js +38 -10
  76. package/dist/service/stagedSerialization.js.map +1 -1
  77. package/dist/service/types.d.ts +11 -1
  78. package/dist/types/constants.d.ts +2 -0
  79. package/dist/types/constants.js +3 -1
  80. package/dist/types/constants.js.map +1 -1
  81. package/dist/utils/query.d.ts +0 -2
  82. package/dist/utils/query.js +0 -20
  83. package/dist/utils/query.js.map +1 -1
  84. package/frontend-vue/app/{composables → build/composables}/useDataBrowserGrid.ts +65 -50
  85. package/frontend-vue/app/build/composables/useDataBrowserTabs.ts +366 -0
  86. package/frontend-vue/app/build/composables/useDatabaseHealth.ts +50 -0
  87. package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +140 -0
  88. package/frontend-vue/app/{composables → build/composables}/useDiagramPersistence.ts +47 -39
  89. package/frontend-vue/app/build/composables/useQueryRuntime.ts +217 -0
  90. package/frontend-vue/app/build/composables/useQueryStore.ts +162 -0
  91. package/frontend-vue/app/build/data/cellValues.ts +132 -0
  92. package/frontend-vue/app/build/data/instanceOptions.ts +36 -0
  93. package/frontend-vue/app/build/data/pickerMenus.ts +175 -0
  94. package/frontend-vue/app/build/data/recentTables.ts +48 -0
  95. package/frontend-vue/app/build/data/stagedEdits.ts +117 -0
  96. package/frontend-vue/app/build/data/stagedWrites.ts +64 -0
  97. package/frontend-vue/app/build/data/useStagedSave.ts +298 -0
  98. package/frontend-vue/app/build/diagram/autosave.ts +72 -0
  99. package/frontend-vue/app/build/diagram/graph.ts +585 -0
  100. package/frontend-vue/app/build/diagram/history.ts +178 -0
  101. package/frontend-vue/app/build/diagram/shortcuts.ts +62 -0
  102. package/frontend-vue/app/build/diagram/sync.ts +137 -0
  103. package/frontend-vue/app/build/query/completion.ts +100 -0
  104. package/frontend-vue/app/build/query/csv.ts +32 -0
  105. package/frontend-vue/app/build/query/examples.ts +38 -0
  106. package/frontend-vue/app/build/query/runs.ts +168 -0
  107. package/frontend-vue/app/build/query/tableColumns.ts +35 -0
  108. package/frontend-vue/app/build/query/target.ts +143 -0
  109. package/frontend-vue/app/build/schemas/filterState.ts +128 -0
  110. package/frontend-vue/app/build/schemas/useSchemasListing.ts +62 -0
  111. package/frontend-vue/app/build/utils/databaseLinks.ts +98 -0
  112. package/frontend-vue/app/build/utils/fieldTypes.ts +198 -0
  113. package/frontend-vue/app/build/utils/relativeTime.ts +19 -0
  114. package/frontend-vue/app/{utils → build/utils}/stagedSerialization.ts +7 -1
  115. package/frontend-vue/app/components/ConnectionStatus.vue +122 -0
  116. package/frontend-vue/app/components/DataBrowser.vue +126 -0
  117. package/frontend-vue/app/components/DataBrowserSidebar.vue +293 -210
  118. package/frontend-vue/app/components/DataBrowserTabBar.vue +185 -93
  119. package/frontend-vue/app/components/DataFilterBar.vue +163 -0
  120. package/frontend-vue/app/components/DataGrid.vue +737 -499
  121. package/frontend-vue/app/components/DataGridCell.vue +125 -77
  122. package/frontend-vue/app/components/DataGridHeaderCell.vue +119 -81
  123. package/frontend-vue/app/components/DiagramNoteNode.vue +81 -63
  124. package/frontend-vue/app/components/DiagramRelationEdge.vue +57 -27
  125. package/frontend-vue/app/components/DiagramStubNode.vue +16 -17
  126. package/frontend-vue/app/components/DiagramTableNode.vue +106 -68
  127. package/frontend-vue/app/components/InstancePicker.vue +195 -0
  128. package/frontend-vue/app/components/JsonCellPanel.vue +55 -41
  129. package/frontend-vue/app/components/QueryBuilderPanel.vue +290 -238
  130. package/frontend-vue/app/components/QueryConsole.vue +726 -0
  131. package/frontend-vue/app/components/QueryLibrary.vue +314 -0
  132. package/frontend-vue/app/components/QueryResultPanel.vue +556 -126
  133. package/frontend-vue/app/components/RowDrawer.vue +520 -0
  134. package/frontend-vue/app/components/SchemaDiagram.vue +1016 -1051
  135. package/frontend-vue/app/components/SchemaPicker.vue +86 -0
  136. package/frontend-vue/app/components/SchemasFilterBar.vue +201 -0
  137. package/frontend-vue/app/components/TableInspector.vue +741 -0
  138. package/frontend-vue/app/config/shortcuts-registry.ts +65 -0
  139. package/frontend-vue/dms.frontend.ts +5 -14
  140. package/frontend-vue/i18n/locales/database-en-GB.json +527 -193
  141. package/frontend-vue/i18n/locales/database-fr-FR.json +529 -195
  142. package/frontend-vue/package.json +2 -1
  143. package/frontend-vue/pnpm-lock.yaml +2601 -858
  144. package/frontend-vue/pnpm-workspace.yaml +1 -1
  145. package/frontend-vue/tests/dataBrowser.test.ts +206 -0
  146. package/frontend-vue/tests/diagram.test.ts +516 -0
  147. package/frontend-vue/tests/helpers.test.ts +205 -0
  148. package/frontend-vue/tests/instanceOptions.test.ts +53 -0
  149. package/frontend-vue/tests/locales.test.ts +56 -0
  150. package/frontend-vue/tests/persistence.test.ts +106 -5
  151. package/frontend-vue/tests/pickerMenus.test.ts +254 -0
  152. package/frontend-vue/tests/queryConsole.test.ts +282 -0
  153. package/frontend-vue/tests/queryRuntime.test.ts +2 -2
  154. package/frontend-vue/tests/registration.test.ts +36 -25
  155. package/frontend-vue/tests/schemasFilters.test.ts +115 -0
  156. package/package.json +7 -7
  157. package/frontend-vue/app/components/QueryFavoritesPanel.vue +0 -127
  158. package/frontend-vue/app/components/QueryHistoryPanel.vue +0 -134
  159. package/frontend-vue/app/components/SchemaTableInspector.vue +0 -278
  160. package/frontend-vue/app/composables/useDataBrowserTabs.ts +0 -255
  161. package/frontend-vue/app/composables/useDatabaseHealth.ts +0 -50
  162. package/frontend-vue/app/composables/useDatabaseSchemas.ts +0 -92
  163. package/frontend-vue/app/composables/useQueryRuntime.ts +0 -118
  164. package/frontend-vue/app/composables/useQueryStore.ts +0 -117
  165. package/frontend-vue/app/composables/useTableViewRowClick.ts +0 -22
  166. package/frontend-vue/app/custom-pages/database/data.vue +0 -41
  167. package/frontend-vue/app/custom-pages/database/overview.vue +0 -367
  168. package/frontend-vue/app/custom-pages/database/query.vue +0 -436
  169. package/frontend-vue/app/custom-pages/database/schemas.vue +0 -218
  170. package/frontend-vue/app/utils/databaseFieldModifiers.ts +0 -33
  171. package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +0 -224
  172. package/frontend-vue/app/utils/dmsTableView.ts +0 -83
  173. package/frontend-vue/app/utils/relativeTime.ts +0 -18
@@ -0,0 +1,298 @@
1
+ import {
2
+ DEFAULT_INSTANCE_VALUE,
3
+ instanceBadge,
4
+ } from '../composables/useDataBrowserTabs'
5
+ import { browseSelectionQuery } from '../composables/useDataBrowserGrid'
6
+ import { h, onScopeDispose } from 'vue'
7
+ import type { BrowserTab } from '../composables/useDataBrowserTabs'
8
+ import { cellLabel } from './cellValues'
9
+ import { type StagedChange, undoBodies, useStagedEdits } from './stagedEdits'
10
+ import { changesOf, type RowWriteOutcome, writeRows } from './stagedWrites'
11
+
12
+ // Writes the staged edits of a tab after the user reviewed them, then offers
13
+ // to take them back for a few seconds, in the save bar of the current tab
14
+ // (another tab ends the offer). Each row is written with one call of the
15
+ // edit route; an undo writes the previous values back the same way. Only the
16
+ // rows written are announced and undone: a row gone meanwhile drops its
17
+ // changes, and a failure stops the save with the rows left still staged.
18
+
19
+ const EDIT_ENDPOINT = '/api/database/browse/edit'
20
+ const UNDO_WINDOW_MS = 10_000
21
+ const UNDONE_NOTICE_MS = 3_000
22
+
23
+ /** What the save bar says once a save is written, then once it is undone. */
24
+ interface SaveNotice {
25
+ state: 'saved' | 'undoing' | 'undone'
26
+ changes: StagedChange[]
27
+ scope: string
28
+ }
29
+
30
+ interface StagedSaveOptions {
31
+ tab: () => BrowserTab
32
+ onSaved: () => void
33
+ }
34
+
35
+ /** The server's reason when it gave one, else the error's own message. */
36
+ function messageOf(error: unknown): string {
37
+ const data = (error as { data?: unknown } | null)?.data
38
+ if (typeof data === 'string' && data) return data
39
+ const message = (data as { message?: unknown } | null)?.message
40
+ if (typeof message === 'string' && message) return message
41
+ return error instanceof Error ? error.message : String(error)
42
+ }
43
+
44
+ export function useStagedSave(options: StagedSaveOptions) {
45
+ const { t, n } = useI18n()
46
+ const { $authFetch } = useAuthFetch()
47
+ const { confirm } = useConfirm()
48
+ const toast = useToast()
49
+ const staged = useStagedEdits()
50
+ const saving = ref(false)
51
+ const notice = ref<SaveNotice | null>(null)
52
+ let expiry: ReturnType<typeof setTimeout> | undefined
53
+
54
+ /** Shows a notice, for `ms` when given, or ends it with null. */
55
+ function show(next: SaveNotice | null, ms?: number) {
56
+ clearTimeout(expiry)
57
+ notice.value = next
58
+ if (next && ms !== undefined)
59
+ expiry = setTimeout(() => {
60
+ notice.value = null
61
+ }, ms)
62
+ }
63
+
64
+ // The notice belongs to the tab it was written on.
65
+ watch(
66
+ () => options.tab().id,
67
+ () => show(null),
68
+ )
69
+ onScopeDispose(() => clearTimeout(expiry))
70
+
71
+ // A new edit ends the undo: written under it, the old values would make
72
+ // its review show the wrong "was", and its save would overwrite them.
73
+ watch(
74
+ () => staged.count(options.tab().id),
75
+ (count) => {
76
+ if (count > 0 && notice.value?.state === 'saved') show(null)
77
+ },
78
+ )
79
+
80
+ function scope(tab: BrowserTab): string {
81
+ const badge =
82
+ tab.instance === DEFAULT_INSTANCE_VALUE
83
+ ? `@${t('dms_database.data.scope.default')}`
84
+ : instanceBadge(tab.instance, t('dms_database.data.scope.all'))
85
+ return `${tab.schema}.${tab.table} ${badge}`
86
+ }
87
+
88
+ async function writeRow(
89
+ tab: BrowserTab,
90
+ rowId: string,
91
+ body: Record<string, unknown>,
92
+ ) {
93
+ await $authFetch(EDIT_ENDPOINT, {
94
+ method: 'PUT',
95
+ query: { ...browseSelectionQuery(tab), id: rowId },
96
+ body,
97
+ })
98
+ }
99
+
100
+ // One change per row: the field and the row on a first line, the value
101
+ // before and after on its own line, wrapped, so neither hides the other.
102
+ function diffTable(changes: StagedChange[]) {
103
+ return h(
104
+ 'div',
105
+ {
106
+ class:
107
+ 'border-default divide-default max-h-72 divide-y overflow-auto rounded-md border font-mono text-[12px]',
108
+ },
109
+ changes.map((change) => {
110
+ const before = cellLabel(change.before)
111
+ const after = cellLabel(change.after)
112
+ return h('div', { class: 'grid gap-0.5 px-3 py-2' }, [
113
+ h('div', { class: 'flex min-w-0 items-baseline gap-2' }, [
114
+ h('span', { class: 'text-highlighted shrink-0' }, change.field),
115
+ h(
116
+ 'span',
117
+ { class: 'text-dimmed min-w-0 truncate', title: change.rowId },
118
+ change.rowId,
119
+ ),
120
+ ]),
121
+ h(
122
+ 'div',
123
+ {
124
+ class: 'line-clamp-4 break-all',
125
+ title: `${before} → ${after}`,
126
+ },
127
+ [
128
+ h('s', { class: 'text-dimmed' }, before),
129
+ ' → ',
130
+ h('span', { class: 'text-success' }, after),
131
+ ],
132
+ ),
133
+ ])
134
+ }),
135
+ )
136
+ }
137
+
138
+ // Each row written (or gone) leaves the staged edits at once: a failure
139
+ // further on keeps only the rows not written.
140
+ function saveAll(tab: BrowserTab): Promise<RowWriteOutcome> {
141
+ const rows = staged
142
+ .rowIds(tab.id)
143
+ .map((rowId): [string, Record<string, unknown>] => [
144
+ rowId,
145
+ staged.rowBody(tab.id, rowId),
146
+ ])
147
+ return writeRows(
148
+ rows,
149
+ (rowId, body) => writeRow(tab, rowId, body),
150
+ (rowId) => staged.discard(tab.id, [rowId]),
151
+ )
152
+ }
153
+
154
+ // What the review dialog says once a save went only part of the way;
155
+ // nothing written at all is an error, shown by the dialog for a retry.
156
+ function partialOutcome(
157
+ outcome: RowWriteOutcome,
158
+ changes: StagedChange[],
159
+ savedCount: number,
160
+ ) {
161
+ const nothingSettled =
162
+ outcome.written.length === 0 && outcome.gone.length === 0
163
+ if (outcome.error !== null && nothingSettled) throw outcome.error
164
+ if (outcome.error === null && outcome.gone.length === 0) return undefined
165
+ const details: string[] = []
166
+ if (outcome.gone.length > 0)
167
+ details.push(
168
+ t(
169
+ 'dms_database.data.save.rows_gone',
170
+ { count: n(outcome.gone.length) },
171
+ outcome.gone.length,
172
+ ),
173
+ )
174
+ if (outcome.error !== null) {
175
+ const left = changesOf(changes, outcome.left).length
176
+ details.push(
177
+ t(
178
+ 'dms_database.data.save.not_written',
179
+ { count: n(left), error: messageOf(outcome.error) },
180
+ left,
181
+ ),
182
+ )
183
+ }
184
+ return {
185
+ partial: {
186
+ title: t(
187
+ 'dms_database.data.save.partial_title',
188
+ { saved: n(savedCount), total: n(changes.length) },
189
+ changes.length,
190
+ ),
191
+ description: details.join(' '),
192
+ },
193
+ }
194
+ }
195
+
196
+ async function undo() {
197
+ const tab = options.tab()
198
+ const current = notice.value
199
+ if (current?.state !== 'saved') return
200
+ // Kept while the values are written back, however long that takes.
201
+ show({ ...current, state: 'undoing' })
202
+ const outcome = await writeRows(
203
+ undoBodies(current.changes),
204
+ (rowId, body) => writeRow(tab, rowId, body),
205
+ )
206
+ const restored = changesOf(current.changes, outcome.written)
207
+ const gone = changesOf(current.changes, outcome.gone)
208
+ const left = changesOf(current.changes, outcome.left)
209
+ // Another tab since: its bar says nothing of this one, the toasts do.
210
+ const here = options.tab().id === tab.id
211
+ if (left.length > 0) {
212
+ // The rows the failure left keep their undo, for a retry.
213
+ if (here)
214
+ show({ ...current, state: 'saved', changes: left }, UNDO_WINDOW_MS)
215
+ toast.add({
216
+ title: t(
217
+ 'dms_database.data.save.undo_partial',
218
+ { count: n(left.length + gone.length) },
219
+ left.length + gone.length,
220
+ ),
221
+ description: messageOf(outcome.error),
222
+ color: 'error',
223
+ })
224
+ } else if (here) {
225
+ show(
226
+ restored.length > 0
227
+ ? { ...current, state: 'undone', changes: restored }
228
+ : null,
229
+ UNDONE_NOTICE_MS,
230
+ )
231
+ }
232
+ if (gone.length > 0 && left.length === 0)
233
+ toast.add({
234
+ title: t(
235
+ 'dms_database.data.save.undo_partial',
236
+ { count: n(gone.length) },
237
+ gone.length,
238
+ ),
239
+ description: t(
240
+ 'dms_database.data.save.rows_missing',
241
+ { count: n(outcome.gone.length) },
242
+ outcome.gone.length,
243
+ ),
244
+ color: 'warning',
245
+ })
246
+ options.onSaved()
247
+ }
248
+
249
+ function announce(tab: BrowserTab, saved: StagedChange[]) {
250
+ if (options.tab().id !== tab.id) return
251
+ show({ state: 'saved', changes: saved, scope: scope(tab) }, UNDO_WINDOW_MS)
252
+ }
253
+
254
+ async function review() {
255
+ const tab = options.tab()
256
+ const changes = staged.list(tab.id)
257
+ if (changes.length === 0 || saving.value) return
258
+ // Filled as rows land, whatever happens next: a failure, a retry, or
259
+ // the dialog closed, what was written is announced and undoable.
260
+ const saved: StagedChange[] = []
261
+ let settled = false
262
+ try {
263
+ await confirm({
264
+ title: t(
265
+ 'dms_database.data.save.title',
266
+ { table: tab.table },
267
+ changes.length,
268
+ ),
269
+ description: t(
270
+ 'dms_database.data.save.description',
271
+ { scope: scope(tab) },
272
+ changes.length,
273
+ ),
274
+ confirmLabel: t('dms_database.data.save.confirm', changes.length),
275
+ cancelLabel: t('dms_database.data.save.keep_editing'),
276
+ color: 'warning',
277
+ icon: 'i-ph-pencil-simple',
278
+ body: () => diffTable(changes),
279
+ onConfirm: async () => {
280
+ saving.value = true
281
+ try {
282
+ const outcome = await saveAll(tab)
283
+ saved.push(...changesOf(changes, outcome.written))
284
+ if (outcome.written.length + outcome.gone.length > 0) settled = true
285
+ return partialOutcome(outcome, changes, saved.length)
286
+ } finally {
287
+ saving.value = false
288
+ }
289
+ },
290
+ })
291
+ } finally {
292
+ if (settled) options.onSaved()
293
+ if (saved.length > 0) announce(tab, saved)
294
+ }
295
+ }
296
+
297
+ return { saving, review, notice, undo }
298
+ }
@@ -0,0 +1,72 @@
1
+ import { clonePositions, type DiagramState } from './history'
2
+
3
+ // When the diagram of one schema is written (D-2): a change waits for a short
4
+ // pause, then its differences go out through that schema's writer. The state
5
+ // is copied the moment a write is asked for, so the canvas loading another
6
+ // schema afterwards never reaches it; writes run one after the other.
7
+
8
+ export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'
9
+
10
+ /** What writes one schema's diagram: a DiagramSync. */
11
+ export interface DiagramWriter {
12
+ hasChanges(state: DiagramState): boolean
13
+ save(state: DiagramState): Promise<void>
14
+ }
15
+
16
+ function snapshotState(state: DiagramState): DiagramState {
17
+ return {
18
+ positions: clonePositions(state.positions),
19
+ notes: state.notes.map((note) => ({ ...note })),
20
+ }
21
+ }
22
+
23
+ export class DiagramAutosave {
24
+ private timer: ReturnType<typeof setTimeout> | null = null
25
+ private writes: Promise<void> = Promise.resolve()
26
+
27
+ constructor(
28
+ private readonly writer: DiagramWriter,
29
+ /** The state to write, read when the write is asked for. */
30
+ private readonly read: () => DiagramState,
31
+ private readonly delayMs: number,
32
+ private readonly onStatus: (status: SaveStatus) => void,
33
+ ) {}
34
+
35
+ /** Saves once edits pause. */
36
+ schedule() {
37
+ this.stopTimer()
38
+ this.timer = setTimeout(() => void this.flush(), this.delayMs)
39
+ }
40
+
41
+ /**
42
+ * Writes a scheduled save now, from a copy of the state taken at once.
43
+ * Resolves once every write asked for so far has ended.
44
+ */
45
+ flush(): Promise<void> {
46
+ if (this.timer === null) return this.writes
47
+ this.stopTimer()
48
+ const state = snapshotState(this.read())
49
+ this.writes = this.writes.then(() => this.write(state))
50
+ return this.writes
51
+ }
52
+
53
+ private stopTimer() {
54
+ if (this.timer !== null) clearTimeout(this.timer)
55
+ this.timer = null
56
+ }
57
+
58
+ private async write(state: DiagramState) {
59
+ // Nothing left to write, say after an undo: an earlier error is moot.
60
+ if (!this.writer.hasChanges(state)) {
61
+ this.onStatus('idle')
62
+ return
63
+ }
64
+ this.onStatus('saving')
65
+ try {
66
+ await this.writer.save(state)
67
+ this.onStatus('saved')
68
+ } catch {
69
+ this.onStatus('error')
70
+ }
71
+ }
72
+ }