@flowgram-vue/history-storage 0.2.0

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.
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import Dexie, { type Table } from 'dexie';
7
+
8
+ import { HistoryOperationRecord, HistoryRecord } from './types';
9
+
10
+ /**
11
+ * 历史数据库
12
+ */
13
+ export class HistoryDatabase extends Dexie {
14
+ readonly history: Table<HistoryRecord>;
15
+
16
+ readonly operation: Table<HistoryOperationRecord>;
17
+
18
+ resourceStorageLimit: number = 100;
19
+
20
+ constructor(databaseName: string = 'ide-history-storage') {
21
+ super(databaseName);
22
+ this.version(1).stores({
23
+ history: '++id, &uuid, resourceURI',
24
+ operation: '++id, &uuid, historyId, uri, resourceURI',
25
+ });
26
+ }
27
+
28
+ /**
29
+ * 某个uri下所有的history记录
30
+ * @param resourceURI 资源uri
31
+ * @returns
32
+ */
33
+ allHistoryByResourceURI(resourceURI: string) {
34
+ return this.history.where({ resourceURI }).toArray();
35
+ }
36
+
37
+ /**
38
+ * 根据uuid获取历史
39
+ * @param uuid
40
+ * @returns
41
+ */
42
+ getHistoryByUUID(uuid: string) {
43
+ return this.history.get({ uuid });
44
+ }
45
+
46
+ /**
47
+ * 某个uri下所有的operation记录
48
+ * @param resourceURI 资源uri
49
+ * @returns
50
+ */
51
+ allOperationByResourceURI(resourceURI: string) {
52
+ return this.operation.where({ resourceURI }).toArray();
53
+ }
54
+
55
+ /**
56
+ * 添加历史记录
57
+ * @param history 历史记录
58
+ * @param operations 操作记录
59
+ * @returns
60
+ */
61
+ addHistoryRecord(history: HistoryRecord, operations: HistoryOperationRecord[]) {
62
+ return this.transaction('rw', this.history, this.operation, async () => {
63
+ const count = await this.history.where({ resourceURI: history.resourceURI }).count();
64
+ if (count >= this.resourceStorageLimit) {
65
+ const limit = count - this.resourceStorageLimit;
66
+ const items = await this.history
67
+ .where({ resourceURI: history.resourceURI })
68
+ .limit(limit)
69
+ .toArray();
70
+ const ids = items.map(i => i.id);
71
+ const uuid = items.map(i => i.uuid);
72
+ await Promise.all([
73
+ this.history.bulkDelete(ids),
74
+ ...uuid.map(async uuid => {
75
+ await this.operation.where({ historyId: uuid }).delete();
76
+ }),
77
+ ]);
78
+ }
79
+
80
+ return Promise.all([this.history.add(history), this.operation.bulkAdd(operations)]);
81
+ });
82
+ }
83
+
84
+ /**
85
+ * 更新历史记录
86
+ * @param historyRecord
87
+ * @returns
88
+ */
89
+ async updateHistoryByUUID(uuid: string, historyRecord: Partial<HistoryRecord>) {
90
+ const history = await this.getHistoryByUUID(uuid);
91
+ if (!history) {
92
+ console.warn('no history record found');
93
+ return;
94
+ }
95
+ return this.history.update(history.id, historyRecord);
96
+ }
97
+
98
+ /**
99
+ * 添加操作记录
100
+ * @param record 操作记录
101
+ * @returns
102
+ */
103
+ addOperationRecord(record: HistoryOperationRecord) {
104
+ return this.operation.add(record);
105
+ }
106
+
107
+ /**
108
+ * 更新操作记录
109
+ * @param record 操作记录
110
+ * @returns
111
+ */
112
+ async updateOperationRecord(record: HistoryOperationRecord) {
113
+ const op = await this.operation.where({ uuid: record.uuid }).first();
114
+ if (!op) {
115
+ console.warn('no operation record found');
116
+ return;
117
+ }
118
+ return this.operation.put({
119
+ id: op.id,
120
+ ...record,
121
+ });
122
+ }
123
+
124
+ /**
125
+ * 重置数据库
126
+ * @returns
127
+ */
128
+ reset() {
129
+ return this.transaction('rw', this.history, this.operation, async () => {
130
+ await Promise.all(this.tables.map(table => table.clear()));
131
+ });
132
+ }
133
+
134
+ /**
135
+ * 清空某个资源下所有的数据
136
+ * @param resourceURI
137
+ * @returns
138
+ */
139
+ resetByResourceURI(resourceURI: string) {
140
+ return this.transaction('rw', this.history, this.operation, async () => {
141
+ await Promise.all(this.tables.map(table => table.where({ resourceURI }).delete()));
142
+ });
143
+ }
144
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { ContainerModule } from 'inversify';
7
+
8
+ import { HistoryStorageManager } from './history-storage-manager';
9
+
10
+ export const HistoryStorageContainerModule = new ContainerModule(bind => {
11
+ bind(HistoryStorageManager).toSelf().inSingletonScope();
12
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { inject, injectable } from 'inversify';
7
+ import { DisposableCollection } from '@flowgram-vue/utils';
8
+ import {
9
+ HistoryItem,
10
+ HistoryManager,
11
+ HistoryOperation,
12
+ HistoryStackChangeType,
13
+ HistoryService,
14
+ HistoryStackAddOperationEvent,
15
+ HistoryStackUpdateOperationEvent,
16
+ } from '@flowgram-vue/history';
17
+ import { PluginContext } from '@flowgram-vue/core';
18
+
19
+ import { HistoryOperationRecord, HistoryRecord, HistoryStoragePluginOptions } from './types';
20
+ import { HistoryDatabase } from './history-database';
21
+
22
+ /**
23
+ * 历史存储管理
24
+ */
25
+ @injectable()
26
+ export class HistoryStorageManager {
27
+ private _toDispose = new DisposableCollection();
28
+
29
+ db: HistoryDatabase;
30
+
31
+ @inject(HistoryManager)
32
+ protected historyManager: HistoryManager;
33
+
34
+ /**
35
+ * 初始化
36
+ * @param ctx
37
+ */
38
+ onInit(_ctx: PluginContext, opts: HistoryStoragePluginOptions) {
39
+ this.db = new HistoryDatabase(opts?.databaseName);
40
+
41
+ if (opts?.resourceStorageLimit) {
42
+ this.db.resourceStorageLimit = opts.resourceStorageLimit;
43
+ }
44
+
45
+ this._toDispose.push(
46
+ this.historyManager.historyStack.onChange(event => {
47
+ if (event.type === HistoryStackChangeType.ADD) {
48
+ const [history, operations] = this.historyItemToRecord(event.service, event.value);
49
+ this.db.addHistoryRecord(history, operations).catch(console.error);
50
+ }
51
+
52
+ // operation merge的时候需要更新snapshot
53
+ if (
54
+ [HistoryStackChangeType.ADD_OPERATION, HistoryStackChangeType.UPDATE_OPERATION].includes(
55
+ event.type,
56
+ )
57
+ ) {
58
+ const {
59
+ service,
60
+ value: { historyItem },
61
+ } = event as HistoryStackAddOperationEvent | HistoryStackUpdateOperationEvent;
62
+ // 更新快照
63
+ this.db
64
+ .updateHistoryByUUID(historyItem.id, {
65
+ resourceJSON: service.getSnapshot() || '',
66
+ })
67
+ .catch(console.error);
68
+ }
69
+
70
+ if (event.type === HistoryStackChangeType.ADD_OPERATION) {
71
+ const operationRecord: HistoryOperationRecord = this.historyOperationToRecord(
72
+ event.value.historyItem,
73
+ event.value.operation,
74
+ );
75
+ this.db.addOperationRecord(operationRecord).catch(console.error);
76
+ }
77
+ if (event.type === HistoryStackChangeType.UPDATE_OPERATION) {
78
+ const operationRecord: HistoryOperationRecord = this.historyOperationToRecord(
79
+ event.value.historyItem,
80
+ event.value.operation,
81
+ );
82
+ this.db.updateOperationRecord(operationRecord).catch(console.error);
83
+ }
84
+ }),
85
+ );
86
+ }
87
+
88
+ /**
89
+ * 内存历史转数据表记录
90
+ * @param historyItem
91
+ * @returns
92
+ */
93
+ historyItemToRecord(
94
+ historyService: HistoryService,
95
+ historyItem: HistoryItem,
96
+ ): [HistoryRecord, HistoryOperationRecord[]] {
97
+ const operations = historyItem.operations.map(op =>
98
+ this.historyOperationToRecord(historyItem, op),
99
+ );
100
+
101
+ return [
102
+ {
103
+ uuid: historyItem.id,
104
+ timestamp: historyItem.timestamp,
105
+ type: historyItem.type,
106
+ resourceURI: historyItem.uri?.toString() || '',
107
+ resourceJSON: historyService.getSnapshot() || '',
108
+ },
109
+ operations,
110
+ ];
111
+ }
112
+
113
+ /**
114
+ * 内存操作转数据表操作
115
+ * @param historyItem
116
+ * @param op
117
+ * @returns
118
+ */
119
+ historyOperationToRecord(historyItem: HistoryItem, op: HistoryOperation): HistoryOperationRecord {
120
+ return {
121
+ uuid: op.id,
122
+ type: op.type,
123
+ timestamp: op.timestamp,
124
+ label: op.label || '',
125
+ uri: op?.uri?.toString() || '',
126
+ resourceURI: historyItem.uri?.toString() || '',
127
+ description: op.description || '',
128
+ value: JSON.stringify(op.value),
129
+ historyId: historyItem.id,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * 销毁
135
+ */
136
+ dispose() {
137
+ this._toDispose.dispose();
138
+ }
139
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './create-history-storage-plugin';
7
+ export * from './use-storage-hisotry-items';
8
+ export * from './types';
9
+ export * from './history-database';
10
+ export * from './history-storage-container-module';
11
+ export * from './history-storage-manager';
package/src/types.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export interface HistoryRecord {
7
+ /**
8
+ * 自增id
9
+ */
10
+ id?: number;
11
+ /**
12
+ * 唯一标识
13
+ */
14
+ uuid: string;
15
+ /**
16
+ * 类型 如 push undo redo
17
+ */
18
+ type: string;
19
+ /**
20
+ * 时间戳
21
+ */
22
+ timestamp: number;
23
+ /**
24
+ * 资源uri
25
+ */
26
+ resourceURI: string;
27
+ /**
28
+ * 资源json
29
+ */
30
+ resourceJSON: unknown;
31
+ }
32
+
33
+ export interface HistoryOperationRecord {
34
+ /**
35
+ * 自增id
36
+ */
37
+ id?: number;
38
+ /**
39
+ * 唯一标识
40
+ */
41
+ uuid: string;
42
+ /**
43
+ * 历史记录唯一标志,记录的uuid
44
+ */
45
+ historyId: string;
46
+ /**
47
+ * 类型,如 addFromNode deleteFromNode
48
+ */
49
+ type: string;
50
+ /**
51
+ * 操作值,不同类型不同,json字符串
52
+ */
53
+ value: string;
54
+ /**
55
+ * uri操作对象uri,如某个node的uri
56
+ */
57
+ uri: string;
58
+ /**
59
+ * 操作资源uri,如某个流程的uri
60
+ */
61
+ resourceURI: string;
62
+ /**
63
+ * 操作显示标题
64
+ */
65
+ label: string;
66
+ /**
67
+ * 操作显示描述
68
+ */
69
+ description: string;
70
+ /**
71
+ * 时间戳
72
+ */
73
+ timestamp: number;
74
+ }
75
+
76
+ /**
77
+ * 插件配置
78
+ */
79
+ export interface HistoryStoragePluginOptions {
80
+ /**
81
+ * 数据库名称
82
+ */
83
+ databaseName?: string;
84
+ /**
85
+ * 每个资源最大历史记录数量
86
+ */
87
+ resourceStorageLimit?: number;
88
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { liveQuery } from 'dexie';
7
+ import { groupBy } from 'lodash-es';
8
+ import { reactive, toValue, watchEffect } from 'vue';
9
+ import type { MaybeRefOrGetter } from 'vue';
10
+ import { HistoryItem, HistoryOperation, HistoryStack } from '@flowgram-vue/history';
11
+
12
+ import { HistoryStorageManager } from './history-storage-manager';
13
+
14
+ export function useStorageHistoryItems(
15
+ historyStorageManager: HistoryStorageManager,
16
+ resourceURI: MaybeRefOrGetter<string>
17
+ ): {
18
+ items: HistoryItem[];
19
+ } {
20
+ const state = reactive<{ items: HistoryItem[] }>({
21
+ items: [],
22
+ });
23
+
24
+ watchEffect((onCleanup) => {
25
+ const uri = toValue(resourceURI);
26
+ const observable = liveQuery(async () => {
27
+ const [historyItems, operations] = await Promise.all([
28
+ historyStorageManager.db.allHistoryByResourceURI(uri),
29
+ historyStorageManager.db.allOperationByResourceURI(uri),
30
+ ]);
31
+
32
+ const grouped = groupBy<HistoryOperation>(
33
+ operations.map((o) => ({
34
+ id: o.uuid,
35
+ timestamp: o.timestamp,
36
+ type: o.type,
37
+ label: o.label,
38
+ description: o.description,
39
+ value: o.value ? JSON.parse(o.value) : undefined,
40
+ uri: o.uri,
41
+ historyId: o.historyId,
42
+ })),
43
+ 'historyId'
44
+ );
45
+ return historyItems
46
+ .sort((a, b) => (b.id as number) - (a.id as number))
47
+ .map(
48
+ (historyItem) =>
49
+ ({
50
+ id: historyItem.uuid,
51
+ type: historyItem.type,
52
+ timestamp: historyItem.timestamp,
53
+ operations: grouped[historyItem.uuid] || [],
54
+ time: HistoryStack.dateFormat(historyItem.timestamp),
55
+ uri: historyItem.resourceURI,
56
+ }) as HistoryItem
57
+ );
58
+ });
59
+
60
+ const subscription = observable.subscribe({
61
+ next: (value) => {
62
+ state.items = value || [];
63
+ },
64
+ error: (err) => {
65
+ console.error(err);
66
+ },
67
+ });
68
+
69
+ onCleanup(() => subscription.unsubscribe());
70
+ });
71
+
72
+ return state;
73
+ }