@tstdl/base 0.93.170 → 0.93.172
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.
- package/api/server/gateway.js +1 -1
- package/api/server/middlewares/server-sent-events.middleware.js +21 -8
- package/api/utils.d.ts +0 -1
- package/api/utils.js +0 -1
- package/document-management/models/ai-configuration.d.ts +4 -0
- package/document-management/server/services/document-management-ai.service.d.ts +1 -1
- package/document-management/server/services/document-management-ai.service.js +28 -16
- package/examples/document-management/ai-provider.d.ts +1 -0
- package/examples/document-management/ai-provider.js +5 -4
- package/examples/document-management/main.js +1 -1
- package/notification/api/notification.api.d.ts +2 -0
- package/notification/api/notification.api.js +1 -0
- package/notification/server/api/notification.api-controller.js +3 -1
- package/notification/server/services/notification-sse.service.d.ts +2 -1
- package/notification/server/services/notification-sse.service.js +34 -19
- package/package.json +1 -1
- package/sse/data-stream-source.d.ts +7 -2
- package/sse/data-stream-source.js +6 -3
- package/sse/server-sent-events-source.d.ts +7 -1
- package/sse/server-sent-events-source.js +20 -2
package/api/server/gateway.js
CHANGED
|
@@ -202,7 +202,7 @@ let ApiGateway = ApiGateway_1 = class ApiGateway {
|
|
|
202
202
|
abortSignal: context.abortSignal,
|
|
203
203
|
serverSentEvents: {
|
|
204
204
|
get lastEventId() {
|
|
205
|
-
return context.request.headers.tryGetSingle('Last-Event-ID');
|
|
205
|
+
return (context.request.headers.tryGetSingle('Last-Event-ID') ?? context.request.query.tryGetSingle('lastEventId'));
|
|
206
206
|
},
|
|
207
207
|
},
|
|
208
208
|
tryGetToken: async () => {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { DataStream, ServerSentEvents } from '../../../sse/index.js';
|
|
1
2
|
import { toArray } from '../../../utils/array/array.js';
|
|
3
|
+
import { isDefined } from '../../../utils/type-guards.js';
|
|
2
4
|
/**
|
|
3
5
|
* Middleware that adds required CORS headers for SSE and cache busting.
|
|
4
6
|
* @param context Gateway context.
|
|
@@ -6,13 +8,24 @@ import { toArray } from '../../../utils/array/array.js';
|
|
|
6
8
|
*/
|
|
7
9
|
export async function serverSentEventsMiddleware(context, next) {
|
|
8
10
|
await next();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
const isEvents = (context.response.bodyType == 'events');
|
|
12
|
+
const isOptions = (context.request.method == 'OPTIONS');
|
|
13
|
+
if (isEvents) {
|
|
14
|
+
context.response.headers.setIfMissing('Cache-Control', 'no-cache');
|
|
15
|
+
context.response.headers.setIfMissing('Connection', 'keep-alive');
|
|
16
|
+
context.response.headers.setIfMissing('X-Accel-Buffering', 'no');
|
|
17
|
+
}
|
|
18
|
+
if (isOptions) {
|
|
19
|
+
const requestMethod = context.request.headers.tryGetSingle('Access-Control-Request-Method') ?? context.request.method;
|
|
20
|
+
const endpointDefinition = context.api.endpoints.get(requestMethod)?.definition;
|
|
21
|
+
const isSse = isDefined(endpointDefinition) && ((endpointDefinition.result == DataStream) || (endpointDefinition.result == ServerSentEvents));
|
|
22
|
+
if (isSse || (endpointDefinition?.bustCache == true)) {
|
|
23
|
+
const existing = context.response.headers.tryGet('Access-Control-Allow-Headers');
|
|
24
|
+
const items = toArray(existing).flatMap((header) => header?.split(',') ?? []).map((header) => header.trim()).filter((header) => header.length > 0);
|
|
25
|
+
const headers = new Set(items);
|
|
26
|
+
headers.add('Cache-Control');
|
|
27
|
+
headers.add('Last-Event-ID');
|
|
28
|
+
context.response.headers.set('Access-Control-Allow-Headers', [...headers].join(', '));
|
|
29
|
+
}
|
|
11
30
|
}
|
|
12
|
-
const existing = context.response.headers.tryGet('Access-Control-Allow-Headers');
|
|
13
|
-
const items = toArray(existing).flatMap((header) => header?.split(',') ?? []).map((header) => header.trim()).filter((header) => header.length > 0);
|
|
14
|
-
const headers = new Set(items);
|
|
15
|
-
headers.add('Cache-Control');
|
|
16
|
-
headers.add('Last-Event-ID');
|
|
17
|
-
context.response.headers.set('Access-Control-Allow-Headers', [...headers].join(', '));
|
|
18
31
|
}
|
package/api/utils.d.ts
CHANGED
|
@@ -5,6 +5,5 @@ type GetApiEndpointUrlData = {
|
|
|
5
5
|
defaultPrefix: string | undefined | null;
|
|
6
6
|
explicitVersion?: number | null;
|
|
7
7
|
};
|
|
8
|
-
export declare const defaultAccessControlAllowHeaders = "Content-Type, Authorization";
|
|
9
8
|
export declare function getFullApiEndpointResource({ api, endpoint, defaultPrefix, explicitVersion }: GetApiEndpointUrlData): string;
|
|
10
9
|
export {};
|
package/api/utils.js
CHANGED
|
@@ -2,7 +2,6 @@ import { toArray } from '../utils/array/array.js';
|
|
|
2
2
|
import { compareByValueDescending } from '../utils/comparison.js';
|
|
3
3
|
import { sort } from '../utils/iterable-helpers/sort.js';
|
|
4
4
|
import { isDefined, isNull } from '../utils/type-guards.js';
|
|
5
|
-
export const defaultAccessControlAllowHeaders = 'Content-Type, Authorization';
|
|
6
5
|
export function getFullApiEndpointResource({ api, endpoint, defaultPrefix, explicitVersion }) {
|
|
7
6
|
const versionArray = toArray(isDefined(explicitVersion) ? explicitVersion : endpoint.version);
|
|
8
7
|
const version = sort(versionArray, compareByValueDescending)[0];
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ModelReference } from 'genkit';
|
|
2
2
|
import type { Instructions } from '../../ai/prompts/instructions-formatter.js';
|
|
3
3
|
import type { DocumentWorkflowStep } from './document-workflow.model.js';
|
|
4
|
+
import type { GeminiModelConfig } from '../../ai/index.js';
|
|
4
5
|
export type InstructionStrategy = 'replace' | 'append';
|
|
6
|
+
export type DocumentManagementThinkingLevel = NonNullable<GeminiModelConfig['thinkingConfig']>['thinkingLevel'];
|
|
5
7
|
export type InstructionOverride = string | {
|
|
6
8
|
/**
|
|
7
9
|
* Simple way: Provide a specific format pattern.
|
|
@@ -20,6 +22,8 @@ export type InstructionOverride = string | {
|
|
|
20
22
|
export type AiConfiguration = {
|
|
21
23
|
/** The model to use. */
|
|
22
24
|
model?: ModelReference<any>;
|
|
25
|
+
/** The thinking level to use for thinking models. */
|
|
26
|
+
thinkingLevel?: DocumentManagementThinkingLevel;
|
|
23
27
|
/** The language to use for AI outputs. */
|
|
24
28
|
language?: string;
|
|
25
29
|
/** Prompt overrides. */
|
|
@@ -2,7 +2,7 @@ import { type TstdlGenkitGenerationOptions } from '../../../ai/genkit/index.js';
|
|
|
2
2
|
import { type PromptBuilder } from '../../../ai/prompts/index.js';
|
|
3
3
|
import { type SchemaTestable } from '../../../schema/index.js';
|
|
4
4
|
import type { ObjectLiteral } from '../../../types/types.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type ModelReference } from 'genkit';
|
|
6
6
|
import type { AiConfiguration, DocumentPropertyDataType, InstructionOverride } from '../../models/index.js';
|
|
7
7
|
import { Document, DocumentWorkflowStep } from '../../models/index.js';
|
|
8
8
|
import { type AiConfigurationResolveDataMap } from './document-management-ai-provider.service.js';
|
|
@@ -19,8 +19,8 @@ import { distinct } from '../../../utils/array/index.js';
|
|
|
19
19
|
import { numericDateToDateTime, tryDateObjectToNumericDate } from '../../../utils/date-time.js';
|
|
20
20
|
import { fromEntries, hasOwnProperty, objectEntries, objectKeys } from '../../../utils/object/object.js';
|
|
21
21
|
import { assertDefined, assertDefinedPass, assertNotNull, isDefined, isNotNull, isNotNullOrUndefined, isNull, isString, isUndefined } from '../../../utils/type-guards.js';
|
|
22
|
+
import { GenkitError } from 'genkit';
|
|
22
23
|
import { Document, DocumentProperty, DocumentRequestState, DocumentType, DocumentTypeProperty, DocumentWorkflowStep } from '../../models/index.js';
|
|
23
|
-
import { DocumentManagementConfiguration } from '../module.js';
|
|
24
24
|
import { documentCategory, documentRequest, documentRequestCollectionAssignment, documentType } from '../schemas.js';
|
|
25
25
|
import { DocumentCategoryTypeService } from './document-category-type.service.js';
|
|
26
26
|
import { DocumentCollectionService } from './document-collection.service.js';
|
|
@@ -41,7 +41,6 @@ let DocumentManagementAiService = DocumentManagementAiService_1 = class Document
|
|
|
41
41
|
#documentCategoryTypeService = inject(DocumentCategoryTypeService);
|
|
42
42
|
#documentFileService = inject(DocumentFileService);
|
|
43
43
|
#documentPropertyService = inject(DocumentPropertyService);
|
|
44
|
-
#documentManagementConfiguration = inject(DocumentManagementConfiguration, undefined, { optional: true });
|
|
45
44
|
#aiProvider = inject(DocumentManagementAiProviderService, undefined, { optional: true });
|
|
46
45
|
#documentPropertyRepository = injectRepository(DocumentProperty);
|
|
47
46
|
#documentRepository = injectRepository(Document);
|
|
@@ -83,7 +82,7 @@ let DocumentManagementAiService = DocumentManagementAiService_1 = class Document
|
|
|
83
82
|
promptBuilder,
|
|
84
83
|
schema,
|
|
85
84
|
document,
|
|
86
|
-
config: { maxOutputTokens:
|
|
85
|
+
config: { maxOutputTokens: 1024 },
|
|
87
86
|
aiConfig,
|
|
88
87
|
});
|
|
89
88
|
const typeId = typeLabelEntries.find((entry) => entry.label == result.documentType)?.id;
|
|
@@ -149,7 +148,7 @@ let DocumentManagementAiService = DocumentManagementAiService_1 = class Document
|
|
|
149
148
|
data: { existingTags: tagLabels },
|
|
150
149
|
schema: generationSchema,
|
|
151
150
|
document,
|
|
152
|
-
config: { maxOutputTokens:
|
|
151
|
+
config: { maxOutputTokens: 4096 },
|
|
153
152
|
aiConfig,
|
|
154
153
|
});
|
|
155
154
|
const filteredDocumentTags = extraction.documentTags.filter((tag) => (tag != extraction.documentTitle) && (tag != extraction.documentSubtitle));
|
|
@@ -207,7 +206,7 @@ let DocumentManagementAiService = DocumentManagementAiService_1 = class Document
|
|
|
207
206
|
promptBuilder: createAssignCollectionPrompt(),
|
|
208
207
|
data: { document: documentData, documentProperties: fromEntries(propertyEntries), collections },
|
|
209
208
|
schema: assignCollectionSchema,
|
|
210
|
-
config: { maxOutputTokens:
|
|
209
|
+
config: { maxOutputTokens: 2048 },
|
|
211
210
|
});
|
|
212
211
|
return result.collectionIds;
|
|
213
212
|
}
|
|
@@ -253,13 +252,15 @@ let DocumentManagementAiService = DocumentManagementAiService_1 = class Document
|
|
|
253
252
|
promptBuilder: createAssignRequestPrompt(),
|
|
254
253
|
data: { document: documentData, documentProperties: fromEntries(propertyEntries), requests },
|
|
255
254
|
schema: assignRequestSchema,
|
|
256
|
-
config: { maxOutputTokens:
|
|
255
|
+
config: { maxOutputTokens: 1024 },
|
|
257
256
|
});
|
|
258
257
|
return result.requestId;
|
|
259
258
|
}
|
|
260
259
|
async runAi(tenantId, step, stepData, options) {
|
|
261
260
|
const config = options.aiConfig ?? await this.resolveAiConfiguration(tenantId, step, stepData);
|
|
262
261
|
const model = config.model ?? options.defaultModel;
|
|
262
|
+
const thinkingLevel = config.thinkingLevel ?? 'LOW';
|
|
263
|
+
const configuredModel = isString(model) ? model : model.withConfig({ thinkingConfig: { thinkingLevel } });
|
|
263
264
|
const builder = options.promptBuilder;
|
|
264
265
|
if (isDefined(config.language)) {
|
|
265
266
|
builder.addInstructions({ 'Output Language': languagePrompt(config.language) });
|
|
@@ -282,17 +283,26 @@ let DocumentManagementAiService = DocumentManagementAiService_1 = class Document
|
|
|
282
283
|
if (isDefined(options.document)) {
|
|
283
284
|
builder.addMedia(await this.#documentFileService.getContent(options.document), options.document.mimeType);
|
|
284
285
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
286
|
+
try {
|
|
287
|
+
const result = await this.#genkit.generate(genkitGenerationOptions({
|
|
288
|
+
model: configuredModel,
|
|
289
|
+
config: options.config,
|
|
290
|
+
output: { schema: options.schema },
|
|
291
|
+
system: builder.buildSystemPrompt(),
|
|
292
|
+
prompt: builder.buildUserPrompt(),
|
|
293
|
+
}));
|
|
294
|
+
this.#logger.trace(`AI result for ${step} ${options.targetId ?? ''}: usage=${JSON.stringify(result.usage)}, finishReason=${result.finishReason}`);
|
|
295
|
+
if (isNull(result.output)) {
|
|
296
|
+
throw new Error(`AI returned null output for ${step} ${options.targetId ?? ''}. Finish reason: ${result.finishReason}`);
|
|
297
|
+
}
|
|
298
|
+
return result.output;
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (error instanceof GenkitError) {
|
|
302
|
+
this.#logger.error(`AI ${step} ${options.targetId ?? ''} failed: status=${error.status}, message=${error.originalMessage}, details=${JSON.stringify(error.detail)}`);
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
294
305
|
}
|
|
295
|
-
return result.output;
|
|
296
306
|
}
|
|
297
307
|
async resolveAiConfiguration(tenantId, step, data) {
|
|
298
308
|
const globalConfig = await this.#aiProvider?.getGlobalConfiguration(tenantId);
|
|
@@ -312,6 +322,7 @@ let DocumentManagementAiService = DocumentManagementAiService_1 = class Document
|
|
|
312
322
|
stepConfig,
|
|
313
323
|
].filter(isDefined);
|
|
314
324
|
const model = mergeMostSpecific(configs, 'model');
|
|
325
|
+
const thinkingLevel = mergeMostSpecific(configs, 'thinkingLevel');
|
|
315
326
|
const language = mergeMostSpecific(configs, 'language');
|
|
316
327
|
const systemAddition = resolveAdditions(configs.map((c) => c.prompt?.systemAddition));
|
|
317
328
|
const userAddition = resolveAdditions(configs.map((c) => c.prompt?.userAddition));
|
|
@@ -321,6 +332,7 @@ let DocumentManagementAiService = DocumentManagementAiService_1 = class Document
|
|
|
321
332
|
const classification = mergeMostSpecific(configs, 'classification');
|
|
322
333
|
return {
|
|
323
334
|
model,
|
|
335
|
+
thinkingLevel,
|
|
324
336
|
language,
|
|
325
337
|
prompt: {
|
|
326
338
|
systemAddition,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AiConfiguration, DocumentManagementAiConfiguration, DocumentWorkflowStep } from '../../document-management/index.js';
|
|
2
2
|
import { DocumentManagementAiProviderService, type AiConfigurationResolveDataMap } from '../../document-management/server/index.js';
|
|
3
3
|
export declare class ExampleAiProviderService extends DocumentManagementAiProviderService {
|
|
4
|
+
#private;
|
|
4
5
|
/**
|
|
5
6
|
* Providing global defaults for the entire Document Management module.
|
|
6
7
|
*/
|
|
@@ -9,6 +9,7 @@ import { fewShotPrompt, stylePrompt } from '../../ai/prompts/index.js';
|
|
|
9
9
|
import { DocumentManagementAiProviderService } from '../../document-management/server/index.js';
|
|
10
10
|
import { Singleton } from '../../injector/index.js';
|
|
11
11
|
let ExampleAiProviderService = class ExampleAiProviderService extends DocumentManagementAiProviderService {
|
|
12
|
+
#model = injectModel('gemini-3.1-flash-lite-preview').withConfig({ thinkingConfig: { thinkingLevel: 'LOW' } });
|
|
12
13
|
/**
|
|
13
14
|
* Providing global defaults for the entire Document Management module.
|
|
14
15
|
*/
|
|
@@ -16,9 +17,9 @@ let ExampleAiProviderService = class ExampleAiProviderService extends DocumentMa
|
|
|
16
17
|
return {
|
|
17
18
|
defaults: {
|
|
18
19
|
language: 'German', // Steer all AI outputs to German
|
|
19
|
-
model:
|
|
20
|
+
model: this.#model,
|
|
20
21
|
prompt: {
|
|
21
|
-
|
|
22
|
+
// systemAddition: 'Additional global instructions for all AI steps.',
|
|
22
23
|
},
|
|
23
24
|
},
|
|
24
25
|
};
|
|
@@ -32,9 +33,9 @@ let ExampleAiProviderService = class ExampleAiProviderService extends DocumentMa
|
|
|
32
33
|
getClassificationConfiguration() {
|
|
33
34
|
return {
|
|
34
35
|
// Use a faster/cheaper model for classification
|
|
35
|
-
model:
|
|
36
|
+
model: this.#model,
|
|
36
37
|
prompt: {
|
|
37
|
-
systemAddition: 'For classification, focus strictly on
|
|
38
|
+
systemAddition: 'For classification, focus strictly on document content.',
|
|
38
39
|
},
|
|
39
40
|
};
|
|
40
41
|
}
|
|
@@ -49,7 +49,7 @@ const config = {
|
|
|
49
49
|
},
|
|
50
50
|
},
|
|
51
51
|
s3: {
|
|
52
|
-
endpoint: string('S3_ENDPOINT', 'http://localhost:
|
|
52
|
+
endpoint: string('S3_ENDPOINT', 'http://localhost:19552'),
|
|
53
53
|
accessKey: string('S3_ACCESS_KEY', 'tstdl-dev'),
|
|
54
54
|
secretKey: string('S3_SECRET_KEY', 'tstdl-dev'),
|
|
55
55
|
bucket: string('S3_BUCKET', undefined),
|
|
@@ -15,6 +15,7 @@ export declare const notificationApiDefinition: {
|
|
|
15
15
|
credentials: true;
|
|
16
16
|
dataStream: {
|
|
17
17
|
idProvider: () => string;
|
|
18
|
+
heartbeatInterval: number;
|
|
18
19
|
};
|
|
19
20
|
};
|
|
20
21
|
types: {
|
|
@@ -131,6 +132,7 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
|
|
|
131
132
|
credentials: true;
|
|
132
133
|
dataStream: {
|
|
133
134
|
idProvider: () => string;
|
|
135
|
+
heartbeatInterval: number;
|
|
134
136
|
};
|
|
135
137
|
};
|
|
136
138
|
types: {
|
|
@@ -21,7 +21,8 @@ let NotificationApiController = class NotificationApiController {
|
|
|
21
21
|
const token = await getToken();
|
|
22
22
|
const source = this.sseService.register(token.payload.tenant, token.payload.subject);
|
|
23
23
|
const asyncIterable = toAsyncIterable(source);
|
|
24
|
-
|
|
24
|
+
const abortHandler = () => this.sseService.unregister(token.payload.tenant, token.payload.subject, source);
|
|
25
|
+
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
25
26
|
try {
|
|
26
27
|
if (isDefined(lastEventId)) {
|
|
27
28
|
const lastEventIdNumber = Number(lastEventId);
|
|
@@ -41,6 +42,7 @@ let NotificationApiController = class NotificationApiController {
|
|
|
41
42
|
yield* asyncIterable;
|
|
42
43
|
}
|
|
43
44
|
finally {
|
|
45
|
+
abortSignal.removeEventListener('abort', abortHandler);
|
|
44
46
|
this.sseService.unregister(token.payload.tenant, token.payload.subject, source);
|
|
45
47
|
}
|
|
46
48
|
}
|
|
@@ -14,9 +14,10 @@ type NotificationBusMessage = {
|
|
|
14
14
|
unreadCount?: number;
|
|
15
15
|
};
|
|
16
16
|
export type NotificationBusMessageData = TypedOmit<NotificationBusMessage, 'tenantId' | 'userId'>;
|
|
17
|
-
export declare class NotificationSseService {
|
|
17
|
+
export declare class NotificationSseService implements Disposable {
|
|
18
18
|
#private;
|
|
19
19
|
[afterResolve](): void;
|
|
20
|
+
[Symbol.dispose](): void;
|
|
20
21
|
register(tenantId: string, userId: string): Subject<NotificationStreamItem>;
|
|
21
22
|
unregister(tenantId: string, userId: string, source: Subject<NotificationStreamItem>): void;
|
|
22
23
|
dispatch(tenantId: string, userId: string, data: NotificationBusMessageData): Promise<void>;
|
|
@@ -7,6 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { Subject } from 'rxjs';
|
|
8
8
|
import { afterResolve, inject, Singleton } from '../../../injector/index.js';
|
|
9
9
|
import { MessageBusProvider } from '../../../message-bus/index.js';
|
|
10
|
+
import { isDefined, isUndefined } from '../../../utils/type-guards.js';
|
|
10
11
|
let NotificationSseService = class NotificationSseService {
|
|
11
12
|
#messageBusProvider = inject(MessageBusProvider);
|
|
12
13
|
#messageBus = this.#messageBusProvider.get('notification');
|
|
@@ -14,6 +15,16 @@ let NotificationSseService = class NotificationSseService {
|
|
|
14
15
|
[afterResolve]() {
|
|
15
16
|
this.#messageBus.allMessages$.subscribe((message) => this.dispatchToLocal(message));
|
|
16
17
|
}
|
|
18
|
+
[Symbol.dispose]() {
|
|
19
|
+
for (const tenantMap of this.#sources.values()) {
|
|
20
|
+
for (const userSources of tenantMap.values()) {
|
|
21
|
+
for (const source of userSources) {
|
|
22
|
+
source.complete();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
this.#sources.clear();
|
|
27
|
+
}
|
|
17
28
|
register(tenantId, userId) {
|
|
18
29
|
const source = new Subject();
|
|
19
30
|
let tenantMap = this.#sources.get(tenantId);
|
|
@@ -31,15 +42,17 @@ let NotificationSseService = class NotificationSseService {
|
|
|
31
42
|
}
|
|
32
43
|
unregister(tenantId, userId, source) {
|
|
33
44
|
const tenantMap = this.#sources.get(tenantId);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
userSources
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
if (isDefined(tenantMap)) {
|
|
46
|
+
const userSources = tenantMap.get(userId);
|
|
47
|
+
if (isDefined(userSources)) {
|
|
48
|
+
userSources.delete(source);
|
|
49
|
+
if (userSources.size == 0) {
|
|
50
|
+
tenantMap.delete(userId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (tenantMap.size == 0) {
|
|
54
|
+
this.#sources.delete(tenantId);
|
|
39
55
|
}
|
|
40
|
-
}
|
|
41
|
-
if (tenantMap?.size == 0) {
|
|
42
|
-
this.#sources.delete(tenantId);
|
|
43
56
|
}
|
|
44
57
|
source.complete();
|
|
45
58
|
}
|
|
@@ -53,17 +66,19 @@ let NotificationSseService = class NotificationSseService {
|
|
|
53
66
|
dispatchToLocal(message) {
|
|
54
67
|
const tenantMap = this.#sources.get(message.tenantId);
|
|
55
68
|
const userSources = tenantMap?.get(message.userId);
|
|
56
|
-
if (userSources
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
if (isUndefined(userSources)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const item = {
|
|
73
|
+
notification: message.notification,
|
|
74
|
+
readId: message.readId,
|
|
75
|
+
readAll: message.readAll,
|
|
76
|
+
archiveId: message.archiveId,
|
|
77
|
+
archiveAll: message.archiveAll,
|
|
78
|
+
unreadCount: message.unreadCount,
|
|
79
|
+
};
|
|
80
|
+
for (const source of userSources) {
|
|
81
|
+
source.next(item);
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
};
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Signal } from '../signals/index.js';
|
|
1
2
|
import type { UndefinableJson } from '../types/types.js';
|
|
2
3
|
import type { AnyIterable } from '../utils/any-iterable-iterator.js';
|
|
3
4
|
import { ServerSentEventsSource } from './server-sent-events-source.js';
|
|
@@ -9,14 +10,18 @@ export type DataStreamSourceOptions<T> = {
|
|
|
9
10
|
* For notification-like streams, where only new items are sent, delta should usually be disabled.
|
|
10
11
|
*/
|
|
11
12
|
delta?: boolean;
|
|
13
|
+
/** Interval in milliseconds to send a heartbeat (SSE comment). */
|
|
14
|
+
heartbeatInterval?: number;
|
|
15
|
+
/** Initial retry recommendation for consumers in milliseconds. */
|
|
16
|
+
retry?: number;
|
|
12
17
|
errorFormatter?: DataStreamErrorFormatter;
|
|
13
18
|
idProvider?: (data: T) => string | undefined;
|
|
14
19
|
};
|
|
15
20
|
export declare class DataStreamSource<T> {
|
|
16
21
|
#private;
|
|
17
22
|
readonly eventSource: ServerSentEventsSource;
|
|
18
|
-
readonly closed:
|
|
19
|
-
constructor(
|
|
23
|
+
readonly closed: Signal<boolean>;
|
|
24
|
+
constructor(options?: DataStreamSourceOptions<T>);
|
|
20
25
|
static fromIterable<T>(iterable: AnyIterable<T>, options?: DataStreamSourceOptions<T>): DataStreamSource<T>;
|
|
21
26
|
send(data: T, id?: string): Promise<void>;
|
|
22
27
|
close(): Promise<void>;
|
|
@@ -17,13 +17,16 @@ export class DataStreamSource {
|
|
|
17
17
|
#useDelta;
|
|
18
18
|
#errorFormatter;
|
|
19
19
|
#idProvider;
|
|
20
|
-
eventSource
|
|
21
|
-
closed
|
|
20
|
+
eventSource;
|
|
21
|
+
closed;
|
|
22
22
|
#lastData;
|
|
23
|
-
constructor(
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
const { delta = true, errorFormatter, idProvider, heartbeatInterval, retry } = options;
|
|
24
25
|
this.#useDelta = delta;
|
|
25
26
|
this.#errorFormatter = errorFormatter ?? defaultErrorFormatter;
|
|
26
27
|
this.#idProvider = idProvider;
|
|
28
|
+
this.eventSource = new ServerSentEventsSource({ heartbeatInterval, retry });
|
|
29
|
+
this.closed = this.eventSource.closed;
|
|
27
30
|
}
|
|
28
31
|
static fromIterable(iterable, options) {
|
|
29
32
|
const source = new DataStreamSource(options);
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { ServerSentJsonEvent, ServerSentTextEvent } from './model.js';
|
|
2
|
+
export type ServerSentEventsSourceOptions = {
|
|
3
|
+
/** Interval in milliseconds to send a heartbeat (SSE comment). */
|
|
4
|
+
heartbeatInterval?: number;
|
|
5
|
+
/** Initial retry recommendation for consumers in milliseconds. */
|
|
6
|
+
retry?: number;
|
|
7
|
+
};
|
|
2
8
|
export declare class ServerSentEventsSource {
|
|
3
9
|
#private;
|
|
4
10
|
readonly readable: ReadableStream<string>;
|
|
5
11
|
readonly closed: import("../signals/api.js").Signal<boolean>;
|
|
6
12
|
readonly error: import("../signals/api.js").Signal<Error | undefined>;
|
|
7
|
-
constructor();
|
|
13
|
+
constructor(options?: ServerSentEventsSourceOptions);
|
|
8
14
|
close(): Promise<void>;
|
|
9
15
|
sendComment(comment: string): Promise<void>;
|
|
10
16
|
sendText({ name, data, id, retry }: ServerSentTextEvent): Promise<void>;
|
|
@@ -8,17 +8,35 @@ export class ServerSentEventsSource {
|
|
|
8
8
|
readable;
|
|
9
9
|
closed = this.#closed.asReadonly();
|
|
10
10
|
error = this.#error.asReadonly();
|
|
11
|
-
constructor() {
|
|
11
|
+
constructor(options) {
|
|
12
12
|
const { writable, readable } = new TransformStream();
|
|
13
13
|
this.#writable = writable;
|
|
14
14
|
this.readable = readable;
|
|
15
15
|
this.#writer = this.#writable.getWriter();
|
|
16
|
+
let interval;
|
|
16
17
|
this.#writer.closed
|
|
17
|
-
.then(() =>
|
|
18
|
+
.then(() => {
|
|
19
|
+
this.#closed.set(true);
|
|
20
|
+
clearInterval(interval);
|
|
21
|
+
})
|
|
18
22
|
.catch((error) => {
|
|
19
23
|
this.#error.set(error);
|
|
20
24
|
this.#closed.set(true);
|
|
25
|
+
clearInterval(interval);
|
|
21
26
|
});
|
|
27
|
+
if (isDefined(options?.retry)) {
|
|
28
|
+
void this.sendText({ retry: options.retry });
|
|
29
|
+
}
|
|
30
|
+
if (isDefined(options?.heartbeatInterval)) {
|
|
31
|
+
interval = setInterval(() => {
|
|
32
|
+
if (this.#closed()) {
|
|
33
|
+
clearInterval(interval);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
void this.sendComment('').catch(() => { });
|
|
37
|
+
}, options.heartbeatInterval);
|
|
38
|
+
interval.unref?.();
|
|
39
|
+
}
|
|
22
40
|
}
|
|
23
41
|
async close() {
|
|
24
42
|
await this.#writer.close();
|