@tstdl/base 0.93.107 → 0.93.109

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,6 @@
1
1
  import { type Observable } from 'rxjs';
2
2
  import type { ToJson } from '../interfaces.js';
3
- import { type Signal } from '../signals/index.js';
3
+ import { type Signal } from '../signals/api.js';
4
4
  export declare abstract class Collection<T, TItemsIterator extends IterableIterator<any>, TThis extends Collection<T, TItemsIterator, TThis> = Collection<T, TItemsIterator, any>> implements Iterable<T>, ToJson {
5
5
  private readonly sizeSubject;
6
6
  private readonly changeSubject;
@@ -1,5 +1,6 @@
1
1
  import { BehaviorSubject, Subject, distinctUntilChanged, filter, firstValueFrom, map, startWith } from 'rxjs';
2
- import { toLazySignal, untracked } from '../signals/index.js';
2
+ import { untracked } from '../signals/api.js';
3
+ import { toLazySignal } from '../signals/to-lazy-signal.js';
3
4
  import { lazyProperty } from '../utils/object/lazy-property.js';
4
5
  export class Collection {
5
6
  sizeSubject = new BehaviorSubject(0);
@@ -40,7 +40,7 @@ __decorate([
40
40
  InAppNotification = __decorate([
41
41
  NotificationTable({ name: 'in_app' }),
42
42
  ForeignKey(() => NotificationLogEntity, ['tenantId', 'logId', 'userId'], ['tenantId', 'id', 'userId']),
43
- Index(['tenantId', 'userId', 'readTimestamp', 'archiveTimestamp'])
43
+ Index(['tenantId', 'userId', 'logId'], { where: () => ({ archiveTimestamp: null }) })
44
44
  ], InAppNotification);
45
45
  export { InAppNotification };
46
46
  export class InAppNotificationView extends InAppNotification {
@@ -1,5 +1,6 @@
1
+ import type { Merge } from 'type-fest';
1
2
  import { type EnumType } from '../../enumeration/index.js';
2
- import { type Json, TenantEntity, type Uuid } from '../../orm/index.js';
3
+ import { type Json, TenantBaseEntity, type Timestamp, type Uuid } from '../../orm/index.js';
3
4
  import type { ObjectLiteral } from '../../types/types.js';
4
5
  export declare const NotificationChannel: {
5
6
  readonly InApp: "in-app";
@@ -44,19 +45,17 @@ export type NotificationLog<Definitions extends NotificationDefinitionMap = Noti
44
45
  export type NotificationLogView<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> = {
45
46
  [Type in NotificationTypes<Definitions>]: Omit<NotificationLogEntity<Definitions, Type>, 'type' | 'payload'> & {
46
47
  type: Type;
47
- payload: NotificationPayload<Definitions, Type>;
48
- view: NotificationView<Definitions, Type>;
48
+ payload: Merge<NotificationPayload<Definitions, Type>, NotificationView<Definitions, Type>>;
49
49
  };
50
50
  }[NotificationTypes<Definitions>];
51
- export declare class NotificationLogEntity<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap, Type extends NotificationTypes<Definitions> = NotificationTypes<Definitions>> extends TenantEntity {
51
+ export declare class NotificationLogEntity<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap, Type extends NotificationTypes<Definitions> = NotificationTypes<Definitions>> extends TenantBaseEntity {
52
52
  static readonly entityName = "NotificationLog";
53
53
  userId: Uuid;
54
54
  type: Type;
55
+ timestamp: Timestamp;
55
56
  priority: NotificationPriority;
56
57
  status: NotificationStatus;
57
58
  currentStep: number;
59
+ triggerSubjectId: Uuid;
58
60
  payload: Json<NotificationPayload<Definitions, Type>> | null;
59
61
  }
60
- export declare class NotificationLogViewEntity<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap, Type extends NotificationTypes<Definitions> = NotificationTypes<Definitions>> extends NotificationLogEntity<Definitions, Type> {
61
- view?: unknown;
62
- }
@@ -7,10 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
- import { User } from '../../authentication/models/user.model.js';
10
+ import { Subject, User } from '../../authentication/index.js';
11
11
  import { defineEnum } from '../../enumeration/index.js';
12
- import { JsonProperty, Reference, TenantEntity, TenantReference, Unique, UuidProperty } from '../../orm/index.js';
13
- import { Enumeration, Integer, StringProperty, Unknown } from '../../schema/index.js';
12
+ import { Index, JsonProperty, Reference, TenantBaseEntity, TenantReference, TimestampProperty, Unique, UuidProperty } from '../../orm/index.js';
13
+ import { Enumeration, Integer, StringProperty } from '../../schema/index.js';
14
14
  import { NotificationTable } from './notification-table.js';
15
15
  import { NotificationType } from './notification-type.model.js';
16
16
  export const NotificationChannel = defineEnum('NotificationChannel', {
@@ -31,13 +31,15 @@ export const NotificationStatus = defineEnum('NotificationStatus', {
31
31
  Read: 'read',
32
32
  Failed: 'failed',
33
33
  });
34
- let NotificationLogEntity = class NotificationLogEntity extends TenantEntity {
34
+ let NotificationLogEntity = class NotificationLogEntity extends TenantBaseEntity {
35
35
  static entityName = 'NotificationLog';
36
36
  userId;
37
37
  type;
38
+ timestamp;
38
39
  priority;
39
40
  status;
40
41
  currentStep;
42
+ triggerSubjectId;
41
43
  payload;
42
44
  };
43
45
  __decorate([
@@ -50,6 +52,10 @@ __decorate([
50
52
  StringProperty(),
51
53
  __metadata("design:type", String)
52
54
  ], NotificationLogEntity.prototype, "type", void 0);
55
+ __decorate([
56
+ TimestampProperty(),
57
+ __metadata("design:type", Number)
58
+ ], NotificationLogEntity.prototype, "timestamp", void 0);
53
59
  __decorate([
54
60
  Enumeration(NotificationPriority),
55
61
  __metadata("design:type", String)
@@ -62,19 +68,18 @@ __decorate([
62
68
  Integer(),
63
69
  __metadata("design:type", Number)
64
70
  ], NotificationLogEntity.prototype, "currentStep", void 0);
71
+ __decorate([
72
+ TenantReference(() => Subject),
73
+ UuidProperty(),
74
+ __metadata("design:type", String)
75
+ ], NotificationLogEntity.prototype, "triggerSubjectId", void 0);
65
76
  __decorate([
66
77
  JsonProperty({ nullable: true }),
67
78
  __metadata("design:type", Object)
68
79
  ], NotificationLogEntity.prototype, "payload", void 0);
69
80
  NotificationLogEntity = __decorate([
70
81
  NotificationTable({ name: 'log' }),
71
- Unique(['tenantId', 'id', 'userId'])
82
+ Unique(['tenantId', 'id', 'userId']),
83
+ Index(['tenantId', 'userId', 'timestamp'])
72
84
  ], NotificationLogEntity);
73
85
  export { NotificationLogEntity };
74
- export class NotificationLogViewEntity extends NotificationLogEntity {
75
- view;
76
- }
77
- __decorate([
78
- Unknown(),
79
- __metadata("design:type", Object)
80
- ], NotificationLogViewEntity.prototype, "view", void 0);
@@ -16,15 +16,12 @@ CREATE TABLE "notification"."log" (
16
16
  "tenant_id" uuid NOT NULL,
17
17
  "user_id" uuid NOT NULL,
18
18
  "type" text NOT NULL,
19
+ "timestamp" timestamp with time zone NOT NULL,
19
20
  "priority" "notification"."notification_priority" NOT NULL,
20
21
  "status" "notification"."notification_status" NOT NULL,
21
22
  "current_step" integer NOT NULL,
23
+ "trigger_subject_id" uuid NOT NULL,
22
24
  "payload" jsonb,
23
- "revision" integer NOT NULL,
24
- "revision_timestamp" timestamp with time zone NOT NULL,
25
- "create_timestamp" timestamp with time zone NOT NULL,
26
- "delete_timestamp" timestamp with time zone,
27
- "attributes" jsonb DEFAULT '{}'::jsonb NOT NULL,
28
25
  CONSTRAINT "log_tenant_id_id_pk" PRIMARY KEY("tenant_id","id"),
29
26
  CONSTRAINT "log_tenant_id_id_user_id_unique" UNIQUE("tenant_id","id","user_id")
30
27
  );
@@ -79,7 +76,9 @@ ALTER TABLE "notification"."in_app" ADD CONSTRAINT "in_app_id_user_fkey" FOREIGN
79
76
  ALTER TABLE "notification"."in_app" ADD CONSTRAINT "in_app_tenantId_logId_userId_log_fkey" FOREIGN KEY ("tenant_id","log_id","user_id") REFERENCES "notification"."log"("tenant_id","id","user_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
80
77
  ALTER TABLE "notification"."log" ADD CONSTRAINT "log_type_type_key_fk" FOREIGN KEY ("type") REFERENCES "notification"."type"("key") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
81
78
  ALTER TABLE "notification"."log" ADD CONSTRAINT "log_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
79
+ ALTER TABLE "notification"."log" ADD CONSTRAINT "log_id_subject_fkey" FOREIGN KEY ("tenant_id","trigger_subject_id") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
82
80
  ALTER TABLE "notification"."preference" ADD CONSTRAINT "preference_type_type_key_fk" FOREIGN KEY ("type") REFERENCES "notification"."type"("key") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
83
81
  ALTER TABLE "notification"."preference" ADD CONSTRAINT "preference_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
84
82
  ALTER TABLE "notification"."web_push_subscription" ADD CONSTRAINT "web_push_subscription_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
85
- CREATE INDEX "in_app_tenant_id_user_id_read_timestamp_archive_timestamp_idx" ON "notification"."in_app" USING btree ("tenant_id","user_id","read_timestamp","archive_timestamp");
83
+ CREATE INDEX "in_app_tenant_id_user_id_log_id_idx" ON "notification"."in_app" USING btree ("tenant_id","user_id","log_id") WHERE "notification"."in_app"."archive_timestamp" is null;--> statement-breakpoint
84
+ CREATE INDEX "log_tenant_id_user_id_timestamp_idx" ON "notification"."log" USING btree ("tenant_id","user_id","timestamp");
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "7751e29f-1334-4511-8bf5-0d0f2550b5ae",
2
+ "id": "90cd6144-d455-4a13-8148-ca119f8bfe79",
3
3
  "prevId": "00000000-0000-0000-0000-000000000000",
4
4
  "version": "7",
5
5
  "dialect": "postgresql",
@@ -47,8 +47,8 @@
47
47
  }
48
48
  },
49
49
  "indexes": {
50
- "in_app_tenant_id_user_id_read_timestamp_archive_timestamp_idx": {
51
- "name": "in_app_tenant_id_user_id_read_timestamp_archive_timestamp_idx",
50
+ "in_app_tenant_id_user_id_log_id_idx": {
51
+ "name": "in_app_tenant_id_user_id_log_id_idx",
52
52
  "columns": [
53
53
  {
54
54
  "expression": "tenant_id",
@@ -63,19 +63,14 @@
63
63
  "nulls": "last"
64
64
  },
65
65
  {
66
- "expression": "read_timestamp",
67
- "isExpression": false,
68
- "asc": true,
69
- "nulls": "last"
70
- },
71
- {
72
- "expression": "archive_timestamp",
66
+ "expression": "log_id",
73
67
  "isExpression": false,
74
68
  "asc": true,
75
69
  "nulls": "last"
76
70
  }
77
71
  ],
78
72
  "isUnique": false,
73
+ "where": "\"notification\".\"in_app\".\"archive_timestamp\" is null",
79
74
  "concurrently": false,
80
75
  "method": "btree",
81
76
  "with": {}
@@ -160,6 +155,12 @@
160
155
  "primaryKey": false,
161
156
  "notNull": true
162
157
  },
158
+ "timestamp": {
159
+ "name": "timestamp",
160
+ "type": "timestamp with time zone",
161
+ "primaryKey": false,
162
+ "notNull": true
163
+ },
163
164
  "priority": {
164
165
  "name": "priority",
165
166
  "type": "notification_priority",
@@ -180,45 +181,48 @@
180
181
  "primaryKey": false,
181
182
  "notNull": true
182
183
  },
183
- "payload": {
184
- "name": "payload",
185
- "type": "jsonb",
186
- "primaryKey": false,
187
- "notNull": false
188
- },
189
- "revision": {
190
- "name": "revision",
191
- "type": "integer",
192
- "primaryKey": false,
193
- "notNull": true
194
- },
195
- "revision_timestamp": {
196
- "name": "revision_timestamp",
197
- "type": "timestamp with time zone",
198
- "primaryKey": false,
199
- "notNull": true
200
- },
201
- "create_timestamp": {
202
- "name": "create_timestamp",
203
- "type": "timestamp with time zone",
184
+ "trigger_subject_id": {
185
+ "name": "trigger_subject_id",
186
+ "type": "uuid",
204
187
  "primaryKey": false,
205
188
  "notNull": true
206
189
  },
207
- "delete_timestamp": {
208
- "name": "delete_timestamp",
209
- "type": "timestamp with time zone",
210
- "primaryKey": false,
211
- "notNull": false
212
- },
213
- "attributes": {
214
- "name": "attributes",
190
+ "payload": {
191
+ "name": "payload",
215
192
  "type": "jsonb",
216
193
  "primaryKey": false,
217
- "notNull": true,
218
- "default": "'{}'::jsonb"
194
+ "notNull": false
195
+ }
196
+ },
197
+ "indexes": {
198
+ "log_tenant_id_user_id_timestamp_idx": {
199
+ "name": "log_tenant_id_user_id_timestamp_idx",
200
+ "columns": [
201
+ {
202
+ "expression": "tenant_id",
203
+ "isExpression": false,
204
+ "asc": true,
205
+ "nulls": "last"
206
+ },
207
+ {
208
+ "expression": "user_id",
209
+ "isExpression": false,
210
+ "asc": true,
211
+ "nulls": "last"
212
+ },
213
+ {
214
+ "expression": "timestamp",
215
+ "isExpression": false,
216
+ "asc": true,
217
+ "nulls": "last"
218
+ }
219
+ ],
220
+ "isUnique": false,
221
+ "concurrently": false,
222
+ "method": "btree",
223
+ "with": {}
219
224
  }
220
225
  },
221
- "indexes": {},
222
226
  "foreignKeys": {
223
227
  "log_type_type_key_fk": {
224
228
  "name": "log_type_type_key_fk",
@@ -249,6 +253,22 @@
249
253
  ],
250
254
  "onDelete": "no action",
251
255
  "onUpdate": "no action"
256
+ },
257
+ "log_id_subject_fkey": {
258
+ "name": "log_id_subject_fkey",
259
+ "tableFrom": "log",
260
+ "tableTo": "subject",
261
+ "schemaTo": "authentication",
262
+ "columnsFrom": [
263
+ "tenant_id",
264
+ "trigger_subject_id"
265
+ ],
266
+ "columnsTo": [
267
+ "tenant_id",
268
+ "id"
269
+ ],
270
+ "onDelete": "no action",
271
+ "onUpdate": "no action"
252
272
  }
253
273
  },
254
274
  "compositePrimaryKeys": {
@@ -5,8 +5,8 @@
5
5
  {
6
6
  "idx": 0,
7
7
  "version": "7",
8
- "when": 1770118617281,
9
- "tag": "0000_ancient_hellion",
8
+ "when": 1770233636064,
9
+ "tag": "0000_oval_rage",
10
10
  "breakpoints": true
11
11
  }
12
12
  ]
@@ -80,7 +80,7 @@ let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transa
80
80
  for (const channel of enabledChannels) {
81
81
  // TODO: what if an error occurs here? partial delivery? Should we use tasks to allow retrying individual channels?
82
82
  const [viewData] = await this.#notificationAncillaryService.getViewData([notification]);
83
- await this.sendToChannel({ ...notification, view: viewData }, channel, tx);
83
+ await this.sendToChannel({ ...notification, payload: { ...notification.payload, ...viewData } }, channel, tx);
84
84
  }
85
85
  await this.#notificationLogRepository.withTransaction(tx).update(notificationId, { status: NotificationStatus.Sent, currentStep: 1 });
86
86
  if (isNotNull(type.escalations) && (type.escalations.length > 0)) {
@@ -99,7 +99,7 @@ let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transa
99
99
  return TaskProcessResult.Complete();
100
100
  }
101
101
  const [viewData] = await this.#notificationAncillaryService.getViewData([notification]);
102
- await this.sendToChannel({ ...notification, view: viewData }, rule.channel, tx);
102
+ await this.sendToChannel({ ...notification, payload: { ...notification.payload, ...viewData } }, rule.channel, tx);
103
103
  await this.#notificationLogRepository.withTransaction(tx).update(notificationId, { currentStep: step + 1 });
104
104
  if (step < type.escalations.length) {
105
105
  const nextRule = type.escalations[step];
@@ -4,7 +4,7 @@ import type { TypedOmit } from '../../../types/types.js';
4
4
  import { NotificationPreference, type InAppNotificationView, type NotificationChannel, type NotificationDefinitionMap, type NotificationLog } from '../../models/index.js';
5
5
  export declare class NotificationService<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> extends Transactional {
6
6
  #private;
7
- send(tenantId: string, userId: string, notification: TypedOmit<NewEntity<NotificationLog<Definitions>>, 'tenantId' | 'id' | 'userId' | 'status' | 'currentStep' | 'priority' | 'metadata'> & Partial<Pick<NotificationLog<Definitions>, 'priority'>>): Promise<void>;
7
+ send(tenantId: string, userId: string, notification: TypedOmit<NewEntity<NotificationLog<Definitions>>, 'tenantId' | 'id' | 'userId' | 'timestamp' | 'status' | 'currentStep' | 'priority'> & Partial<Pick<NotificationLog<Definitions>, 'priority'>>): Promise<void>;
8
8
  listInApp(tenantId: string, userId: string, options?: {
9
9
  limit?: number;
10
10
  offset?: number;
@@ -25,6 +25,7 @@ let NotificationService = class NotificationService extends Transactional {
25
25
  const notificationToInsert = {
26
26
  tenantId,
27
27
  userId,
28
+ timestamp: TRANSACTION_TIMESTAMP,
28
29
  priority: NotificationPriority.Medium,
29
30
  ...notification,
30
31
  status: NotificationStatus.Pending,
@@ -41,11 +42,11 @@ let NotificationService = class NotificationService extends Transactional {
41
42
  inApp: inAppNotification,
42
43
  })
43
44
  .from(notificationLog)
44
- .innerJoin(inAppNotification, eq(inAppNotification.logId, notificationLog.id))
45
- .where(and(eq(inAppNotification.tenantId, tenantId), eq(notificationLog.userId, userId), options.includeArchived ? undefined : isNull(inAppNotification.archiveTimestamp)))
45
+ .innerJoin(inAppNotification, and(eq(inAppNotification.tenantId, notificationLog.tenantId), eq(inAppNotification.userId, notificationLog.userId), eq(inAppNotification.logId, notificationLog.id)))
46
+ .where(and(eq(notificationLog.tenantId, tenantId), eq(notificationLog.userId, userId), options.includeArchived ? undefined : isNull(inAppNotification.archiveTimestamp)))
46
47
  .limit(options.limit ?? 50)
47
48
  .offset(options.offset ?? 0)
48
- .orderBy(desc(notificationLog.createTimestamp));
49
+ .orderBy(desc(notificationLog.timestamp));
49
50
  const inAppRows = rows.map((row) => row.inApp);
50
51
  const notificationRows = rows.map((row) => row.notification);
51
52
  const notificationEntities = await this.#notificationLogRepository.mapManyToEntity(notificationRows);
@@ -54,7 +55,8 @@ let NotificationService = class NotificationService extends Transactional {
54
55
  const views = notificationEntities.map((notification, index) => {
55
56
  const inApp = inAppEntities[index];
56
57
  const viewData = notificationViewDatas[index];
57
- return toInAppNotificationView(inApp, { ...notification, view: viewData });
58
+ const payload = { ...notification.payload, ...viewData };
59
+ return toInAppNotificationView(inApp, { ...notification, payload });
58
60
  });
59
61
  return views;
60
62
  }
@@ -86,6 +86,7 @@ describe('Notification Flow (Integration)', () => {
86
86
  await notificationService.send(tenantId, user.id, {
87
87
  type: 'test',
88
88
  priority: 'high',
89
+ triggerSubjectId: user.id,
89
90
  payload: { message: 'Hello', testField: 'Test Value' },
90
91
  });
91
92
  const logs = await logRepo.loadManyByQuery({ tenantId });
@@ -123,13 +124,13 @@ describe('Notification Flow (Integration)', () => {
123
124
  },
124
125
  });
125
126
  await notificationService.send(tenantId, user.id, {
126
- type: 'throttled', priority: 'medium', payload: {},
127
+ type: 'throttled', priority: 'medium', triggerSubjectId: user.id, payload: {},
127
128
  });
128
129
  await notificationService.send(tenantId, user.id, {
129
- type: 'throttled', priority: 'medium', payload: {},
130
+ type: 'throttled', priority: 'medium', triggerSubjectId: user.id, payload: {},
130
131
  });
131
132
  const logs = await logRepo.loadManyByQuery({ tenantId });
132
- logs.sort((a, b) => Number(a.metadata.createTimestamp) - Number(b.metadata.createTimestamp));
133
+ logs.sort((a, b) => Number(a.timestamp) - Number(b.timestamp));
133
134
  // First one should pass
134
135
  const result1 = await worker.deliver(logs[0].id);
135
136
  expect(result1.payload.action).toBe('complete'); // No escalations
@@ -149,7 +150,7 @@ describe('Notification Flow (Integration)', () => {
149
150
  },
150
151
  });
151
152
  await notificationService.send(tenantId, user.id, {
152
- type: 'readTest', priority: 'medium', payload: {},
153
+ type: 'readTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
153
154
  });
154
155
  const logs = await logRepo.loadManyByQuery({ tenantId });
155
156
  const log = logs[0];
@@ -176,7 +177,7 @@ describe('Notification Flow (Integration)', () => {
176
177
  await notificationService.updatePreference(tenantId, user.id, 'prefTest', NotificationChannel.InApp, false);
177
178
  await notificationService.updatePreference(tenantId, user.id, 'prefTest', NotificationChannel.Email, true);
178
179
  await notificationService.send(tenantId, user.id, {
179
- type: 'prefTest', priority: 'medium', payload: {},
180
+ type: 'prefTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
180
181
  });
181
182
  const logs = await logRepo.loadManyByQuery({ tenantId });
182
183
  const log = logs[0];
@@ -201,7 +202,7 @@ describe('Notification Flow (Integration)', () => {
201
202
  // Disable InApp so only WebPush is attempted
202
203
  await notificationService.updatePreference(tenantId, user.id, 'unknownTest', NotificationChannel.InApp, false);
203
204
  await notificationService.send(tenantId, user.id, {
204
- type: 'unknownTest', priority: 'medium', payload: {},
205
+ type: 'unknownTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
205
206
  });
206
207
  const logs = await logRepo.loadManyByQuery({ tenantId });
207
208
  const log = logs[0];
@@ -218,7 +219,7 @@ describe('Notification Flow (Integration)', () => {
218
219
  const user = await subjectService.createUser({ tenantId, email: 'manage@example.com', firstName: 'Manage', lastName: 'User' });
219
220
  await typeService.initializeTypes({ manageTest: { label: 'Manage Test' } });
220
221
  await notificationService.send(tenantId, user.id, {
221
- type: 'manageTest', priority: 'medium', payload: {},
222
+ type: 'manageTest', priority: 'medium', triggerSubjectId: user.id, payload: {},
222
223
  });
223
224
  const logs = await logRepo.loadManyByQuery({ tenantId });
224
225
  await worker.deliver(logs[0].id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.107",
3
+ "version": "0.93.109",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -101,8 +101,6 @@
101
101
  "./pool": "./pool/index.js",
102
102
  "./process": "./process/index.js",
103
103
  "./promise": "./promise/index.js",
104
- "./task-queue": "./task-queue/index.js",
105
- "./task-queue/postgres": "./task-queue/postgres/index.js",
106
104
  "./random": "./random/index.js",
107
105
  "./rate-limit": "./rate-limit/index.js",
108
106
  "./rate-limit/postgres": "./rate-limit/postgres/index.js",
@@ -120,6 +118,8 @@
120
118
  "./signals": "./signals/index.js",
121
119
  "./signals/implementation": "./signals/implementation/index.js",
122
120
  "./sse": "./sse/index.js",
121
+ "./task-queue": "./task-queue/index.js",
122
+ "./task-queue/postgres": "./task-queue/postgres/index.js",
123
123
  "./templates": "./templates/index.js",
124
124
  "./templates/providers": "./templates/providers/index.js",
125
125
  "./templates/renderers": "./templates/renderers/index.js",
@@ -128,6 +128,7 @@
128
128
  "./text": "./text/index.js",
129
129
  "./threading": "./threading/index.js",
130
130
  "./types": "./types/index.js",
131
+ "./typst": "./typst/index.js",
131
132
  "./unit-test": "./unit-test/index.js",
132
133
  "./utils": "./utils/index.js",
133
134
  "./utils/array": "./utils/array/index.js",
@@ -1,5 +1,5 @@
1
1
  import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2
- import type { Record } from '../types/types.js';
2
+ import type { Record, TypedOmit } from '../types/types.js';
3
3
  type WaitReadResultFormat = 'string' | 'binary';
4
4
  type WaitReadResultFormatType<T extends WaitReadResultFormat> = T extends 'string' ? string : Uint8Array<ArrayBuffer>;
5
5
  export type WaitOptions = {
@@ -35,9 +35,9 @@ export type SpawnCommandResult = TransformStream<Uint8Array, Uint8Array> & {
35
35
  export declare function spawnWaitCommand(command: string, args?: string[], options?: SpawnOptions & WaitOptions): Promise<WaitResult>;
36
36
  export declare function spawnWaitCommand(command: string, options?: SpawnOptions & WaitOptions): Promise<WaitResult>;
37
37
  /** spwans a command, waits for it to complete and reads its output */
38
- export declare function spawnWaitReadCommand<F extends WaitReadResultFormat>(format: F, command: string, args?: string[], options?: SpawnOptions & WaitOptions): Promise<WaitReadResult<F>>;
38
+ export declare function spawnWaitReadCommand<F extends WaitReadResultFormat>(format: F, command: string, args?: string[], options?: TypedOmit<SpawnOptions, 'arguments'> & WaitOptions): Promise<WaitReadResult<F>>;
39
39
  export declare function spawnWaitReadCommand<F extends WaitReadResultFormat>(format: F, command: string, options?: SpawnOptions & WaitOptions): Promise<WaitReadResult<F>>;
40
40
  /** Spawns a command as a child process. */
41
- export declare function spawnCommand(command: string, args?: string[], options?: SpawnOptions): Promise<SpawnCommandResult>;
41
+ export declare function spawnCommand(command: string, args?: string[], options?: TypedOmit<SpawnOptions, 'arguments'>): Promise<SpawnCommandResult>;
42
42
  export declare function spawnCommand(command: string, options?: SpawnOptions): Promise<SpawnCommandResult>;
43
43
  export {};
package/process/spawn.js CHANGED
@@ -10,7 +10,7 @@ export async function spawnWaitCommand(command, argsOrOptions, optionsOrNothing)
10
10
  return await process.wait({ throwOnNonZeroExitCode: options?.throwOnNonZeroExitCode });
11
11
  }
12
12
  export async function spawnWaitReadCommand(format, command, argsOrOptions, optionsOrNothing) {
13
- const [args, options] = isArray(argsOrOptions) ? [argsOrOptions, optionsOrNothing] : [undefined, argsOrOptions];
13
+ const [args, options] = isArray(argsOrOptions) ? [argsOrOptions, optionsOrNothing] : [argsOrOptions?.arguments, argsOrOptions];
14
14
  const process = await spawnCommand(command, args, options);
15
15
  return await process.waitRead(format, { throwOnNonZeroExitCode: options?.throwOnNonZeroExitCode });
16
16
  }
@@ -116,6 +116,7 @@ export declare class PostgresTaskQueue<Definitions extends TaskDefinitionMap = T
116
116
  resetState?: boolean;
117
117
  transaction?: Transaction;
118
118
  }): Promise<void>;
119
+ notify(): void;
119
120
  getConsumer<Type extends TaskTypes<Definitions>>(cancellationSignal: CancellationSignal, options?: {
120
121
  forceDequeue?: boolean;
121
122
  types?: Type[];
@@ -60,6 +60,7 @@ import { and, asc, count, eq, gt, gte, inArray, lt, lte, notInArray, or, sql, is
60
60
  import { merge } from 'rxjs';
61
61
  import { CancellationSignal } from '../../cancellation/index.js';
62
62
  import { CircuitBreaker, CircuitBreakerState } from '../../circuit-breaker/index.js';
63
+ import { serializeError } from '../../errors/index.js';
63
64
  import { afterResolve, inject, provide, Singleton } from '../../injector/index.js';
64
65
  import { Logger } from '../../logger/index.js';
65
66
  import { MessageBus } from '../../message-bus/index.js';
@@ -69,7 +70,6 @@ import { RateLimiter } from '../../rate-limit/index.js';
69
70
  import { createArray, distinct, toArray } from '../../utils/array/array.js';
70
71
  import { digest } from '../../utils/cryptography.js';
71
72
  import { currentTimestamp } from '../../utils/date-time.js';
72
- import { serializeError } from '../../errors/index.js';
73
73
  import { cancelableTimeout } from '../../utils/timing.js';
74
74
  import { isDefined, isNotNull, isNull, isString, isUndefined } from '../../utils/type-guards.js';
75
75
  import { millisecondsPerSecond } from '../../utils/units.js';
@@ -987,6 +987,9 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
987
987
  .where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id), or(eq(taskTable.status, TaskStatus.Pending), eq(taskTable.status, TaskStatus.Completed), eq(taskTable.status, TaskStatus.Cancelled), eq(taskTable.status, TaskStatus.Dead), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP))))
988
988
  .execute();
989
989
  }
990
+ notify() {
991
+ this.#messageBus.publishAndForget();
992
+ }
990
993
  async *getConsumer(cancellationSignal, options) {
991
994
  const continue$ = merge(this.#messageBus.allMessages$, cancellationSignal);
992
995
  while (cancellationSignal.isUnset) {
@@ -16,6 +16,7 @@ export declare class TaskContext<Definitions extends TaskDefinitionMap, Type ext
16
16
  get state(): TaskState<Definitions, Type> | null;
17
17
  get attempt(): number;
18
18
  get triesLeft(): number;
19
+ get isFinalAttempt(): boolean;
19
20
  get signal(): CancellationSignal;
20
21
  get logger(): Logger;
21
22
  complete(result?: TaskResult<Definitions, Type>, options?: {
@@ -42,6 +42,9 @@ export class TaskContext {
42
42
  get triesLeft() {
43
43
  return this.#queue.maxTries - this.#task.tries;
44
44
  }
45
+ get isFinalAttempt() {
46
+ return this.triesLeft <= 0;
47
+ }
45
48
  get signal() {
46
49
  return this.#signal.signal;
47
50
  }
@@ -285,6 +285,13 @@ export declare abstract class TaskQueue<Definitions extends TaskDefinitionMap =
285
285
  resetState?: boolean;
286
286
  transaction?: Transaction;
287
287
  }): Promise<void>;
288
+ /**
289
+ * Notifies the queue that new tasks might be available for processing.
290
+ * Used to wake up waiting consumers.
291
+ * Useful for unit tests.
292
+ * @internal
293
+ */
294
+ abstract notify(): void;
288
295
  abstract getConsumer<Type extends TaskTypes<Definitions>>(cancellationSignal: CancellationSignal, options?: {
289
296
  forceDequeue?: boolean;
290
297
  types?: Type[];
@@ -139,4 +139,33 @@ describe('Worker & Base Class Tests', () => {
139
139
  token.set();
140
140
  expect(executed).toBe(true);
141
141
  });
142
+ it('should correctly report isFinalAttempt in TaskContext', async () => {
143
+ const queueProvider = injector.resolve(TaskQueueProvider);
144
+ const queueName = `final-try-test-${Date.now()}`;
145
+ const testQueue = queueProvider.get(queueName, {
146
+ maxTries: 2,
147
+ retryDelayMinimum: 0,
148
+ retryDelayGrowth: 1,
149
+ });
150
+ await testQueue.enqueue('work', {});
151
+ testQueue.notify();
152
+ const token = new CancellationToken();
153
+ const finalAttemptValues = [];
154
+ testQueue.process({ cancellationSignal: token }, async (context) => {
155
+ finalAttemptValues.push(context.isFinalAttempt);
156
+ if (context.attempt === 1) {
157
+ throw new Error('fail first attempt');
158
+ }
159
+ return TaskProcessResult.Complete();
160
+ });
161
+ for (let i = 0; i < 100; i++) {
162
+ if (finalAttemptValues.length === 2)
163
+ break;
164
+ testQueue.notify();
165
+ await timeout(100);
166
+ }
167
+ token.set();
168
+ expect(finalAttemptValues).toEqual([false, true]);
169
+ await testQueue.clear();
170
+ });
142
171
  });
package/test5.d.ts CHANGED
@@ -1,13 +1 @@
1
1
  import './polyfills.js';
2
- import type { NotificationDefinitionMap } from './notification/index.js';
3
- export type Notifications = NotificationDefinitionMap<{
4
- 'analysis-started': {
5
- payload: {
6
- analysisId: string;
7
- };
8
- view: {
9
- name: string;
10
- };
11
- };
12
- 'analysis-completed': Record<never, never>;
13
- }>;
package/test5.js CHANGED
@@ -1,24 +1,20 @@
1
1
  import './polyfills.js';
2
+ import { writeFile } from 'node:fs/promises';
2
3
  import { Application } from './application/application.js';
3
4
  import { provideModule, provideSignalHandler } from './application/index.js';
4
- import { inject } from './injector/inject.js';
5
- import { JsonLogFormatter, PrettyPrintLogFormatter } from './logger/index.js';
5
+ import { PrettyPrintLogFormatter } from './logger/index.js';
6
6
  import { provideConsoleLogTransport } from './logger/transports/console.js';
7
- import { NotificationService } from './notification/server/index.js';
7
+ import { renderTypst } from './typst/render.js';
8
+ const template = `
9
+ I got an ice cream for
10
+ \\$1.50! \\u{1f600}
11
+ `;
8
12
  async function main(_cancellationSignal) {
9
- const notificationService = inject((NotificationService));
10
- const notificationApi = new null();
11
- const notification$ = (await notificationApi.stream());
12
- const tenantId = 'test-tenant';
13
- const userId = 'user-123';
14
- await notificationService.send(tenantId, userId, {
15
- type: 'analysis-started',
16
- payload: { analysisId: 'analysis-456' },
17
- });
13
+ const pdfBytes = await renderTypst(template, { format: 'docx' });
14
+ await writeFile('/tmp/output.docx', pdfBytes);
18
15
  }
19
16
  Application.run('Test', [
20
17
  provideConsoleLogTransport(PrettyPrintLogFormatter),
21
- provideConsoleLogTransport(JsonLogFormatter),
22
18
  provideModule(main),
23
19
  provideSignalHandler(),
24
20
  ]);
@@ -0,0 +1 @@
1
+ export * from './render.js';
package/typst/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './render.js';
@@ -0,0 +1,23 @@
1
+ export type TypstRenderOptions = {
2
+ /**
3
+ * The root directory for resolving imports in the Typst source. If not specified, imports will be resolved relative to the location of the source file.
4
+ */
5
+ root?: string;
6
+ /**
7
+ * The output format for the rendered document.
8
+ * Note: `docx` output uses pandoc under the hood, which does not support all Typst features.
9
+ * @default 'pdf'
10
+ */
11
+ format?: 'pdf' | 'png' | 'svg' | 'html' | 'docx';
12
+ };
13
+ /**
14
+ * Renders Typst source code to a file in the specified format.
15
+ *
16
+ * ## WARNING
17
+ * **This function should not be used with untrusted typst source, as it can lead to arbitrary code execution on the system.**
18
+ *
19
+ * Requires typst to be installed on the system.
20
+ * @param source
21
+ * @param options
22
+ */
23
+ export declare function renderTypst(source: string, options?: TypstRenderOptions): Promise<Uint8Array>;
@@ -0,0 +1,32 @@
1
+ import { spawnCommand } from '../process/spawn.js';
2
+ import { decodeText } from '../utils/encoding.js';
3
+ /**
4
+ * Renders Typst source code to a file in the specified format.
5
+ *
6
+ * ## WARNING
7
+ * **This function should not be used with untrusted typst source, as it can lead to arbitrary code execution on the system.**
8
+ *
9
+ * Requires typst to be installed on the system.
10
+ * @param source
11
+ * @param options
12
+ */
13
+ export async function renderTypst(source, options) {
14
+ const format = options?.format ?? 'pdf';
15
+ const command = (format == 'docx') ? 'pandoc' : 'typst';
16
+ const args = (format == 'docx')
17
+ ? ['--from', 'typst', '--to', 'docx', '--output', '-', '-']
18
+ : ['compile', '--format', format, '-', '-'];
19
+ const process = await spawnCommand(command, args);
20
+ const [{ code, output, error }] = await Promise.all([
21
+ process.waitRead('binary', { throwOnNonZeroExitCode: false }),
22
+ process.write(source),
23
+ ]);
24
+ const errorString = decodeText(error);
25
+ if (code !== 0) {
26
+ throw new Error(`
27
+ Typst compilation failed with exit code ${code}.\n
28
+ Error Output:\n${errorString}
29
+ `.trim());
30
+ }
31
+ return output;
32
+ }