oak-backend-base 3.2.1 → 3.2.2

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.
@@ -1,24 +1,25 @@
1
1
  /// <reference types="node" />
2
2
  import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
3
- import { AppLoader as GeneralAppLoader, EntityDict, OpRecord } from "oak-domain/lib/types";
3
+ import { AppLoader as GeneralAppLoader, Trigger, EntityDict, Watcher, OpRecord } from "oak-domain/lib/types";
4
4
  import { DbStore } from "./DbStore";
5
5
  import { BackendRuntimeContext } from 'oak-frontend-base';
6
6
  import { IncomingHttpHeaders, IncomingMessage } from 'http';
7
7
  import { Namespace } from 'socket.io';
8
- import { ClusterInfo } from 'oak-domain/lib/types/Cluster';
8
+ import DataSubscriber from './cluster/DataSubscriber';
9
9
  export declare class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> extends GeneralAppLoader<ED, Cxt> {
10
- private dbStore;
10
+ protected dbStore: DbStore<ED, Cxt>;
11
11
  private aspectDict;
12
12
  private externalDependencies;
13
- private dataSubscriber?;
14
- private contextBuilder;
13
+ protected dataSubscriber?: DataSubscriber<ED, Cxt>;
14
+ protected contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>) => Promise<Cxt>;
15
15
  private requireSth;
16
- constructor(path: string, contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>, header?: IncomingHttpHeaders, clusterInfo?: ClusterInfo) => Promise<Cxt>, ns?: Namespace);
16
+ protected makeContext(cxtStr?: string, headers?: IncomingHttpHeaders): Promise<Cxt>;
17
+ constructor(path: string, contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>) => Promise<Cxt>, ns?: Namespace, nsServer?: Namespace);
18
+ protected registerTrigger(trigger: Trigger<ED, keyof ED, Cxt>): void;
17
19
  initTriggers(): void;
18
- startWatchers(): void;
19
20
  mount(initialize?: true): Promise<void>;
20
21
  unmount(): Promise<void>;
