@blocklet/sdk 1.17.2-beta-20251111-114226-13276e42 → 1.17.2-beta-20251113-121338-9c917e68

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,6 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { TNotification, TNotificationInput, TSendOptions } from '../types/notification';
3
3
  type $TSFixMe = any;
4
+ export declare const getConnectionToken: () => string;
4
5
  export declare const getSender: () => {
5
6
  appDid: string;
6
7
  wallet: import("@ocap/wallet").WalletObject<string>;
@@ -30,17 +31,15 @@ declare const doSendMail: (receiver: string | string[], notification: TNotificat
30
31
  export declare const broadcast: (notification: TNotificationInput, options?: TSendOptions) => Promise<any>;
31
32
  export declare const _eventBus: EventEmitter<[never]>;
32
33
  export declare const _emitter: EventEmitter<[never]>;
34
+ export declare const cleanup: () => void;
33
35
  export declare const ensureClient: () => Promise<void>;
34
- export declare const cleanup: () => Promise<void>;
35
36
  export declare const on: (event: string, cb?: $TSFixMe) => Promise<EventEmitter<[never]>>;
36
37
  export declare const off: any;
37
38
  export declare const _message: {
38
39
  on: (event: string, cb: $TSFixMe) => Promise<EventEmitter<[never]>>;
39
40
  off: any;
40
41
  };
41
- export { doSendToUser as sendToUser };
42
- export { doSendMail as sendToMail };
43
- export { doSendToRelay as sendToRelay };
42
+ export { doSendMail as sendToMail, doSendToRelay as sendToRelay, doSendToUser as sendToUser };
44
43
  declare const _default: {
45
44
  sendToUser: (receiver: string | string[], notification: TNotificationInput, options?: TSendOptions) => Promise<any>;
46
45
  sendToRelay: (topic: string, event: string, data: any) => Promise<any>;
@@ -52,5 +51,6 @@ declare const _default: {
52
51
  on: (event: string, cb: $TSFixMe) => Promise<EventEmitter<[never]>>;
53
52
  off: any;
54
53
  };
54
+ initClient: () => Promise<void>;
55
55
  };
56
56
  export default _default;
@@ -36,24 +36,45 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.sendToRelay = exports.sendToMail = exports.sendToUser = exports._message = exports.off = exports.on = exports.cleanup = exports.ensureClient = exports._emitter = exports._eventBus = exports.broadcast = exports.getSender = void 0;
39
+ exports.sendToUser = exports.sendToRelay = exports.sendToMail = exports._message = exports.off = exports.on = exports.ensureClient = exports.cleanup = exports._emitter = exports._eventBus = exports.broadcast = exports.getSender = exports.getConnectionToken = void 0;
40
40
  /* eslint-disable no-console */
41
41
  /* eslint-disable @typescript-eslint/naming-convention */
42
42
  const Jwt = __importStar(require("@arcblock/jwt"));
43
- const debug_1 = __importDefault(require("debug"));
44
- const node_events_1 = require("node:events");
45
43
  const ws_1 = require("@arcblock/ws");
46
- const util_1 = require("@blocklet/meta/lib/util");
47
- const channel_1 = require("@blocklet/meta/lib/channel");
48
44
  const constant_1 = require("@blocklet/constant");
45
+ const channel_1 = require("@blocklet/meta/lib/channel");
46
+ const util_1 = require("@blocklet/meta/lib/util");
47
+ const debug_1 = __importDefault(require("debug"));
48
+ const debounce_1 = __importDefault(require("lodash/debounce"));
49
+ const node_events_1 = require("node:events");
49
50
  const check_blocklet_env_1 = require("../util/check-blocklet-env");
50
- const send_notification_1 = require("../util/send-notification");
51
51
  const constants_1 = require("../util/constants");
52
- const wallet_1 = require("../wallet");
53
- const notification_1 = require("../validators/notification");
54
52
  const parse_docker_endpoint_1 = require("../util/parse-docker-endpoint");
55
- const socket_1 = require("../util/socket");
53
+ const send_notification_1 = require("../util/send-notification");
54
+ const notification_1 = require("../validators/notification");
55
+ const wallet_1 = require("../wallet");
56
56
  const debug = (0, debug_1.default)('@blocklet/sdk:notification');
57
+ let client = null;
58
+ let connectionToken = null;
59
+ let connectionTokenTimer = null;
60
+ let initPromise = null; // 全局 Promise,确保多次调用时等待同一个初始化过程
61
+ const refreshConnectionToken = async (force = false) => {
62
+ // don't refresh token if client is closed
63
+ if (!force && connectionToken === null && connectionTokenTimer === null) {
64
+ return;
65
+ }
66
+ const accessWallet = (0, wallet_1.getAccessWallet)();
67
+ connectionToken = await accessWallet.signJWT({});
68
+ // refresh token in 12 hours
69
+ if (connectionTokenTimer) {
70
+ clearTimeout(connectionTokenTimer);
71
+ }
72
+ connectionTokenTimer = setTimeout(refreshConnectionToken, process.env.NODE_ENV === 'test' ? 1000 * 3 : 1000 * 60 * 60);
73
+ console.log('refresh connection token', Jwt.decode(connectionToken));
74
+ };
75
+ const debouncedRefreshConnectionToken = (0, debounce_1.default)(refreshConnectionToken, 300);
76
+ const getConnectionToken = () => connectionToken;
77
+ exports.getConnectionToken = getConnectionToken;
57
78
  const getSender = () => {
58
79
  const wallet = (0, wallet_1.getWallet)();
59
80
  const accessWallet = (0, wallet_1.getAccessWallet)();
@@ -128,165 +149,163 @@ const ensureErrorListener = () => {
128
149
  messageEmitter.on('error', noop);
129
150
  exports._eventBus.on('error', noop);
130
151
  };
131
- let client = null;
132
- let reconnectTimer = null;
133
- let reconnectAttempts = 0;
134
- const MAX_RECONNECT_ATTEMPTS = process.env.NODE_ENV === 'test' ? 5 : 30;
135
- const RECONNECT_DELAY_MS = process.env.NODE_ENV === 'test' ? 500 : 2000;
136
- const reconnect = () => {
137
- // Schedule reconnection (only once even if multiple channels fail)
138
- if (!reconnectTimer) {
139
- // Check retry limit
140
- if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
141
- console.error(`Max reconnection attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Stopping reconnection.`);
142
- return;
143
- }
144
- reconnectAttempts += 1;
145
- console.log(`Scheduling reconnection attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}...`);
146
- reconnectTimer = setTimeout(async () => {
147
- reconnectTimer = null;
148
- // eslint-disable-next-line no-use-before-define, @typescript-eslint/no-use-before-define
149
- await initClient();
150
- }, RECONNECT_DELAY_MS);
151
- }
152
- };
153
- const joinChannelErrorHandler = (name, type, emitters) => (err) => {
152
+ const joinChannelErrorHandler = (name, type, emitters) => async (err) => {
154
153
  const msg = `join ${name || 'channel'} ${type || 'error'}${err?.message ? ': ' : ''}${err?.message || ''}`;
155
154
  console.error(msg);
156
155
  (emitters || [emitter]).forEach((x) => x.emit('error', { message: msg }));
157
- // On any join error/timeout, cleanup client and schedule reconnect with fresh token
156
+ await debouncedRefreshConnectionToken();
157
+ };
158
+ const initClient = () => {
159
+ // 如果正在初始化,返回同一个 Promise 等待完成
160
+ if (initPromise) {
161
+ return initPromise;
162
+ }
163
+ // 如果已经初始化完成,直接返回
158
164
  if (client) {
159
- console.log(`Cleaning up client due to ${name} join error, will reconnect with fresh token...`);
160
- (0, socket_1.safeDisconnect)(client).finally(() => {
161
- client = null;
162
- reconnect();
163
- });
165
+ return Promise.resolve();
164
166
  }
165
- };
166
- const joinSuccessHandler = () => {
167
- reconnectAttempts = 0;
168
- };
169
- const initClient = async () => {
170
- if (!client) {
171
- ensureErrorListener();
172
- const accessWallet = (0, wallet_1.getAccessWallet)();
173
- const componentDid = process.env.BLOCKLET_COMPONENT_DID;
174
- const appDid = process.env.BLOCKLET_APP_PID;
175
- const { publicKey: pk } = accessWallet;
176
- // Generate one token and reuse it for connection and all channels
177
- const token = await accessWallet.signJWT({});
178
- // Build URL with query parameters directly
179
- const baseUrl = `ws://${(0, parse_docker_endpoint_1.getServerHost)()}:${process.env.ABT_NODE_SERVICE_PORT}${constants_1.SERVICE_PREFIX}/websocket`;
180
- const url = `${baseUrl}?token=${encodeURIComponent(token)}&pk=${encodeURIComponent(pk)}`;
181
- client = new ws_1.WsClient(url, {
182
- heartbeatIntervalMs: 10 * 1000,
183
- });
184
- client.connect();
185
- const messageChannel = client.channel(accessWallet.address, () => ({ token, pk }));
186
- const appPublicChannel = client.channel((0, channel_1.getAppPublicChannel)(appDid), () => ({ token, pk }));
187
- const componentChannel = client.channel((0, channel_1.getComponentChannel)(appDid, componentDid), () => ({
188
- token,
189
- pk,
190
- apiKey: process.env.BLOCKLET_COMPONENT_API_KEY,
191
- }));
192
- const eventBusChannel = client.channel((0, channel_1.getEventBusChannel)(appDid), () => ({ token, pk }));
193
- messageChannel
194
- .join()
195
- .receive('ok', joinSuccessHandler)
196
- .receive('error', joinChannelErrorHandler('message channel', 'error', [messageEmitter, emitter]))
197
- .receive('timeout', joinChannelErrorHandler('message channel', 'timeout', [messageEmitter, emitter]));
198
- appPublicChannel
199
- .join()
200
- .receive('ok', joinSuccessHandler)
201
- .receive('error', joinChannelErrorHandler('app public channel', 'error'))
202
- .receive('timeout', joinChannelErrorHandler('app public channel', 'timeout'));
203
- componentChannel
204
- .join()
205
- .receive('ok', joinSuccessHandler)
206
- .receive('error', joinChannelErrorHandler('app component channel', 'error'))
207
- .receive('timeout', joinChannelErrorHandler('app component channel', 'timeout'));
208
- eventBusChannel
209
- .join()
210
- .receive('ok', joinSuccessHandler)
211
- .receive('error', joinChannelErrorHandler('eventbus channel', 'error'))
212
- .receive('timeout', joinChannelErrorHandler('eventbus channel', 'timeout'));
213
- messageChannel.on('message', ({ status, response } = {}) => {
214
- debug('messageChannel.on', { status, response });
215
- if (status === 'ok') {
216
- messageEmitter.emit(response.type, response);
217
- if (response.type === notification_1.NOTIFICATION_TYPES.HI) {
218
- emitter.emit(response.type, response);
167
+ // 创建新的初始化 Promise
168
+ initPromise = (async () => {
169
+ try {
170
+ ensureErrorListener();
171
+ const accessWallet = (0, wallet_1.getAccessWallet)();
172
+ const componentDid = process.env.BLOCKLET_COMPONENT_DID;
173
+ const appDid = process.env.BLOCKLET_APP_PID;
174
+ const { publicKey: pk } = accessWallet;
175
+ await refreshConnectionToken(true);
176
+ // Build URL with query parameters directly
177
+ const baseUrl = `ws://${(0, parse_docker_endpoint_1.getServerHost)()}:${process.env.ABT_NODE_SERVICE_PORT}${constants_1.SERVICE_PREFIX}/websocket`;
178
+ const url = `${baseUrl}?token=${encodeURIComponent(connectionToken)}&pk=${encodeURIComponent(pk)}`;
179
+ client = new ws_1.WsClient(url, {
180
+ heartbeatIntervalMs: 10 * 1000,
181
+ });
182
+ client.connect();
183
+ const messageChannel = client.channel(accessWallet.address, () => ({ token: connectionToken, pk }));
184
+ const appPublicChannel = client.channel((0, channel_1.getAppPublicChannel)(appDid), () => ({ token: connectionToken, pk }));
185
+ const componentChannel = client.channel((0, channel_1.getComponentChannel)(appDid, componentDid), () => ({
186
+ token: connectionToken,
187
+ pk,
188
+ apiKey: process.env.BLOCKLET_COMPONENT_API_KEY,
189
+ }));
190
+ const eventBusChannel = client.channel((0, channel_1.getEventBusChannel)(appDid), () => ({ token: connectionToken, pk }));
191
+ messageChannel
192
+ .join()
193
+ .receive('error', joinChannelErrorHandler('message channel', 'error', [messageEmitter, emitter]))
194
+ .receive('timeout', joinChannelErrorHandler('message channel', 'timeout', [messageEmitter, emitter]));
195
+ appPublicChannel
196
+ .join()
197
+ .receive('error', joinChannelErrorHandler('app public channel', 'error'))
198
+ .receive('timeout', joinChannelErrorHandler('app public channel', 'timeout'));
199
+ componentChannel
200
+ .join()
201
+ .receive('error', joinChannelErrorHandler('app component channel', 'error'))
202
+ .receive('timeout', joinChannelErrorHandler('app component channel', 'timeout'));
203
+ eventBusChannel
204
+ .join()
205
+ .receive('error', joinChannelErrorHandler('eventbus channel', 'error'))
206
+ .receive('timeout', joinChannelErrorHandler('eventbus channel', 'timeout'));
207
+ messageChannel.on('message', ({ status, response } = {}) => {
208
+ debug('messageChannel.on', { status, response });
209
+ if (status === 'ok') {
210
+ messageEmitter.emit(response.type, response);
211
+ if (response.type === notification_1.NOTIFICATION_TYPES.HI) {
212
+ emitter.emit(response.type, response);
213
+ }
219
214
  }
220
- }
221
- else {
222
- emitError(response);
223
- console.error('Message channel error', { status, response });
224
- }
225
- });
226
- eventBusChannel.on('event', ({ status, response } = {}) => {
227
- debug('eventBusChannel.on', { status, response });
228
- if (status === 'ok') {
229
- // ignore events from self
230
- if (response.source !== process.env.BLOCKLET_COMPONENT_DID) {
231
- exports._eventBus.emit('event', response);
215
+ else {
216
+ emitError(response);
217
+ console.error('Message channel error', { status, response });
232
218
  }
233
- }
234
- else {
235
- exports._eventBus.emit('error', response);
236
- console.error('Event channel error', { status, response });
237
- }
238
- });
239
- [...Object.keys(constant_1.BlockletInternalEvents), ...Object.keys(constant_1.TeamEvents)].forEach((key) => {
240
- const event = constant_1.BlockletInternalEvents[key] || constant_1.TeamEvents[key];
241
- componentChannel.on(event, async ({ status, response } = {}) => {
242
- debug('componentChannel.on', { event, status, response });
219
+ });
220
+ eventBusChannel.on('event', ({ status, response } = {}) => {
221
+ debug('eventBusChannel.on', { status, response });
243
222
  if (status === 'ok') {
244
- const { data, sender, time } = response;
245
- if (!time || new Date(time).getTime() < new Date(process.env.BLOCKLET_START_AT).getTime()) {
246
- return;
223
+ // ignore events from self
224
+ if (response.source !== process.env.BLOCKLET_COMPONENT_DID) {
225
+ exports._eventBus.emit('event', response);
247
226
  }
248
- // verify sender is server
249
- const tolerance = 600;
250
- if (!(await Jwt.verify(sender.token, process.env.ABT_NODE_PK, { tolerance }))) {
251
- const message = `verify sender failed in internal events. event: ${event}, sender: ${JSON.stringify({
252
- sender,
253
- decode: Jwt.decode(sender.token),
254
- now: Date.now(),
255
- ABT_NODE_PK: process.env.ABT_NODE_PK,
256
- })}`;
257
- emitError({ message });
258
- console.error(message);
259
- return;
227
+ }
228
+ else {
229
+ exports._eventBus.emit('error', response);
230
+ console.error('Event channel error', { status, response });
231
+ }
232
+ });
233
+ [...Object.keys(constant_1.BlockletInternalEvents), ...Object.keys(constant_1.TeamEvents)].forEach((key) => {
234
+ const event = constant_1.BlockletInternalEvents[key] || constant_1.TeamEvents[key];
235
+ componentChannel.on(event, async ({ status, response } = {}) => {
236
+ debug('componentChannel.on', { event, status, response });
237
+ if (status === 'ok') {
238
+ const { data, sender, time } = response;
239
+ if (!time || new Date(time).getTime() < new Date(process.env.BLOCKLET_START_AT).getTime()) {
240
+ return;
241
+ }
242
+ // verify sender is server
243
+ const tolerance = 600;
244
+ if (!(await Jwt.verify(sender.token, process.env.ABT_NODE_PK, { tolerance }))) {
245
+ const message = `verify sender failed in internal events. event: ${event}, sender: ${JSON.stringify({
246
+ sender,
247
+ decode: Jwt.decode(sender.token),
248
+ now: Date.now(),
249
+ ABT_NODE_PK: process.env.ABT_NODE_PK,
250
+ })}`;
251
+ emitError({ message });
252
+ console.error(message);
253
+ return;
254
+ }
255
+ emitter.emit(event, data);
256
+ // Emit team events to event bus
257
+ if (constant_1.TeamEvents[key]) {
258
+ exports._eventBus.emit('event', {
259
+ id: (0, util_1.nanoid)(),
260
+ time: new Date().toISOString(),
261
+ type: `blocklet.${event}`,
262
+ data: { object: data.user },
263
+ });
264
+ }
260
265
  }
261
- emitter.emit(event, data);
262
- // Emit team events to event bus
263
- if (constant_1.TeamEvents[key]) {
264
- exports._eventBus.emit('event', {
265
- id: (0, util_1.nanoid)(),
266
- time: new Date().toISOString(),
267
- type: `blocklet.${event}`,
268
- data: { object: data.user },
269
- });
266
+ else {
267
+ emitError(response);
268
+ console.error('Component channel error', { status, response });
270
269
  }
270
+ });
271
+ });
272
+ appPublicChannel.on(notification_1.NOTIFICATION_TYPES.HI, ({ status, response } = {}) => {
273
+ debug('appPublicChannel.on', { event: notification_1.NOTIFICATION_TYPES.HI, status, response });
274
+ if (status === 'ok') {
275
+ emitter.emit(notification_1.NOTIFICATION_TYPES.HI, response);
271
276
  }
272
277
  else {
273
- emitError(response);
274
- console.error('Component channel error', { status, response });
278
+ emitter.emit('error', response);
279
+ console.error('App public channel error', { status, response });
275
280
  }
276
281
  });
277
- });
278
- appPublicChannel.on(notification_1.NOTIFICATION_TYPES.HI, ({ status, response } = {}) => {
279
- debug('appPublicChannel.on', { event: notification_1.NOTIFICATION_TYPES.HI, status, response });
280
- if (status === 'ok') {
281
- emitter.emit(notification_1.NOTIFICATION_TYPES.HI, response);
282
- }
283
- else {
284
- emitter.emit('error', response);
285
- console.error('App public channel error', { status, response });
286
- }
287
- });
282
+ }
283
+ catch (err) {
284
+ initPromise = null; // 清除 Promise 以便重试
285
+ console.warn('Failed to init notification service', err);
286
+ throw err;
287
+ }
288
+ })();
289
+ return initPromise;
290
+ };
291
+ const cleanup = () => {
292
+ try {
293
+ if (connectionTokenTimer) {
294
+ clearTimeout(connectionTokenTimer);
295
+ connectionTokenTimer = null;
296
+ }
297
+ if (client) {
298
+ client.disconnect();
299
+ client = null;
300
+ }
301
+ connectionToken = null;
302
+ initPromise = null;
303
+ }
304
+ catch (err) {
305
+ console.warn('Failed to cleanup notification service', err);
288
306
  }
289
307
  };
308
+ exports.cleanup = cleanup;
290
309
  const ensureClient = async () => {
291
310
  if (process.env.BLOCKLET_MODE === 'test') {
292
311
  return;
@@ -296,18 +315,6 @@ const ensureClient = async () => {
296
315
  }
297
316
  };
298
317
  exports.ensureClient = ensureClient;
299
- const cleanup = async () => {
300
- if (reconnectTimer) {
301
- clearTimeout(reconnectTimer);
302
- reconnectTimer = null;
303
- }
304
- if (client) {
305
- await (0, socket_1.safeDisconnect)(client);
306
- client = null;
307
- }
308
- reconnectAttempts = 0;
309
- };
310
- exports.cleanup = cleanup;
311
318
  const on = async (event, cb) => {
312
319
  try {
313
320
  await (0, exports.ensureClient)();
@@ -339,4 +346,5 @@ exports.default = {
339
346
  on: exports.on,
340
347
  off: exports.off,
341
348
  _message: exports._message,
349
+ initClient,
342
350
  };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.17.2-beta-20251111-114226-13276e42",
6
+ "version": "1.17.2-beta-20251113-121338-9c917e68",
7
7
  "description": "graphql client to read/write data on abt node",
8
8
  "homepage": "https://www.arcblock.io/docs/blocklet-sdk-nodejs",
9
9
  "main": "lib/index.js",
@@ -26,20 +26,20 @@
26
26
  "author": "linchen1987 <linchen.1987@foxmail.com> (http://github.com/linchen1987)",
27
27
  "license": "Apache-2.0",
28
28
  "dependencies": {
29
- "@abtnode/constant": "1.17.2-beta-20251111-114226-13276e42",
30
- "@abtnode/db-cache": "1.17.2-beta-20251111-114226-13276e42",
31
- "@abtnode/util": "1.17.2-beta-20251111-114226-13276e42",
29
+ "@abtnode/constant": "1.17.2-beta-20251113-121338-9c917e68",
30
+ "@abtnode/db-cache": "1.17.2-beta-20251113-121338-9c917e68",
31
+ "@abtnode/util": "1.17.2-beta-20251113-121338-9c917e68",
32
32
  "@arcblock/did": "^1.27.4",
33
33
  "@arcblock/did-connect-js": "^1.27.4",
34
34
  "@arcblock/did-ext": "^1.27.4",
35
35
  "@arcblock/jwt": "^1.27.4",
36
36
  "@arcblock/ws": "^1.27.4",
37
- "@blocklet/constant": "1.17.2-beta-20251111-114226-13276e42",
38
- "@blocklet/env": "1.17.2-beta-20251111-114226-13276e42",
37
+ "@blocklet/constant": "1.17.2-beta-20251113-121338-9c917e68",
38
+ "@blocklet/env": "1.17.2-beta-20251113-121338-9c917e68",
39
39
  "@blocklet/error": "^0.3.2",
40
- "@blocklet/meta": "1.17.2-beta-20251111-114226-13276e42",
41
- "@blocklet/server-js": "1.17.2-beta-20251111-114226-13276e42",
42
- "@blocklet/theme": "^3.2.2",
40
+ "@blocklet/meta": "1.17.2-beta-20251113-121338-9c917e68",
41
+ "@blocklet/server-js": "1.17.2-beta-20251113-121338-9c917e68",
42
+ "@blocklet/theme": "^3.2.3",
43
43
  "@did-connect/authenticator": "^2.2.8",
44
44
  "@did-connect/handler": "^2.2.8",
45
45
  "@nedb/core": "^2.1.5",
@@ -82,5 +82,5 @@
82
82
  "ts-node": "^10.9.1",
83
83
  "typescript": "^5.6.3"
84
84
  },
85
- "gitHead": "3a85bab76a65b0f04427f9a7e74e8d4fcbcdd3f1"
85
+ "gitHead": "82362ffba5d50f01774b257b9cd8143adeb7a898"
86
86
  }