oak-backend-base 3.3.2 → 3.3.4

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.
@@ -12,7 +12,7 @@ export declare class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt exten
12
12
  private aspectDict;
13
13
  private externalDependencies;
14
14
  protected dataSubscriber?: DataSubscriber<ED, Cxt>;
15
- protected synchronizer?: Synchronizer<ED, Cxt>;
15
+ protected synchronizers?: Synchronizer<ED, Cxt>[];
16
16
  protected contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>) => Promise<Cxt>;
17
17
  private requireSth;
18
18
  protected makeContext(cxtStr?: string, headers?: IncomingHttpHeaders): Promise<Cxt>;
package/lib/AppLoader.js CHANGED
@@ -21,7 +21,7 @@ class AppLoader extends types_1.AppLoader {
21
21
  aspectDict;
22
22
  externalDependencies;
23
23
  dataSubscriber;
24
- synchronizer;
24
+ synchronizers;
25
25
  contextBuilder;
26
26
  requireSth(filePath) {
27
27
  const depFilePath = (0, path_1.join)(this.path, filePath);
@@ -101,10 +101,10 @@ class AppLoader extends types_1.AppLoader {
101
101
  const dbConfigFile = (0, path_1.join)(this.path, 'configuration', 'mysql.json');
102
102
  const dbConfig = require(dbConfigFile);
103
103
  const syncConfigFile = (0, path_1.join)(this.path, 'lib', 'configuration', 'sync.js');
104
- const syncConfig = (0, fs_1.existsSync)(syncConfigFile) && require(syncConfigFile).default;
104
+ const syncConfigs = (0, fs_1.existsSync)(syncConfigFile) && require(syncConfigFile).default;
105
105
  return {
106
106
  dbConfig: dbConfig,
107
- syncConfig: syncConfig,
107
+ syncConfigs: syncConfigs,
108
108
  };
109
109
  }
110
110
  constructor(path, contextBuilder, ns, nsServer) {
@@ -149,70 +149,20 @@ class AppLoader extends types_1.AppLoader {
149
149
  adTriggers.forEach((trigger) => this.registerTrigger(trigger));
150
150
  checkers.forEach((checker) => this.dbStore.registerChecker(checker));
151
151
  adCheckers.forEach((checker) => this.dbStore.registerChecker(checker));
152
- if (this.synchronizer) {
152
+ if (this.synchronizers) {
153
153
  // 同步数据到远端结点通过commit trigger来完成
154
- const syncTriggers = this.synchronizer.getSyncTriggers();
155
- syncTriggers.forEach((trigger) => this.registerTrigger(trigger));
154
+ for (const synchronizer of this.synchronizers) {
155
+ const syncTriggers = synchronizer.getSyncTriggers();
156
+ syncTriggers.forEach((trigger) => this.registerTrigger(trigger));
157
+ }
156
158
  }
157
159
  }
158
160
  async mount(initialize) {
159
161
  const { path } = this;
160
162
  if (!initialize) {
161
- const { dbConfig, syncConfig } = this.getConfiguration();
162
- if (syncConfig) {
163
- const { self, remotes } = syncConfig;
164
- const { getSelfEncryptInfo, ...restSelf } = self;
165
- this.synchronizer = new Synchronizer_1.default({
166
- self: {
167
- // entity: self.entity,
168
- getSelfEncryptInfo: async () => {
169
- const context = await this.contextBuilder()(this.dbStore);
170
- await context.begin();
171
- try {
172
- const result = await self.getSelfEncryptInfo(context);
173
- await context.commit();
174
- return result;
175
- }
176
- catch (err) {
177
- await context.rollback();
178
- throw err;
179
- }
180
- },
181
- ...restSelf
182
- },
183
- remotes: remotes.map((r) => {
184
- const { getPushInfo, getPullInfo, ...rest } = r;
185
- return {
186
- getRemotePushInfo: async (id) => {
187
- const context = await this.contextBuilder()(this.dbStore);
188
- await context.begin();
189
- try {
190
- const result = await getPushInfo(id, context);
191
- await context.commit();
192
- return result;
193
- }
194
- catch (err) {
195
- await context.rollback();
196
- throw err;
197
- }
198
- },
199
- getRemotePullInfo: async (userId) => {
200
- const context = await this.contextBuilder()(this.dbStore);
201
- await context.begin();
202
- try {
203
- const result = await getPullInfo(userId, context);
204
- await context.commit();
205
- return result;
206
- }
207
- catch (err) {
208
- await context.rollback();
209
- throw err;
210
- }
211
- },
212
- ...rest,
213
- };
214
- })
215
- }, this.dbStore.getSchema());
163
+ const { syncConfigs } = this.getConfiguration();
164
+ if (syncConfigs) {
165
+ this.synchronizers = syncConfigs.map(config => new Synchronizer_1.default(config, this.dbStore.getSchema()));
216
166
  }
217
167
  this.initTriggers();
218
168
  }
@@ -325,9 +275,11 @@ class AppLoader extends types_1.AppLoader {
325
275
  transformEndpointItem(router, item);
326
276
  }
327
277
  }
328
- if (this.synchronizer) {
329
- const syncEp = this.synchronizer.getSelfEndpoint();
330
- transformEndpointItem(syncEp.name, syncEp);
278
+ if (this.synchronizers) {
279
+ this.synchronizers.forEach((synchronizer) => {
280
+ const syncEp = synchronizer.getSelfEndpoint();
281
+ transformEndpointItem(syncEp.name, syncEp);
282
+ });
331
283
  }
332
284
  return endPointRouters;
333
285
  }
@@ -476,6 +428,13 @@ class AppLoader extends types_1.AppLoader {
476
428
  }
477
429
  }
478
430
  }
431
+ if (this.synchronizers) {
432
+ this.synchronizers.forEach((synchronizer) => {
433
+ // 这个routine在内部处理异步
434
+ const routine = synchronizer.getSyncRoutine();
435
+ this.execWatcher(routine);
436
+ });
437
+ }
479
438
  }
480
439
  async execRoutine(routine) {
481
440
  const context = await this.makeContext();
@@ -1,21 +1,21 @@
1
- import { EntityDict, StorageSchema, EndpointItem } from 'oak-domain/lib/types';
1
+ import { EntityDict, StorageSchema, EndpointItem, SyncConfig, Watcher } from 'oak-domain/lib/types';
2
2
  import { VolatileTrigger } from 'oak-domain/lib/types/Trigger';
3
3
  import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
4
4
  import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
5
- import { SyncConfigWrapper } from './types/Sync';
6
5
  export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> {
7
6
  private config;
8
7
  private schema;
9
- private selfEncryptInfo?;
10
8
  private remotePullInfoMap;
11
9
  private pullMaxBornAtMap;
12
10
  private remotePushChannel;
11
+ private pushAccessMap;
13
12
  /**
14
13
  * 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
15
14
  * @param channel
16
15
  * @param retry
17
16
  */
18
- private pushOnChannel;
17
+ private startChannel;
18
+ private joinChannel;
19
19
  /**
20
20
  * 推向远端Node的oper,需要严格保证按产生的时间序推送。根据幂等原理,这里必须要推送成功
21
21
  * 因此在这里要实现两点:
@@ -25,14 +25,22 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
25
25
  * 其实这里还无法严格保证先产生的oper一定先到达被推送,因为volatile trigger是在事务提交后再发生的,但这种情况在目前应该跑不出来,在实际执行oper的时候assert掉先。by Xc 20240226
26
26
  */
27
27
  private pushOper;
28
- private getSelfEncryptInfo;
28
+ /**
29
+ * 因为应用可能是多租户,得提前确定context下的selfEncryptInfo
30
+ * 由于checkpoint时无法区别不同上下文之间的未完成oper数据,所以接口只能这样设计
31
+ * @param id
32
+ * @param context
33
+ * @param selfEncryptInfo
34
+ * @returns
35
+ */
36
+ private synchronizeOpersToRemote;
29
37
  private makeCreateOperTrigger;
30
- constructor(config: SyncConfigWrapper<ED, Cxt>, schema: StorageSchema<ED>);
38
+ constructor(config: SyncConfig<ED, Cxt>, schema: StorageSchema<ED>);
31
39
  /**
32
40
  * 根据sync的定义,生成对应的 commit triggers
33
41
  * @returns
34
42
  */
35
43
  getSyncTriggers(): VolatileTrigger<ED, keyof ED, Cxt>[];
36
- private checkOperationConsistent;
44
+ getSyncRoutine(): Watcher<ED, keyof ED, Cxt>;
37
45
  getSelfEndpoint(): EndpointItem<ED, Cxt>;
38
46
  }
@@ -1,55 +1,58 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
+ const types_1 = require("oak-domain/lib/types");
4
5
  const relationPath_1 = require("oak-domain/lib/utils/relationPath");
5
6
  const assert_1 = tslib_1.__importDefault(require("assert"));
6
7
  const path_1 = require("path");
7
8
  const lodash_1 = require("oak-domain/lib/utils/lodash");
8
9
  const filter_1 = require("oak-domain/lib/store/filter");
9
- const OAK_SYNC_HEADER_ITEM = 'oak-sync-remote-id';
10
+ const uuid_1 = require("oak-domain/lib/utils/uuid");
11
+ const OAK_SYNC_HEADER_ENTITY = 'oak-sync-entity';
12
+ const OAK_SYNC_HEADER_ENTITYID = 'oak-sync-entity-id';
10
13
  class Synchronizer {
11
14
  config;
12
15
  schema;
13
- selfEncryptInfo;
14
16
  remotePullInfoMap = {};
15
17
  pullMaxBornAtMap = {};
16
18
  remotePushChannel = {};
19
+ pushAccessMap = {};
17
20
  /**
18
21
  * 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
19
22
  * @param channel
20
23
  * @param retry
21
24
  */
22
- async pushOnChannel(channel, retry) {
23
- const { queue, api, nextPushTimestamp } = channel;
24
- (0, assert_1.default)(nextPushTimestamp);
25
- // 失败重试的间隔,失败次数多了应当适当延长,最多延长到1024秒
26
- let nextPushTimestamp2 = typeof retry === 'number' ? Math.pow(2, Math.min(retry, 10)) : 1;
27
- channel.nextPushTimestamp = nextPushTimestamp2 * 1000 + Date.now();
25
+ async startChannel(channel, retry) {
26
+ const { queue, api, selfEncryptInfo, entity, entityId } = channel;
27
+ channel.queue = [];
28
+ channel.running = true;
29
+ channel.nextPushTimestamp = Number.MAX_SAFE_INTEGER;
28
30
  const opers = queue.map(ele => ele.oper);
29
- let restOpers = [];
31
+ let failedOpers = [];
30
32
  let needRetry = false;
31
33
  let json;
32
34
  try {
33
35
  // todo 加密
34
- const selfEncryptInfo = await this.getSelfEncryptInfo();
35
36
  console.log('向远端结点sync数据', api, JSON.stringify(opers));
36
- const res = await fetch(api, {
37
+ const finalApi = (0, path_1.join)(api, selfEncryptInfo.id);
38
+ const res = await fetch(finalApi, {
37
39
  method: 'post',
38
40
  headers: {
39
41
  'Content-Type': 'application/json',
40
- [OAK_SYNC_HEADER_ITEM]: selfEncryptInfo.id,
42
+ [OAK_SYNC_HEADER_ENTITY]: entity,
43
+ [OAK_SYNC_HEADER_ENTITYID]: entityId,
41
44
  },
42
45
  body: JSON.stringify(opers),
43
46
  });
44
47
  if (res.status !== 200) {
45
- throw new Error(`sync数据时,访问api「${api}」的结果不是200。「${res.status}」`);
48
+ throw new Error(`sync数据时,访问api「${finalApi}」的结果不是200。「${res.status}」`);
46
49
  }
47
50
  json = await res.json();
48
51
  }
49
52
  catch (err) {
50
53
  console.error('sync push时出现error', err);
51
54
  needRetry = true;
52
- restOpers = queue;
55
+ failedOpers = queue;
53
56
  }
54
57
  if (!needRetry) {
55
58
  /**
@@ -63,22 +66,62 @@ class Synchronizer {
63
66
  }
64
67
  for (const req of queue) {
65
68
  if (successIds.includes(req.oper.id)) {
66
- req.resolve();
69
+ req.resolve(undefined);
67
70
  }
68
71
  else {
69
- restOpers.push(req);
72
+ failedOpers.push(req);
70
73
  }
71
74
  }
72
75
  }
73
- if (restOpers.length > 0) {
74
- const interval = Math.max(0, channel.nextPushTimestamp - Date.now());
75
- const retry2 = needRetry ? (typeof retry === 'number' ? retry + 1 : 1) : undefined;
76
- console.log('need retry', retry2);
77
- setTimeout(() => this.pushOnChannel(channel, retry2), interval);
76
+ channel.running = false;
77
+ channel.handler = undefined;
78
+ const retry2 = retry + 1;
79
+ console.log('need retry', retry2);
80
+ this.joinChannel(channel, failedOpers, retry2);
81
+ }
82
+ joinChannel(channel, opers, retry) {
83
+ // 要去重且有序
84
+ let idx = 0;
85
+ const now = Date.now();
86
+ opers.forEach((oper) => {
87
+ for (; idx < channel.queue.length; idx++) {
88
+ if (channel.queue[idx].oper.id === oper.oper.id) {
89
+ (0, assert_1.default)(false, '不应当出现重复的oper');
90
+ break;
91
+ }
92
+ else if (channel.queue[idx].oper.bornAt > oper.oper.bornAt) {
93
+ break;
94
+ }
95
+ }
96
+ channel.queue.splice(idx, 0, oper);
97
+ });
98
+ const retryWeight = Math.pow(2, Math.min(retry, 10));
99
+ const nextPushTimestamp = retryWeight * 1000 + now;
100
+ if (channel.queue.length > 0) {
101
+ if (channel.running) {
102
+ if (channel.nextPushTimestamp > nextPushTimestamp) {
103
+ channel.nextPushTimestamp = nextPushTimestamp;
104
+ }
105
+ }
106
+ else {
107
+ if (channel.nextPushTimestamp > nextPushTimestamp) {
108
+ channel.nextPushTimestamp = nextPushTimestamp;
109
+ if (channel.handler) {
110
+ clearTimeout(channel.handler);
111
+ }
112
+ channel.handler = setTimeout(async () => {
113
+ await this.startChannel(channel, retry);
114
+ }, nextPushTimestamp - now);
115
+ }
116
+ else {
117
+ // 当前队列的开始时间要早于自身要求,不用管
118
+ (0, assert_1.default)(channel.handler);
119
+ }
120
+ }
78
121
  }
79
122
  else {
80
123
  channel.handler = undefined;
81
- channel.nextPushTimestamp = undefined;
124
+ channel.nextPushTimestamp = Number.MAX_SAFE_INTEGER;
82
125
  }
83
126
  }
84
127
  // 将产生的oper推送到远端Node。注意要尽量在本地阻止重复推送
@@ -90,72 +133,149 @@ class Synchronizer {
90
133
  *
91
134
  * 其实这里还无法严格保证先产生的oper一定先到达被推送,因为volatile trigger是在事务提交后再发生的,但这种情况在目前应该跑不出来,在实际执行oper的时候assert掉先。by Xc 20240226
92
135
  */
93
- async pushOper(oper, userId, url, endpoint, nextPushTimestamp) {
136
+ async pushOper(oper, userId, url, endpoint, remoteEntity, remoteEntityId, selfEncryptInfo) {
94
137
  if (!this.remotePushChannel[userId]) {
138
+ // channel上缓存这些信息,暂不支持动态更新
95
139
  this.remotePushChannel[userId] = {
96
140
  api: (0, path_1.join)(url, 'endpoint', endpoint),
97
141
  queue: [],
142
+ entity: remoteEntity,
143
+ entityId: remoteEntityId,
144
+ nextPushTimestamp: Number.MAX_SAFE_INTEGER,
145
+ running: false,
146
+ selfEncryptInfo,
98
147
  };
99
148
  }
100
149
  const channel = this.remotePushChannel[userId];
101
- // 要去重且有序
102
- let existed = false;
103
- let idx = 0;
104
- for (; idx < channel.queue.length; idx++) {
105
- if (channel.queue[idx].oper.id === oper.id) {
106
- existed = true;
107
- break;
108
- }
109
- else if (channel.queue[idx].oper.bornAt > oper.bornAt) {
110
- break;
150
+ (0, assert_1.default)(channel.api === (0, path_1.join)(url, 'endpoint', endpoint));
151
+ (0, assert_1.default)(channel.entity === remoteEntity);
152
+ (0, assert_1.default)(channel.entityId === remoteEntityId);
153
+ const promise = new Promise((resolve) => {
154
+ this.joinChannel(channel, [{
155
+ oper,
156
+ resolve,
157
+ }], 0);
158
+ });
159
+ await promise;
160
+ }
161
+ /**
162
+ * 因为应用可能是多租户,得提前确定context下的selfEncryptInfo
163
+ * 由于checkpoint时无法区别不同上下文之间的未完成oper数据,所以接口只能这样设计
164
+ * @param id
165
+ * @param context
166
+ * @param selfEncryptInfo
167
+ * @returns
168
+ */
169
+ async synchronizeOpersToRemote(id, context, selfEncryptInfo) {
170
+ const [oper] = await context.select('oper', {
171
+ data: {
172
+ id: 1,
173
+ action: 1,
174
+ data: 1,
175
+ targetEntity: 1,
176
+ operatorId: 1,
177
+ operEntity$oper: {
178
+ $entity: 'operEntity',
179
+ data: {
180
+ id: 1,
181
+ entity: 1,
182
+ entityId: 1,
183
+ },
184
+ },
185
+ bornAt: 1,
186
+ $$createAt$$: 1,
187
+ filter: 1,
188
+ },
189
+ filter: {
190
+ id,
111
191
  }
112
- }
113
- if (!existed) {
114
- const now = Date.now();
115
- const nextPushTimestamp2 = nextPushTimestamp || now + 1000;
116
- const waiter = new Promise((resolve, reject) => {
117
- if (!existed) {
118
- channel.queue.splice(idx, 0, {
119
- oper,
120
- resolve,
121
- reject,
122
- });
192
+ }, { dontCollect: true, forUpdate: true });
193
+ const { operatorId, targetEntity, operEntity$oper: operEntities, action, data } = oper;
194
+ const entityIds = operEntities.map(ele => ele.entityId);
195
+ const pushEntityNodes = this.pushAccessMap[targetEntity];
196
+ if (pushEntityNodes && pushEntityNodes.length > 0) {
197
+ // 每个pushEntityNode代表配置的一个remoteEntity
198
+ await Promise.all(pushEntityNodes.map(async (node) => {
199
+ const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, endpoint, actions, onSynchronized } = node;
200
+ if (!actions || actions.includes(action)) {
201
+ const pushed = [];
202
+ const rows = await context.select(targetEntity, {
203
+ data: {
204
+ id: 1,
205
+ ...projection,
206
+ },
207
+ filter: {
208
+ id: {
209
+ $in: entityIds,
210
+ },
211
+ },
212
+ }, { dontCollect: true, includedDeleted: true });
213
+ // userId就是需要发送给远端的user,但是要将本次操作的user过滤掉(操作的原本产生者)
214
+ const userSendDict = groupByUsers(rows);
215
+ const pushToUserIdFn = async (userId) => {
216
+ const { entity, entityId, rowIds } = userSendDict[userId];
217
+ // 推送到远端结点的oper
218
+ const oper2 = {
219
+ id: oper.id,
220
+ action: action,
221
+ data: (action === 'create' && data instanceof Array) ? data.filter(ele => rowIds.includes(ele.id)) : data,
222
+ filter: {
223
+ id: rowIds.length === 1 ? rowIds[0] : {
224
+ $in: rowIds,
225
+ }
226
+ },
227
+ bornAt: oper.bornAt,
228
+ targetEntity,
229
+ };
230
+ const { url } = await getRemoteAccessInfo(context, {
231
+ userId,
232
+ remoteEntityId: entityId,
233
+ });
234
+ await this.pushOper(oper, userId, url, endpoint, entity, entityId, selfEncryptInfo);
235
+ };
236
+ for (const userId in userSendDict) {
237
+ if (userId !== operatorId) {
238
+ pushed.push(pushToUserIdFn(userId));
239
+ }
240
+ }
241
+ if (pushed.length > 0) {
242
+ // 对单个oper,这里必须要等所有的push返回,不然会一直等在上面
243
+ await Promise.all(pushed);
244
+ if (onSynchronized) {
245
+ await onSynchronized({
246
+ action: action,
247
+ data: data,
248
+ rowIds: entityIds,
249
+ }, context);
250
+ }
251
+ }
123
252
  }
124
- });
125
- if (!channel.handler) {
126
- channel.nextPushTimestamp = nextPushTimestamp2;
127
- channel.handler = setTimeout(async () => {
128
- await this.pushOnChannel(channel);
129
- }, nextPushTimestamp2 - now);
130
- }
131
- else if (channel.nextPushTimestamp && channel.nextPushTimestamp > nextPushTimestamp2) {
132
- // 当前队列的开始时间要晚于自身的要求,要求提前开始
133
- channel.nextPushTimestamp = nextPushTimestamp2;
134
- }
135
- await waiter;
136
- }
137
- else {
138
- // 感觉应该跑不出来
139
- console.warn('在sync数据时,遇到了重复推送的oper', JSON.stringify(oper), userId, url);
140
- }
141
- }
142
- async getSelfEncryptInfo() {
143
- if (this.selfEncryptInfo) {
144
- return this.selfEncryptInfo;
253
+ }));
254
+ // 到这里说明此oper成功,否则会在内部不停循环重试
255
+ // 主动去把oper上的跨事务标志清除,不依赖底层的triggerExecutor
256
+ await context.operate('oper', {
257
+ id: await (0, uuid_1.generateNewIdAsync)(),
258
+ action: 'update',
259
+ data: {
260
+ [types_1.TriggerDataAttribute]: null,
261
+ [types_1.TriggerUuidAttribute]: null,
262
+ },
263
+ filter: {
264
+ id
265
+ },
266
+ }, {});
145
267
  }
146
- this.selfEncryptInfo = await this.config.self.getSelfEncryptInfo();
147
- return this.selfEncryptInfo;
268
+ return 0;
148
269
  }
149
270
  makeCreateOperTrigger() {
150
271
  const { config } = this;
151
272
  const { remotes, self } = config;
152
273
  // 根据remotes定义,建立从entity到需要同步的远端结点信息的Map
153
- const pushAccessMap = {};
154
274
  remotes.forEach((remote) => {
155
- const { getRemotePushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote, entitySelf } = remote;
275
+ const { getPushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote } = remote;
156
276
  if (pushEntityDefs) {
157
277
  const pushEntities = [];
158
- const endpoint2 = (0, path_1.join)(endpoint || 'sync', entitySelf || self.entitySelf);
278
+ const endpoint2 = (0, path_1.join)(endpoint || 'sync', self.entity);
159
279
  for (const def of pushEntityDefs) {
160
280
  const { path, relationName, recursive, entity, actions, onSynchronized } = def;
161
281
  pushEntities.push(entity);
@@ -168,26 +288,32 @@ class Synchronizer {
168
288
  }, recursive) : (0, relationPath_1.destructDirectPath)(this.schema, entity, path2, recursive);
169
289
  const groupByUsers = (rows) => {
170
290
  const userRowDict = {};
171
- rows.filter((row) => {
172
- const userIds = getData(row)?.map(ele => ele.userId);
173
- if (userIds) {
174
- userIds.forEach((userId) => {
291
+ rows.forEach((row) => {
292
+ const goals = getData(row);
293
+ if (goals) {
294
+ goals.forEach(({ entity, entityId, userId }) => {
175
295
  if (userRowDict[userId]) {
176
- userRowDict[userId].push(row.id);
296
+ // 逻辑上来说同一个userId,其关联的entity和entityId必然相同,这个entity/entityId代表了对方
297
+ (0, assert_1.default)(userRowDict[userId].entity === entity && userRowDict[userId].entityId === entityId);
298
+ userRowDict[userId].rowIds.push(row.id);
177
299
  }
178
300
  else {
179
- userRowDict[userId] = [row.id];
301
+ userRowDict[userId] = {
302
+ entity,
303
+ entityId,
304
+ rowIds: [row.id],
305
+ };
180
306
  }
181
307
  });
182
308
  }
183
309
  });
184
310
  return userRowDict;
185
311
  };
186
- if (!pushAccessMap[entity]) {
187
- pushAccessMap[entity] = [{
312
+ if (!this.pushAccessMap[entity]) {
313
+ this.pushAccessMap[entity] = [{
188
314
  projection,
189
315
  groupByUsers,
190
- getRemotePushInfo,
316
+ getRemotePushInfo: getPushInfo,
191
317
  endpoint: endpoint2,
192
318
  entity,
193
319
  actions,
@@ -195,10 +321,10 @@ class Synchronizer {
195
321
  }];
196
322
  }
197
323
  else {
198
- pushAccessMap[entity].push({
324
+ this.pushAccessMap[entity].push({
199
325
  projection,
200
326
  groupByUsers,
201
- getRemotePushInfo,
327
+ getRemotePushInfo: getPushInfo,
202
328
  endpoint: endpoint2,
203
329
  entity,
204
330
  actions,
@@ -208,7 +334,7 @@ class Synchronizer {
208
334
  }
209
335
  }
210
336
  });
211
- const pushEntities = Object.keys(pushAccessMap);
337
+ const pushEntities = Object.keys(this.pushAccessMap);
212
338
  // push相关联的entity,在发生操作时,需要将operation推送到远端
213
339
  const createOperTrigger = {
214
340
  name: 'push oper to remote node',
@@ -222,88 +348,9 @@ class Synchronizer {
222
348
  },
223
349
  fn: async ({ ids }, context) => {
224
350
  (0, assert_1.default)(ids.length === 1);
225
- const [oper] = await context.select('oper', {
226
- data: {
227
- id: 1,
228
- action: 1,
229
- data: 1,
230
- targetEntity: 1,
231
- operatorId: 1,
232
- operEntity$oper: {
233
- $entity: 'operEntity',
234
- data: {
235
- id: 1,
236
- entity: 1,
237
- entityId: 1,
238
- },
239
- },
240
- bornAt: 1,
241
- $$createAt$$: 1,
242
- },
243
- filter: {
244
- id: ids[0],
245
- }
246
- }, { dontCollect: true, forUpdate: true });
247
- const { operatorId, targetEntity, operEntity$oper: operEntities, action, data } = oper;
248
- const entityIds = operEntities.map(ele => ele.entityId);
249
- const pushEntityNodes = pushAccessMap[targetEntity];
250
- if (pushEntityNodes && pushEntityNodes.length > 0) {
251
- // 每个pushEntityNode代表配置的一个remoteEntity
252
- await Promise.all(pushEntityNodes.map(async (node) => {
253
- const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, endpoint, entity, actions, onSynchronized } = node;
254
- if (!actions || actions.includes(action)) {
255
- const pushed = [];
256
- const rows = await context.select(targetEntity, {
257
- data: {
258
- id: 1,
259
- ...projection,
260
- },
261
- filter: {
262
- id: {
263
- $in: entityIds,
264
- },
265
- },
266
- }, { dontCollect: true, includedDeleted: true });
267
- // userId就是需要发送给远端的user,但是要将本次操作的user过滤掉(操作的原本产生者)
268
- const userSendDict = groupByUsers(rows);
269
- const pushToUserIdFn = async (userId) => {
270
- const rowIds = userSendDict[userId];
271
- // 推送到远端结点的oper
272
- const oper2 = {
273
- id: oper.id,
274
- action: action,
275
- data: (action === 'create' && data instanceof Array) ? data.filter(ele => rowIds.includes(ele.id)) : data,
276
- filter: {
277
- id: rowIds.length === 1 ? rowIds[0] : {
278
- $in: rowIds,
279
- }
280
- },
281
- bornAt: oper.bornAt,
282
- targetEntity,
283
- };
284
- const { url } = await getRemoteAccessInfo(userId);
285
- await this.pushOper(oper2 /** 这里不明白为什么TS过不去 */, userId, url, endpoint);
286
- };
287
- for (const userId in userSendDict) {
288
- if (userId !== operatorId) {
289
- pushed.push(pushToUserIdFn(userId));
290
- }
291
- }
292
- if (pushed.length > 0) {
293
- await Promise.all(pushed);
294
- if (onSynchronized) {
295
- await onSynchronized({
296
- action: action,
297
- data: data,
298
- rowIds: entityIds,
299
- }, context);
300
- }
301
- }
302
- }
303
- }));
304
- return entityIds.length * pushEntityNodes.length;
305
- }
306
- return 0;
351
+ const selfEncryptInfo = await this.config.self.getSelfEncryptInfo(context);
352
+ this.synchronizeOpersToRemote(ids[0], context, selfEncryptInfo);
353
+ throw new types_1.OakException('consistency on oper will be managed by myself');
307
354
  }
308
355
  };
309
356
  return createOperTrigger;
@@ -319,17 +366,40 @@ class Synchronizer {
319
366
  getSyncTriggers() {
320
367
  return [this.makeCreateOperTrigger()];
321
368
  }
322
- async checkOperationConsistent(entity, ids, bornAt) {
369
+ getSyncRoutine() {
370
+ return {
371
+ name: 'checkpoint routine for sync',
372
+ entity: 'oper',
373
+ filter: {
374
+ [types_1.TriggerDataAttribute]: {
375
+ $exists: true,
376
+ }
377
+ },
378
+ projection: {
379
+ id: 1,
380
+ [types_1.TriggerDataAttribute]: 1,
381
+ },
382
+ fn: async (context, data) => {
383
+ for (const ele of data) {
384
+ const { id, [types_1.TriggerDataAttribute]: triggerData } = ele;
385
+ const { cxtStr = '{}' } = triggerData;
386
+ await context.initialize(JSON.parse(cxtStr), true);
387
+ const selfEncryptInfo = await this.config.self.getSelfEncryptInfo(context);
388
+ this.synchronizeOpersToRemote(id, context, selfEncryptInfo);
389
+ }
390
+ return {};
391
+ }
392
+ };
323
393
  }
324
394
  getSelfEndpoint() {
325
395
  return {
326
396
  name: this.config.self.endpoint || 'sync',
327
397
  method: 'post',
328
- params: ['entity'],
398
+ params: ['entity', 'entityId'],
329
399
  fn: async (context, params, headers, req, body) => {
330
400
  // body中是传过来的oper数组信息
331
- const { entity } = params;
332
- const { [OAK_SYNC_HEADER_ITEM]: id } = headers;
401
+ const { entity, entityId } = params;
402
+ const { [OAK_SYNC_HEADER_ENTITY]: meEntity, [OAK_SYNC_HEADER_ENTITYID]: meEntityId } = headers;
333
403
  console.log('接收到来自远端的sync数据', entity, JSON.stringify(body));
334
404
  const successIds = [];
335
405
  let failed;
@@ -337,22 +407,29 @@ class Synchronizer {
337
407
  if (!this.remotePullInfoMap[entity]) {
338
408
  this.remotePullInfoMap[entity] = {};
339
409
  }
340
- if (!this.remotePullInfoMap[entity][id]) {
341
- const { getRemotePullInfo, pullEntities } = this.config.remotes.find(ele => ele.entity === entity);
410
+ if (!this.remotePullInfoMap[entity][entityId]) {
411
+ const { getPullInfo, pullEntities } = this.config.remotes.find(ele => ele.entity === entity);
342
412
  const pullEntityDict = {};
343
413
  if (pullEntities) {
344
414
  pullEntities.forEach((def) => pullEntityDict[def.entity] = def);
345
415
  }
346
- this.remotePullInfoMap[entity][id] = {
347
- pullInfo: await getRemotePullInfo(id),
416
+ this.remotePullInfoMap[entity][entityId] = {
417
+ pullInfo: await getPullInfo(context, {
418
+ selfId: meEntityId,
419
+ remoteEntityId: entityId,
420
+ }),
348
421
  pullEntityDict,
349
422
  };
350
423
  }
351
- const { pullInfo, pullEntityDict } = this.remotePullInfoMap[entity][id];
352
- const { userId, algorithm, publicKey } = pullInfo;
353
- // todo 解密
424
+ const { pullInfo, pullEntityDict } = this.remotePullInfoMap[entity][entityId];
425
+ const { userId, algorithm, publicKey, cxtInfo } = pullInfo;
354
426
  (0, assert_1.default)(userId);
355
- if (!this.pullMaxBornAtMap.hasOwnProperty(id)) {
427
+ context.setCurrentUserId(userId);
428
+ if (cxtInfo) {
429
+ await context.initialize(cxtInfo);
430
+ }
431
+ // todo 解密
432
+ if (!this.pullMaxBornAtMap.hasOwnProperty(entityId)) {
356
433
  const [maxHisOper] = await context.select('oper', {
357
434
  data: {
358
435
  id: 1,
@@ -372,10 +449,9 @@ class Synchronizer {
372
449
  indexFrom: 0,
373
450
  count: 1,
374
451
  }, { dontCollect: true });
375
- this.pullMaxBornAtMap[id] = maxHisOper?.bornAt || 0;
452
+ this.pullMaxBornAtMap[entityId] = maxHisOper?.bornAt || 0;
376
453
  }
377
- let maxBornAt = this.pullMaxBornAtMap[id];
378
- context.setCurrentUserId(userId);
454
+ let maxBornAt = this.pullMaxBornAtMap[entityId];
379
455
  const opers = body;
380
456
  const outdatedOpers = opers.filter(ele => ele.bornAt <= maxBornAt);
381
457
  const freshOpers = opers.filter(ele => ele.bornAt > maxBornAt);
@@ -431,6 +507,7 @@ class Synchronizer {
431
507
  maxBornAt = bornAt;
432
508
  }
433
509
  catch (err) {
510
+ console.error(err);
434
511
  console.error('sync时出错', entity, JSON.stringify(freshOper));
435
512
  failed = {
436
513
  id,
@@ -441,7 +518,7 @@ class Synchronizer {
441
518
  }
442
519
  })()
443
520
  ]);
444
- this.pullMaxBornAtMap[id] = maxBornAt;
521
+ this.pullMaxBornAtMap[entityId] = maxBornAt;
445
522
  return {
446
523
  successIds,
447
524
  failed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oak-backend-base",
3
- "version": "3.3.2",
3
+ "version": "3.3.4",
4
4
  "description": "oak-backend-base",
5
5
  "main": "lib/index",
6
6
  "author": {
@@ -22,8 +22,8 @@
22
22
  "node-schedule": "^2.1.0",
23
23
  "oak-common-aspect": "^2.2.5",
24
24
  "oak-db": "^3.1.0",
25
- "oak-domain": "^4.2.3",
26
- "oak-frontend-base": "^4.2.4",
25
+ "oak-domain": "^4.2.8",
26
+ "oak-frontend-base": "^4.2.7",
27
27
  "socket.io": "^4.7.2",
28
28
  "socket.io-client": "^4.7.2",
29
29
  "uuid": "^8.3.2"