21
- execAspect(name: string, header?: IncomingHttpHeaders, contextString?: string, params?: any): Promise<{
22
+ execAspect(name: string, headers?: IncomingHttpHeaders, contextString?: string, params?: any): Promise<{
22
23
  opRecords: OpRecord<ED>[];
23
24
  result: any;
24
25
  message?: string;
@@ -26,6 +27,10 @@ export declare class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt exten
26
27
  initialize(dropIfExists?: boolean): Promise<void>;
27
28
  getStore(): DbStore<ED, Cxt>;
28
29
  getEndpoints(prefix: string): [string, "get" | "post" | "put" | "delete", string, (params: Record<string, string>, headers: IncomingHttpHeaders, req: IncomingMessage, body?: any) => Promise<any>][];
30
+ protected operateInWatcher<T extends keyof ED>(entity: T, operation: ED[T]['Update'], context: Cxt): Promise<import("oak-domain/lib/types").OperationResult<ED>>;
31
+ protected selectInWatcher<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt): Promise<Partial<ED[T]["Schema"]>[]>;
32
+ protected execWatcher(watcher: Watcher<ED, keyof ED, Cxt>): Promise<void>;
33
+ startWatchers(): void;
29
34
  startTimers(): void;
30
35
  execStartRoutines(): Promise<void>;
31
36
  execRoutine(routine: (context: Cxt) => Promise<void>): Promise<void>;
package/lib/AppLoader.js CHANGED
@@ -86,23 +86,36 @@ class AppLoader extends types_1.AppLoader {
86
86
  Object.assign(sthOut, sth);
87
87
  return sthOut;
88
88
  }
89
- constructor(path, contextBuilder, ns) {
89
+ async makeContext(cxtStr, headers) {
90
+ const context = await this.contextBuilder(cxtStr)(this.dbStore);
91
+ context.clusterInfo = (0, env_2.getClusterInfo)();
92
+ context.headers = headers;
93
+ return context;
94
+ }
95
+ constructor(path, contextBuilder, ns, nsServer) {
90
96
  super(path);
91
97
  const dbConfig = require((0, path_1.join)(path, '/configuration/mysql.json'));
92
98
  const { storageSchema } = require(`${path}/lib/oak-app-domain/Storage`);
93
99
  const { authDeduceRelationMap, selectFreeEntities, updateFreeDict } = require(`${path}/lib/config/relation`);
94
100
  this.externalDependencies = require((0, env_1.OAK_EXTERNAL_LIBS_FILEPATH)((0, path_1.join)(path, 'lib')));
95
101
  this.aspectDict = Object.assign({}, index_1.default, this.requireSth('lib/aspects/index'));
96
- this.dbStore = new DbStore_1.DbStore(storageSchema, contextBuilder, dbConfig, authDeduceRelationMap, selectFreeEntities, updateFreeDict);
102
+ this.dbStore = new DbStore_1.DbStore(storageSchema, (cxtStr) => this.makeContext(cxtStr), dbConfig, authDeduceRelationMap, selectFreeEntities, updateFreeDict);
97
103
  if (ns) {
98
- this.dataSubscriber = new DataSubscriber_1.default(ns, (scene) => this.contextBuilder(scene)(this.dbStore));
99
- this.contextBuilder = (scene) => async (store, header, clusterInfo) => {
100
- const context = await contextBuilder(scene)(store, header, clusterInfo);
104
+ (0, assert_1.default)(nsServer);
105
+ this.dataSubscriber = new DataSubscriber_1.default(ns, nsServer, (scene) => this.contextBuilder(scene)(this.dbStore));
106
+ this.contextBuilder = (scene) => async (store) => {
107
+ const context = await contextBuilder(scene)(store);
101
108
  // 注入在提交前向dataSubscribe
102
109
  const originCommit = context.commit;
103
110
  context.commit = async () => {
104
- this.dataSubscriber.onDataCommited(context);
111
+ const { eventOperationMap, opRecords } = context;
105
112
  await originCommit.call(context);
113
+ Object.keys(eventOperationMap).forEach((event) => {
114
+ const ids = eventOperationMap[event];
115
+ const opRecordsToPublish = opRecords.filter((ele) => !!ele.id && ids.includes(ele.id));
116
+ (0, assert_1.default)(opRecordsToPublish.length === ids.length, '要推送的事件的operation数量不足,请检查确保');
117
+ this.dataSubscriber.publishEvent(event, opRecordsToPublish, context.getSubscriberId());
118
+ });
106
119
  };
107
120
  return context;
108
121
  };
@@ -111,79 +124,19 @@ class AppLoader extends types_1.AppLoader {
111
124
  this.contextBuilder = contextBuilder;
112
125
  }
113
126
  }
127
+ registerTrigger(trigger) {
128
+ this.dbStore.registerTrigger(trigger);
129
+ }
114
130
  initTriggers() {
115
131
  const triggers = this.requireSth('lib/triggers/index');
116
132
  const checkers = this.requireSth('lib/checkers/index');
117
133
  const { ActionDefDict } = require(`${this.path}/lib/oak-app-domain/ActionDefDict`);
118
134
  const { triggers: adTriggers, checkers: adCheckers } = (0, actionDef_1.makeIntrinsicCTWs)(this.dbStore.getSchema(), ActionDefDict);
119
- triggers.forEach((trigger) => this.dbStore.registerTrigger(trigger));
120
- adTriggers.forEach((trigger) => this.dbStore.registerTrigger(trigger));
135
+ triggers.forEach((trigger) => this.registerTrigger(trigger));
136
+ adTriggers.forEach((trigger) => this.registerTrigger(trigger));
121
137
  checkers.forEach((checker) => this.dbStore.registerChecker(checker));
122
138
  adCheckers.forEach((checker) => this.dbStore.registerChecker(checker));
123
139
  }
124
- startWatchers() {
125
- const watchers = this.requireSth('lib/watchers/index');
126
- const { ActionDefDict } = require(`${this.path}/lib/oak-app-domain/ActionDefDict`);
127
- const { watchers: adWatchers } = (0, actionDef_1.makeIntrinsicCTWs)(this.dbStore.getSchema(), ActionDefDict);
128
- const totalWatchers = watchers.concat(adWatchers);
129
- let count = 0;
130
- const doWatchers = async () => {
131
- count++;
132
- const start = Date.now();
133
- const context = await this.contextBuilder()(this.dbStore, undefined, (0, env_2.getClusterInfo)());
134
- for (const w of totalWatchers) {
135
- await context.begin();
136
- try {
137
- if (w.hasOwnProperty('actionData')) {
138
- const { entity, action, filter, actionData } = w;
139
- const filter2 = typeof filter === 'function' ? await filter() : filter;
140
- const data = typeof actionData === 'function' ? await actionData() : actionData; // 这里有个奇怪的编译错误,不理解 by Xc
141
- const result = await this.dbStore.operate(entity, {
142
- id: await (0, uuid_1.generateNewIdAsync)(),
143
- action,
144
- data,
145
- filter: filter2
146
- }, context, {
147
- dontCollect: true,
148
- });
149
- console.log(`执行了watcher【${w.name}】,结果是:`, result);
150
- }
151
- else {
152
- const { entity, projection, fn, filter } = w;
153
- const filter2 = typeof filter === 'function' ? await filter() : filter;
154
- const projection2 = typeof projection === 'function' ? await projection() : projection;
155
- const rows = await this.dbStore.select(entity, {
156
- data: projection2,
157
- filter: filter2,
158
- }, context, {
159
- dontCollect: true,
160
- blockTrigger: true,
161
- });
162
- if (rows.length > 0) {
163
- const result = await fn(context, rows);
164
- console.log(`执行了watcher【${w.name}】,结果是:`, result);
165
- }
166
- }
167
- await context.commit();
168
- }
169
- catch (err) {
170
- await context.rollback();
171
- console.error(`执行了watcher【${w.name}】,发生错误:`, err);
172
- }
173
- }
174
- const duration = Date.now() - start;
175
- console.log(`第${count}次执行watchers,共执行${watchers.length}个,耗时${duration}毫秒`);
176
- const now = Date.now();
177
- try {
178
- await this.dbStore.checkpoint(process.env.NODE_ENV === 'development' ? now - 30 * 1000 : now - 120 * 1000);
179
- }
180
- catch (err) {
181
- console.error(`执行了checkpoint,发生错误:`, err);
182
- }
183
- setTimeout(() => doWatchers(), 120000);
184
- };
185
- doWatchers();
186
- }
187
140
  async mount(initialize) {
188
141
  const { path } = this;
189
142
  if (!initialize) {
@@ -197,8 +150,8 @@ class AppLoader extends types_1.AppLoader {
197
150
  (0, index_1.clearPorts)();
198
151
  this.dbStore.disconnect();
199
152
  }
200
- async execAspect(name, header, contextString, params) {
201
- const context = await this.contextBuilder(contextString)(this.dbStore, header, (0, env_2.getClusterInfo)());
153
+ async execAspect(name, headers, contextString, params) {
154
+ const context = await this.makeContext(contextString, headers);
202
155
  const fn = this.aspectDict[name];
203
156
  if (!fn) {
204
157
  throw new Error(`不存在的接口名称: ${name}`);
@@ -209,7 +162,7 @@ class AppLoader extends types_1.AppLoader {
209
162
  await context.commit();
210
163
  await context.refineOpRecords();
211
164
  return {
212
- opRecords: context.opRecords,
165
+ opRecords: context.opRecords.map(ele => (0, lodash_1.omit)(ele, 'id')),
213
166
  message: context.getMessage(),
214
167
  result,
215
168
  };
@@ -272,7 +225,7 @@ class AppLoader extends types_1.AppLoader {
272
225
  }
273
226
  }
274
227
  endPointRouters.push([name, method, url, async (params, headers, req, body) => {
275
- const context = await this.contextBuilder()(this.dbStore, headers, (0, env_2.getClusterInfo)());
228
+ const context = await this.makeContext(undefined, headers);
276
229
  await context.begin();
277
230
  try {
278
231
  const result = await fn(context, params, headers, req, body);
@@ -297,18 +250,96 @@ class AppLoader extends types_1.AppLoader {
297
250
  }
298
251
  return endPointRouters;
299
252
  }
253
+ operateInWatcher(entity, operation, context) {
254
+ return this.dbStore.operate(entity, operation, context, {
255
+ dontCollect: true,
256
+ });
257
+ }
258
+ selectInWatcher(entity, selection, context) {
259
+ return this.dbStore.select(entity, selection, context, {
260
+ dontCollect: true,
261
+ blockTrigger: true,
262
+ });
263
+ }
264
+ async execWatcher(watcher) {
265
+ const context = await this.makeContext();
266
+ await context.begin();
267
+ try {
268
+ if (watcher.hasOwnProperty('actionData')) {
269
+ const { entity, action, filter, actionData } = watcher;
270
+ const filter2 = typeof filter === 'function' ? await filter() : filter;
271
+ const data = typeof actionData === 'function' ? await (actionData)() : actionData;
272
+ const result = await this.operateInWatcher(entity, {
273
+ id: await (0, uuid_1.generateNewIdAsync)(),
274
+ action,
275
+ data,
276
+ filter: filter2
277
+ }, context);
278
+ console.log(`执行了watcher【${watcher.name}】,结果是:`, result);
279
+ }
280
+ else {
281
+ const { entity, projection, fn, filter } = watcher;
282
+ const filter2 = typeof filter === 'function' ? await filter() : filter;
283
+ const projection2 = typeof projection === 'function' ? await projection() : projection;
284
+ const rows = await this.selectInWatcher(entity, {
285
+ data: projection2,
286
+ filter: filter2,
287
+ }, context);
288
+ if (rows.length > 0) {
289
+ const result = await fn(context, rows);
290
+ console.log(`执行了watcher【${watcher.name}】,结果是:`, result);
291
+ }
292
+ }
293
+ await context.commit();
294
+ }
295
+ catch (err) {
296
+ await context.rollback();
297
+ console.error(`执行了watcher【${watcher.name}】,发生错误:`, err);
298
+ }
299
+ }
300
+ startWatchers() {
301
+ const watchers = this.requireSth('lib/watchers/index');
302
+ const { ActionDefDict } = require(`${this.path}/lib/oak-app-domain/ActionDefDict`);
303
+ const { watchers: adWatchers } = (0, actionDef_1.makeIntrinsicCTWs)(this.dbStore.getSchema(), ActionDefDict);
304
+ const totalWatchers = watchers.concat(adWatchers);
305
+ let count = 0;
306
+ const doWatchers = async () => {
307
+ count++;
308
+ const start = Date.now();
309
+ for (const w of totalWatchers) {
310
+ await this.execWatcher(w);
311
+ }
312
+ const duration = Date.now() - start;
313
+ console.log(`第${count}次执行watchers,共执行${watchers.length}个,耗时${duration}毫秒`);
314
+ const now = Date.now();
315
+ try {
316
+ await this.dbStore.checkpoint(process.env.NODE_ENV === 'development' ? now - 30 * 1000 : now - 120 * 1000);
317
+ }
318
+ catch (err) {
319
+ console.error(`执行了checkpoint,发生错误:`, err);
320
+ }
321
+ setTimeout(() => doWatchers(), 120000);
322
+ };
323
+ doWatchers();
324
+ }
300
325
  startTimers() {
301
326
  const timers = this.requireSth('lib/timers/index');
302
327
  for (const timer of timers) {
303
- const { cron, fn, name } = timer;
328
+ const { cron, name } = timer;
304
329
  (0, node_schedule_1.scheduleJob)(name, cron, async (date) => {
305
330
  const start = Date.now();
306
- const context = await this.contextBuilder()(this.dbStore, undefined, (0, env_2.getClusterInfo)());
331
+ const context = await this.makeContext();
307
332
  await context.begin();
308
333
  console.log(`定时器【${name}】开始执行,时间是【${date.toLocaleTimeString()}】`);
309
334
  try {
310
- const result = await fn(context);
311
- console.log(`定时器【${name}】执行完成,耗时${Date.now() - start}毫秒,结果是【${result}】`);
335
+ if (timer.hasOwnProperty('entity')) {
336
+ await this.execWatcher(timer);
337
+ }
338
+ else {
339
+ const { timer: timerFn } = timer;
340
+ const result = await timerFn(context);
341
+ console.log(`定时器【${name}】执行完成,耗时${Date.now() - start}毫秒,结果是【${result}】`);
342
+ }
312
343
  await context.commit();
313
344
  }
314
345
  catch (err) {
@@ -321,23 +352,28 @@ class AppLoader extends types_1.AppLoader {
321
352
  async execStartRoutines() {
322
353
  const routines = this.requireSth('lib/routines/start');
323
354
  for (const routine of routines) {
324
- const { name, fn } = routine;
325
- const context = await this.contextBuilder()(this.dbStore, undefined, (0, env_2.getClusterInfo)());
326
- const start = Date.now();
327
- await context.begin();
328
- try {
329
- const result = await fn(context);
330
- console.log(`例程【${name}】执行完成,耗时${Date.now() - start}毫秒,结果是【${result}】`);
331
- await context.commit();
355
+ if (routine.hasOwnProperty('entity')) {
356
+ this.execWatcher(routine);
332
357
  }
333
- catch (err) {
334
- console.warn(`例程【${name}】执行失败,耗时${Date.now() - start}毫秒,错误是`, err);
335
- await context.rollback();
358
+ else {
359
+ const { name, routine: routineFn } = routine;
360
+ const context = await this.makeContext();
361
+ const start = Date.now();
362
+ await context.begin();
363
+ try {
364
+ const result = await routineFn(context);
365
+ console.log(`例程【${name}】执行完成,耗时${Date.now() - start}毫秒,结果是【${result}】`);
366
+ await context.commit();
367
+ }
368
+ catch (err) {
369
+ console.warn(`例程【${name}】执行失败,耗时${Date.now() - start}毫秒,错误是`, err);
370
+ await context.rollback();
371
+ }
336
372
  }
337
373
  }
338
374
  }
339
375
  async execRoutine(routine) {
340
- const context = await this.contextBuilder()(this.dbStore, undefined, (0, env_2.getClusterInfo)());
376
+ const context = await this.makeContext();
341
377
  await routine(context);
342
378
  }
343
379
  }
@@ -0,0 +1,17 @@
1
+ import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
2
+ import { EntityDict, OperationResult, Trigger } from 'oak-domain/lib/types';
3
+ import { BackendRuntimeContext } from 'oak-frontend-base';
4
+ import { AppLoader } from './AppLoader';
5
+ import { DbStore } from './DbStore';
6
+ import { Namespace } from 'socket.io';
7
+ import { Socket } from 'socket.io-client';
8
+ export declare class ClusterAppLoader<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> extends AppLoader<ED, Cxt> {
9
+ protected socket: Socket;
10
+ private csTriggers;
11
+ private connect;
12
+ private sub;
13
+ constructor(path: string, contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>) => Promise<Cxt>, nsDs: Namespace, nsServer: Namespace, socketPath: string);
14
+ protected registerTrigger(trigger: Trigger<ED, keyof ED, Cxt>): void;
15
+ protected operateInWatcher<T extends keyof ED>(entity: T, operation: ED[T]['Update'], context: Cxt): Promise<OperationResult<ED>>;
16
+ protected selectInWatcher<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt): Promise<Partial<ED[T]['Schema']>[]>;
17
+ }
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClusterAppLoader = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const lodash_1 = require("oak-domain/lib/utils/lodash");
6
+ const filter_1 = require("oak-domain/lib/store/filter");
7
+ const env_1 = require("./cluster/env");
8
+ const AppLoader_1 = require("./AppLoader");
9
+ const assert_1 = tslib_1.__importDefault(require("assert"));
10
+ const socket_io_client_1 = require("socket.io-client");
11
+ class ClusterAppLoader extends AppLoader_1.AppLoader {
12
+ socket;
13
+ csTriggers;
14
+ connect() {
15
+ const { instanceId } = (0, env_1.getClusterInfo)();
16
+ this.socket.on('connect', () => {
17
+ const csTriggerNames = Object.keys(this.csTriggers).map(ele => `${ele}-${instanceId}`);
18
+ if (csTriggerNames.length > 0) {
19
+ this.socket.emit('sub', csTriggerNames);
20
+ }
21
+ });
22
+ this.socket.on('disconnect', () => {
23
+ const csTriggerNames = Object.keys(this.csTriggers).map(ele => `${ele}-${instanceId}`);
24
+ if (csTriggerNames.length > 0) {
25
+ this.socket.connect();
26
+ }
27
+ });
28
+ this.socket.on('data', async (entity, name, ids, cxtStr, option) => {
29
+ const context = await this.makeContext(cxtStr);
30
+ await context.begin();
31
+ try {
32
+ await this.dbStore.execVolatileTrigger(entity, name, ids, context, option);
33
+ await context.commit();
34
+ }
35
+ catch (err) {
36
+ await context.rollback();
37
+ console.error('在集群环境下,处理来自其它实例的trigger数据,execVolatileTrigger异常', entity, name, ids, option, err);
38
+ }
39
+ });
40
+ this.socket.connect();
41
+ }
42
+ sub(name) {
43
+ const { instanceId } = (0, env_1.getClusterInfo)();
44
+ (0, assert_1.default)(!this.csTriggers[name], `命名为${name}的trigger出现了多次,请检查`);
45
+ this.csTriggers[name] = 1;
46
+ if (this.socket.connected) {
47
+ this.socket.emit('sub', [`${name}-${instanceId}`]);
48
+ }
49
+ else {
50
+ this.socket.connect();
51
+ }
52
+ }
53
+ constructor(path, contextBuilder, nsDs, nsServer, socketPath) {
54
+ super(path, contextBuilder, nsDs, nsServer);
55
+ this.dbStore.setOnVolatileTrigger(async (entity, trigger, ids, cxtStr, option) => {
56
+ const execLocal = async (ids2) => {
57
+ const context = await this.makeContext(cxtStr);
58
+ await context.begin();
59
+ try {
60
+ await this.dbStore.execVolatileTrigger(entity, trigger.name, ids2, context, option);
61
+ await context.commit();
62
+ }
63
+ catch (err) {
64
+ await context.rollback();
65
+ console.error('execVolatileTrigger异常', entity, trigger.name, ids2, option, err);
66
+ }
67
+ };
68
+ if (trigger.cs) {
69
+ // 如果是cluster sensative的触发器,需要发送到相应的instance上被处理
70
+ const context = await this.makeContext();
71
+ const rows = await context.select(entity, {
72
+ data: {
73
+ id: 1,
74
+ $$seq$$: 1,
75
+ },
76
+ filter: {
77
+ id: { $in: ids },
78
+ }
79
+ }, { dontCollect: true });
80
+ await context.commit();
81
+ const { instanceCount, instanceId } = (0, env_1.getClusterInfo)();
82
+ const grouped = (0, lodash_1.groupBy)(rows, (ele) => ele.$$seq$$ % instanceCount);
83
+ for (const seqMod in grouped) {
84
+ const ids2 = grouped[seqMod].map(ele => ele.id);
85
+ if (parseInt(seqMod) === instanceId) {
86
+ await execLocal(ids2);
87
+ }
88
+ else {
89
+ this.dataSubscriber.publishVolatileTrigger(entity, trigger.name, seqMod, ids2, cxtStr, option);
90
+ }
91
+ }
92
+ }
93
+ else {
94
+ await execLocal(ids);
95
+ }
96
+ });
97
+ const { name } = nsServer;
98
+ const socketUrl = `http://localhost:${process.env.PM2_PORT || 8080}${name}`;
99
+ this.socket = (0, socket_io_client_1.io)(socketUrl, {
100
+ path: socketPath,
101
+ });
102
+ this.connect();
103
+ this.csTriggers = {};
104
+ }
105
+ registerTrigger(trigger) {
106
+ // 如果是cluster sensative的trigger,注册到socket事件上
107
+ if (trigger.when === 'commit' && trigger.cs) {
108
+ const { name } = trigger;
109
+ this.sub(name);
110
+ }
111
+ this.dbStore.registerTrigger(trigger);
112
+ }
113
+ operateInWatcher(entity, operation, context) {
114
+ const { instanceCount, instanceId } = (0, env_1.getClusterInfo)();
115
+ (0, assert_1.default)(instanceCount && typeof instanceId === 'number');
116
+ const { filter } = operation;
117
+ const filter2 = (0, filter_1.combineFilters)(entity, this.dbStore.getSchema(), [filter, {
118
+ $$seq$$: {
119
+ $mod: [instanceCount, instanceId]
120
+ }
121
+ }]);
122
+ return super.operateInWatcher(entity, {
123
+ ...operation,
124
+ filter: filter2,
125
+ }, context);
126
+ }
127
+ selectInWatcher(entity, selection, context) {
128
+ const { instanceCount, instanceId } = (0, env_1.getClusterInfo)();
129
+ (0, assert_1.default)(instanceCount && typeof instanceId === 'number');
130
+ const { filter } = selection;
131
+ const filter2 = (0, filter_1.combineFilters)(entity, this.dbStore.getSchema(), [filter, {
132
+ $$seq$$: {
133
+ $mod: [instanceCount, instanceId]
134
+ }
135
+ }]);
136
+ return super.selectInWatcher(entity, {
137
+ ...selection,
138
+ filter: filter2,
139
+ }, context);
140
+ }
141
+ }
142
+ exports.ClusterAppLoader = ClusterAppLoader;
package/lib/DbStore.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MysqlStore, MySqlSelectOption, MysqlOperateOption } from 'oak-db';
2
- import { EntityDict, StorageSchema, Trigger, Checker, SelectOption, SelectFreeEntities, UpdateFreeDict, AuthDeduceRelationMap } from 'oak-domain/lib/types';
2
+ import { EntityDict, StorageSchema, Trigger, Checker, SelectOption, SelectFreeEntities, UpdateFreeDict, AuthDeduceRelationMap, VolatileTrigger, OperateOption } from 'oak-domain/lib/types';
3
3
  import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
4
4
  import { MySQLConfiguration } from 'oak-db/lib/MySQL/types/Configuration';
5
5
  import { BackendRuntimeContext } from 'oak-frontend-base';
@@ -7,12 +7,14 @@ import { AsyncContext, AsyncRowStore } from 'oak-domain/lib/store/AsyncRowStore'
7
7
  export declare class DbStore<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> extends MysqlStore<ED, Cxt> implements AsyncRowStore<ED, Cxt> {
8
8
  private executor;
9
9
  private relationAuth;
10
- constructor(storageSchema: StorageSchema<ED>, contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>) => Promise<Cxt>, mysqlConfiguration: MySQLConfiguration, authDeduceRelationMap: AuthDeduceRelationMap<ED>, selectFreeEntities?: SelectFreeEntities<ED>, updateFreeDict?: UpdateFreeDict<ED>);
10
+ constructor(storageSchema: StorageSchema<ED>, contextBuilder: (scene?: string) => Promise<Cxt>, mysqlConfiguration: MySQLConfiguration, authDeduceRelationMap: AuthDeduceRelationMap<ED>, selectFreeEntities?: SelectFreeEntities<ED>, updateFreeDict?: UpdateFreeDict<ED>, onVolatileTrigger?: <T extends keyof ED>(entity: T, trigger: VolatileTrigger<ED, T, Cxt>, ids: string[], cxtStr: string, option: OperateOption) => Promise<void>);
11
11
  protected cascadeUpdateAsync<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: AsyncContext<ED>, option: MysqlOperateOption): Promise<import("oak-domain/lib/types").OperationResult<ED>>;
12
12
  operate<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: Cxt, option: MysqlOperateOption): Promise<import("oak-domain/lib/types").OperationResult<ED>>;
13
13
  select<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt, option: MySqlSelectOption): Promise<Partial<ED[T]["Schema"]>[]>;
14
14
  count<T extends keyof ED>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: SelectOption): Promise<number>;
15
15
  registerTrigger<T extends keyof ED>(trigger: Trigger<ED, T, Cxt>): void;
16
16
  registerChecker<T extends keyof ED>(checker: Checker<ED, T, Cxt>): void;
17
+ setOnVolatileTrigger(onVolatileTrigger: <T extends keyof ED>(entity: T, trigger: VolatileTrigger<ED, T, Cxt>, ids: string[], cxtStr: string, option: OperateOption) => Promise<void>): void;
18
+ execVolatileTrigger<T extends keyof ED>(entity: T, name: string, ids: string[], context: Cxt, option: OperateOption): Promise<void>;
17
19
  checkpoint(ts: number): Promise<number>;
18
20
  }
package/lib/DbStore.js CHANGED
@@ -7,9 +7,9 @@ const RelationAuth_1 = require("oak-domain/lib/store/RelationAuth");
7
7
  class DbStore extends oak_db_1.MysqlStore {
8
8
  executor;
9
9
  relationAuth;
10
- constructor(storageSchema, contextBuilder, mysqlConfiguration, authDeduceRelationMap, selectFreeEntities = [], updateFreeDict = {}) {
10
+ constructor(storageSchema, contextBuilder, mysqlConfiguration, authDeduceRelationMap, selectFreeEntities = [], updateFreeDict = {}, onVolatileTrigger) {
11
11
  super(storageSchema, mysqlConfiguration);
12
- this.executor = new TriggerExecutor_1.TriggerExecutor((scene) => contextBuilder(scene)(this));
12
+ this.executor = new TriggerExecutor_1.TriggerExecutor((scene) => contextBuilder(scene), undefined, onVolatileTrigger);
13
13
  this.relationAuth = new RelationAuth_1.RelationAuth(storageSchema, authDeduceRelationMap, selectFreeEntities, updateFreeDict);
14
14
  }
15
15
  async cascadeUpdateAsync(entity, operation, context, option) {
@@ -109,6 +109,12 @@ class DbStore extends oak_db_1.MysqlStore {
109
109
  registerChecker(checker) {
110
110
  this.executor.registerChecker(checker);
111
111
  }
112
+ setOnVolatileTrigger(onVolatileTrigger) {
113
+ this.executor.setOnVolatileTrigger(onVolatileTrigger);
114
+ }
115
+ async execVolatileTrigger(entity, name, ids, context, option) {
116
+ return this.executor.execVolatileTrigger(entity, name, ids, context, option);
117
+ }
112
118
  checkpoint(ts) {
113
119
  return this.executor.checkpoint(ts);
114
120
  }
@@ -1,18 +1,23 @@
1
- import { EntityDict } from 'oak-domain/lib/types';
1
+ import { EntityDict, OperateOption, OpRecord } from 'oak-domain/lib/types';
2
2
  import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
3
3
  import { BackendRuntimeContext } from 'oak-frontend-base';
4
4
  import { Namespace } from 'socket.io';
5
+ /**
6
+ * 集群行为备忘:
7
+ * 当socket.io通过adapter在集群间通信时,测试行为如下(测试环境为pm2 + cluster-adapter,其它adpater启用时需要再测一次):
8
+ * 1)当client连接到node1并join room1时,只有node1上会有create room事件(room结构本身在结点间并不共享)
9
+ * 2)当某一个node执行 .adapter.to('room1').emit()时,连接到任一结点的client均能收到消息(但使用room可以实现跨结点推包)
10
+ * 3) serverSideEmit执行时如果有callback,而不是所有的接收者都执行callback的话,会抛出一个异常(意味着不需要本结点来判定是否收到全部的返回值了)
11
+ */
5
12
  export default class DataSubscriber<ED extends EntityDict & BaseEntityDict, Context extends BackendRuntimeContext<ED>> {
6
13
  private ns;
14
+ private nsServer;
7
15
  private contextBuilder;
8
- private filterMap;
9
- private idEntityMap;
10
- constructor(ns: Namespace, contextBuilder: (scene?: string) => Promise<Context>);
11
- private formCreateRoomRoutine;
16
+ constructor(ns: Namespace, nsServer: Namespace, contextBuilder: (scene?: string) => Promise<Context>);
12
17
  /**
13
18
  * 来自外部的socket连接,监听数据变化
14
19
  */
15
20
  private startup;
16
- private sendRecord;
17
- onDataCommited(context: Context): void;
21
+ publishEvent(event: string, records: OpRecord<ED>[], sid?: string): void;
22
+ publishVolatileTrigger(entity: keyof ED, name: string, instanceNumber: string, ids: string[], cxtStr: string, option: OperateOption): void;
18
23
  }
@@ -1,41 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const lodash_1 = require("oak-domain/lib/utils/lodash");
4
- const oak_domain_1 = require("oak-domain");
3
+ const env_1 = require("./env");
4
+ /**
5
+ * 集群行为备忘:
6
+ * 当socket.io通过adapter在集群间通信时,测试行为如下(测试环境为pm2 + cluster-adapter,其它adpater启用时需要再测一次):
7
+ * 1)当client连接到node1并join room1时,只有node1上会有create room事件(room结构本身在结点间并不共享)
8
+ * 2)当某一个node执行 .adapter.to('room1').emit()时,连接到任一结点的client均能收到消息(但使用room可以实现跨结点推包)
9
+ * 3) serverSideEmit执行时如果有callback,而不是所有的接收者都执行callback的话,会抛出一个异常(意味着不需要本结点来判定是否收到全部的返回值了)
10
+ */
5
11
  class DataSubscriber {
6
12
  ns;
13
+ nsServer;
7
14
  contextBuilder;
8
- filterMap;
9
- idEntityMap;
10
- constructor(ns, contextBuilder) {
15
+ constructor(ns, nsServer, contextBuilder) {
11
16
  this.ns = ns;
17
+ this.nsServer = nsServer;
12
18
  this.contextBuilder = contextBuilder;
13
19
  this.startup();
14
- this.filterMap = {};
15
- this.idEntityMap = {};
16
- }
17
- formCreateRoomRoutine(def) {
18
- const { id, entity, filter } = def;
19
- return (room) => {
20
- if (room === id) {
21
- console.log('instance:', process.env.NODE_APP_INSTANCE, 'add filter', room);
22
- // 本房间不存在,说明这个filter是新出现的
23
- if (this.filterMap[entity]) {
24
- // id的唯一性由前台保证,重复则无视
25
- Object.assign(this.filterMap[entity], {
26
- [id]: filter,
27
- });
28
- }
29
- else {
30
- Object.assign(this.filterMap, {
31
- [entity]: {
32
- [id]: filter,
33
- }
34
- });
35
- }
36
- this.idEntityMap[id] = entity;
37
- }
38
- };
39
20
  }
40
21
  /**
41
22
  * 来自外部的socket连接,监听数据变化
@@ -43,39 +24,14 @@ class DataSubscriber {
43
24
  startup() {
44
25
  this.ns.on('connection', async (socket) => {
45
26
  try {
46
- const { 'oak-cxt': cxtStr } = socket.handshake.headers;
47
- const context = await this.contextBuilder(cxtStr);
48
- socket.userId = context.getCurrentUserId();
49
- socket.context = context;
50
- socket.idMap = {};
51
- socket.on('sub', async (data) => {
52
- try {
53
- console.log('instance:', process.env.NODE_APP_INSTANCE, 'on sub', JSON.stringify(data));
54
- await Promise.all(data.map(async (ele) => {
55
- const { id, entity, filter } = ele;
56
- // 尝试select此filter,如果失败说明权限越界
57
- await context.select(entity, {
58
- data: {
59
- id: 1,
60
- },
61
- filter,
62
- }, {});
63
- }));
64
- }
65
- catch (err) {
66
- socket.emit('error', err.toString());
67
- return;
68
- }
69
- data.forEach((ele) => {
70
- const createRoomRoutine = this.formCreateRoomRoutine(ele);
71
- this.ns.adapter.on('create-room', createRoomRoutine);
72
- socket.join(ele.id);
73
- this.ns.adapter.off('create-room', createRoomRoutine);
74
- });
27
+ const { instanceId } = (0, env_1.getClusterInfo)();
28
+ // console.log('on connection', instanceId);
29
+ socket.on('sub', async (events) => {
30
+ events.forEach((event) => socket.join(event));
75
31
  });
76
- socket.on('unsub', (ids) => {
32
+ socket.on('unsub', (events) => {
77
33
  // console.log('instance:', process.env.NODE_APP_INSTANCE, 'on unsub', JSON.stringify(ids));
78
- ids.forEach((id) => {
34
+ events.forEach((id) => {
79
35
  socket.leave(id);
80
36
  });
81
37
  });
@@ -84,75 +40,40 @@ class DataSubscriber {
84
40
  socket.emit('error', err.toString());
85
41
  }
86
42
  });
87
- this.ns.adapter.on('delete-room', (room) => {
88
- const entity = this.idEntityMap[room];
89
- if (entity) {
90
- // console.log('instance:', process.env.NODE_APP_INSTANCE, 'remove filter', room);
91
- (0, lodash_1.unset)(this.filterMap[entity], room);
92
- (0, lodash_1.unset)(this.idEntityMap, room);
43
+ this.nsServer.on('connection', async (socket) => {
44
+ try {
45
+ const { instanceId } = (0, env_1.getClusterInfo)();
46
+ console.log('on nsServer connection', instanceId);
47
+ socket.on('sub', async (events) => {
48
+ console.log('on nsServer sub', instanceId, events);
49
+ events.forEach((event) => socket.join(event));
50
+ });
51
+ socket.on('unsub', (events) => {
52
+ // console.log('instance:', process.env.NODE_APP_INSTANCE, 'on unsub', JSON.stringify(ids));
53
+ events.forEach((id) => {
54
+ socket.leave(id);
55
+ });
56
+ });
57
+ }
58
+ catch (err) {
59
+ socket.emit('error', err.toString());
93
60
  }
94
- });
95
- this.ns.on('sendRecord', (entity, filter, record, isCreate) => {
96
- console.log('instance:', process.env.NODE_APP_INSTANCE, 'get record from another', JSON.stringify(entity));
97
61
  });
98
62
  }
99
- sendRecord(entity, filter, record, sid, isCreate) {
100
- if (entity === 'spContractApplyment') {
101
- console.log('instance:', process.env.NODE_APP_INSTANCE, 'sendRecord', JSON.stringify(entity));
63
+ publishEvent(event, records, sid) {
64
+ const { instanceId } = (0, env_1.getClusterInfo)();
65
+ // console.log('publishEvent', instanceId);
66
+ if (sid) {
67
+ this.ns.to(event).except(sid).emit('data', records);
102
68
  }
103
- this.ns.serverSideEmit('sendRecord', entity, filter, record, isCreate);
104
- if (this.filterMap[entity]) {
105
- Object.keys(this.filterMap[entity]).forEach(async (room) => {
106
- const context = await this.contextBuilder();
107
- const filter2 = this.filterMap[entity][room];
108
- let needSend = false;
109
- if (isCreate) {
110
- // 如果是插入数据肯定是单行,使用相容性检测
111
- const contained = await (0, oak_domain_1.checkFilterContains)(entity, context, filter2, filter, true);
112
- needSend = contained;
113
- }
114
- else {
115
- const repeled = await (0, oak_domain_1.checkFilterRepel)(entity, context, filter, filter2, true);
116
- needSend = !repeled;
117
- }
118
- if (needSend) {
119
- // console.log('instance:', process.env.NODE_APP_INSTANCE, 'needSend', JSON.stringify(room));
120
- if (sid) {
121
- this.ns.to(room).except(sid).emit('data', [record], [room]);
122
- }
123
- else {
124
- this.ns.to(room).emit('data', [record], [room]);
125
- }
126
- }
127
- });
69
+ else {
70
+ this.ns.to(event).emit('data', records);
128
71
  }
129
72
  }
130
- onDataCommited(context) {
131
- const sid = context.getSubscriberId();
132
- const { opRecords } = context;
133
- opRecords.forEach((record) => {
134
- const { a } = record;
135
- switch (a) {
136
- case 'c': {
137
- const { e, d } = record;
138
- this.sendRecord(e, d, record, sid, true);
139
- break;
140
- }
141
- case 'u': {
142
- const { e, d, f } = record;
143
- this.sendRecord(e, f, record, sid);
144
- break;
145
- }
146
- case 'r': {
147
- const { e, f } = record;
148
- this.sendRecord(e, f, record, sid);
149
- break;
150
- }
151
- default: {
152
- break;
153
- }
154
- }
155
- });
73
+ publishVolatileTrigger(entity, name, instanceNumber, ids, cxtStr, option) {
74
+ const { instanceId } = (0, env_1.getClusterInfo)();
75
+ console.log('publishVolatileTrigger', instanceId, instanceNumber);
76
+ this.nsServer.to(`${name}-${instanceNumber}`).emit('data', entity, name, ids, cxtStr, option);
156
77
  }
157
78
  }
158
79
  exports.default = DataSubscriber;
@@ -14,13 +14,27 @@ function getProcessEnvOption(option) {
14
14
  return process.env[upperCase];
15
15
  }
16
16
  }
17
- // 初始化判定集群状态,目前支持pm2的集群信息
17
+ // 初始化判定集群状态,需要在环境变量中注入两个值
18
+ /** pm2注入方法,见:https://pm2.fenxianglu.cn/docs/general/environment-variables
19
+ * apps: [
20
+ {
21
+ name: 'xxx',
22
+ script: "xxxjs",
23
+ instances: "2",
24
+ increment_var: "OAK_INSTANCE_ID",
25
+ env: {
26
+ OAK_INSTANCE_CNT: 9,
27
+ OAK_INSTANCE_ID: 8,
28
+ }
29
+ },
30
+ ],
31
+ **/
18
32
  function initialize() {
19
- const pmId = getProcessEnvOption('NODE_APP_INSTANCE');
20
- if (pmId) {
33
+ const instanceIdStr = getProcessEnvOption('OAK_INSTANCE_ID');
34
+ if (instanceIdStr) {
21
35
  const usingCluster = true;
22
- const instanceId = parseInt(pmId);
23
- const instanceCount = parseInt(getProcessEnvOption('instances'));
36
+ const instanceId = parseInt(instanceIdStr);
37
+ const instanceCount = parseInt(getProcessEnvOption('OAK_INSTANCE_CNT'));
24
38
  return {
25
39
  usingCluster,
26
40
  instanceCount,
package/lib/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { AppLoader } from './AppLoader';
2
+ export { ClusterAppLoader } from './ClusterAppLoader';
2
3
  export * from './cluster/env';
package/lib/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AppLoader = void 0;
3
+ exports.ClusterAppLoader = exports.AppLoader = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  var AppLoader_1 = require("./AppLoader");
6
6
  Object.defineProperty(exports, "AppLoader", { enumerable: true, get: function () { return AppLoader_1.AppLoader; } });
7
+ var ClusterAppLoader_1 = require("./ClusterAppLoader");
8
+ Object.defineProperty(exports, "ClusterAppLoader", { enumerable: true, get: function () { return ClusterAppLoader_1.ClusterAppLoader; } });
7
9
  tslib_1.__exportStar(require("./cluster/env"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oak-backend-base",
3
- "version": "3.2.1",
3
+ "version": "3.2.2",
4
4
  "description": "oak-backend-base",
5
5
  "main": "lib/index",
6
6
  "author": {
@@ -21,10 +21,11 @@
21
21
  "mysql2": "^2.3.3",
22
22
  "node-schedule": "^2.1.0",
23
23
  "oak-common-aspect": "^2.2.3",
24
- "oak-frontend-base": "^4.0.1",
25
- "oak-db": "^3.0.3",
26
- "oak-domain": "^4.0.0",
24
+ "oak-db": "^3.0.4",
25
+ "oak-domain": "^4.0.1",
26
+ "oak-frontend-base": "^4.0.2",
27
27
  "socket.io": "^4.7.2",
28
+ "socket.io-client": "^4.7.2",
28
29
  "uuid": "^8.3.2"
29
30
  },
30
31
  "license": "ISC",