@rsdk/nats.object-storage 5.4.0-next.10
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/CHANGELOG.md +46 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/inject-object-storage-service.decorator.d.ts +2 -0
- package/dist/inject-object-storage-service.decorator.js +19 -0
- package/dist/inject-object-storage-service.decorator.js.map +1 -0
- package/dist/nats-object-storage-module.generator.d.ts +5 -0
- package/dist/nats-object-storage-module.generator.js +30 -0
- package/dist/nats-object-storage-module.generator.js.map +1 -0
- package/dist/nats-object-storage.module.d.ts +9 -0
- package/dist/nats-object-storage.module.js +63 -0
- package/dist/nats-object-storage.module.js.map +1 -0
- package/dist/nats-object-storage.service.d.ts +40 -0
- package/dist/nats-object-storage.service.js +121 -0
- package/dist/nats-object-storage.service.js.map +1 -0
- package/dist/object-storage.d.ts +27 -0
- package/dist/object-storage.js +43 -0
- package/dist/object-storage.js.map +1 -0
- package/dist/patch.d.ts +1 -0
- package/dist/patch.js +239 -0
- package/dist/patch.js.map +1 -0
- package/dist/tokens.fn.d.ts +2 -0
- package/dist/tokens.fn.js +8 -0
- package/dist/tokens.fn.js.map +1 -0
- package/dist/watch-object-storage.decorator.d.ts +23 -0
- package/dist/watch-object-storage.decorator.js +19 -0
- package/dist/watch-object-storage.decorator.js.map +1 -0
- package/dist/watch-object-storage.service.d.ts +45 -0
- package/dist/watch-object-storage.service.js +112 -0
- package/dist/watch-object-storage.service.js.map +1 -0
- package/jest.config.js +1 -0
- package/jest.config.unit.js +1 -0
- package/package.json +42 -0
- package/src/constants.ts +1 -0
- package/src/index.ts +12 -0
- package/src/inject-object-storage-service.decorator.ts +45 -0
- package/src/nats-object-storage-module.generator.ts +52 -0
- package/src/nats-object-storage.module.ts +58 -0
- package/src/nats-object-storage.service.ts +141 -0
- package/src/object-storage.ts +53 -0
- package/src/patch.ts +292 -0
- package/src/tokens.fn.ts +10 -0
- package/src/watch-object-storage.decorator.ts +30 -0
- package/src/watch-object-storage.service.ts +129 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DynamicModule,
|
|
3
|
+
OnModuleDestroy,
|
|
4
|
+
OnModuleInit,
|
|
5
|
+
Provider,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { Module } from '@nestjs/common';
|
|
8
|
+
import { DiscoveryModule } from '@nestjs/core';
|
|
9
|
+
import {
|
|
10
|
+
createNatsClientToken,
|
|
11
|
+
NATS_CLIENT_INJECTION_TOKEN,
|
|
12
|
+
NATS_CLIENT_INJECTION_TOKEN_FOR_PACKAGE,
|
|
13
|
+
} from '@rsdk/nats.common';
|
|
14
|
+
|
|
15
|
+
import { NatsObjectStorageService } from './nats-object-storage.service';
|
|
16
|
+
import { createObjectStorageServiceToken } from './tokens.fn';
|
|
17
|
+
import { WatchObjectStorageService } from './watch-object-storage.service';
|
|
18
|
+
|
|
19
|
+
@Module({
|
|
20
|
+
imports: [DiscoveryModule],
|
|
21
|
+
providers: [NatsObjectStorageService, WatchObjectStorageService],
|
|
22
|
+
})
|
|
23
|
+
export class NatsObjectStorageModule implements OnModuleInit, OnModuleDestroy {
|
|
24
|
+
constructor(private readonly watchService: WatchObjectStorageService) {}
|
|
25
|
+
|
|
26
|
+
public static forFeature(connectionName?: string): DynamicModule {
|
|
27
|
+
const clientProvider: Provider = {
|
|
28
|
+
provide: createObjectStorageServiceToken(connectionName),
|
|
29
|
+
useClass: NatsObjectStorageService,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
module: NatsObjectStorageModule,
|
|
34
|
+
providers: [
|
|
35
|
+
{
|
|
36
|
+
provide: NATS_CLIENT_INJECTION_TOKEN_FOR_PACKAGE,
|
|
37
|
+
useExisting: connectionName
|
|
38
|
+
? createNatsClientToken(connectionName)
|
|
39
|
+
: NATS_CLIENT_INJECTION_TOKEN,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
provide: 'CONNECTION_NAME',
|
|
43
|
+
useFactory: () => connectionName,
|
|
44
|
+
},
|
|
45
|
+
clientProvider,
|
|
46
|
+
],
|
|
47
|
+
exports: [clientProvider],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async onModuleInit(): Promise<void> {
|
|
52
|
+
await this.watchService?.runWatches();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async onModuleDestroy(): Promise<void> {
|
|
56
|
+
await this.watchService?.stopWatches();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
import { delay, Timespan } from '@rsdk/common';
|
|
3
|
+
import { InjectLogger, InternalException, NotFoundException } from '@rsdk/core';
|
|
4
|
+
import { ILogger } from '@rsdk/logging';
|
|
5
|
+
import { NATS_CLIENT_INJECTION_TOKEN_FOR_PACKAGE } from '@rsdk/nats.common';
|
|
6
|
+
import { NatsConnection } from 'nats';
|
|
7
|
+
import type { ObjectStoreOptions } from 'nats/lib/jetstream/types';
|
|
8
|
+
|
|
9
|
+
import { ObjectStorage } from './object-storage';
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class NatsObjectStorageService {
|
|
13
|
+
constructor(
|
|
14
|
+
@InjectLogger(NatsObjectStorageService)
|
|
15
|
+
private readonly logger: ILogger,
|
|
16
|
+
@Inject(NATS_CLIENT_INJECTION_TOKEN_FOR_PACKAGE)
|
|
17
|
+
private readonly nats: NatsConnection,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get or create object storage
|
|
22
|
+
*/
|
|
23
|
+
public async create({
|
|
24
|
+
bucketName,
|
|
25
|
+
domain,
|
|
26
|
+
...opts
|
|
27
|
+
}: Partial<ObjectStoreOptions> & {
|
|
28
|
+
bucketName: string;
|
|
29
|
+
domain?: string;
|
|
30
|
+
}): Promise<ObjectStorage> {
|
|
31
|
+
const jsOptions = domain ? { domain } : undefined;
|
|
32
|
+
const js = this.nats.jetstream(jsOptions);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const storage = await js.views.os(bucketName, opts);
|
|
36
|
+
|
|
37
|
+
this.logger.trace('Object storage initialized', {
|
|
38
|
+
connectionName: this.nats.info,
|
|
39
|
+
bucket: bucketName,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return new ObjectStorage(storage);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
this.logger.error('Failed to get or create object storage', {
|
|
45
|
+
err: error,
|
|
46
|
+
bucket: bucketName,
|
|
47
|
+
});
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get object storage
|
|
54
|
+
*/
|
|
55
|
+
public async bind({
|
|
56
|
+
bucketName,
|
|
57
|
+
domain,
|
|
58
|
+
...opts
|
|
59
|
+
}: Partial<ObjectStoreOptions> & {
|
|
60
|
+
bucketName: string;
|
|
61
|
+
domain?: string;
|
|
62
|
+
}): Promise<ObjectStorage> {
|
|
63
|
+
const jsOptions = domain ? { domain } : undefined;
|
|
64
|
+
const js = this.nats.jetstream(jsOptions);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
if (!(await this.isBucketExists(bucketName, jsOptions))) {
|
|
68
|
+
throw new NotFoundException(
|
|
69
|
+
`Object storage bucket "${bucketName}" not found`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const storage = await js.views.os(bucketName, opts);
|
|
74
|
+
|
|
75
|
+
this.logger.trace('Object storage bound', {
|
|
76
|
+
connectionName: this.nats.info,
|
|
77
|
+
bucket: bucketName,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return new ObjectStorage(storage);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
this.logger.error('Failed to get object storage', {
|
|
83
|
+
err: error,
|
|
84
|
+
bucket: bucketName,
|
|
85
|
+
});
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Waits for the specified bucket to exist.
|
|
92
|
+
*
|
|
93
|
+
* @param bucketName - The name of the bucket to wait for.
|
|
94
|
+
* @param timeout - The maximum time to wait for the bucket to exist, in milliseconds. Defaults to infinity.
|
|
95
|
+
* @throws {InternalException} If the bucket does not exist within the specified timeout.
|
|
96
|
+
*/
|
|
97
|
+
async waitForBucketExists(
|
|
98
|
+
bucketName: string,
|
|
99
|
+
timeout: number = Number.POSITIVE_INFINITY,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
const start = Date.now();
|
|
102
|
+
|
|
103
|
+
while (true) {
|
|
104
|
+
if (await this.isBucketExists(bucketName)) {
|
|
105
|
+
this.logger.trace('Object storage bucket exists');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (timeout && Date.now() - start > timeout) {
|
|
110
|
+
throw new InternalException(`Bucket ${bucketName} does not exist`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.logger.trace(
|
|
114
|
+
'Object storage bucket does not exist, retry after 3 seconds...',
|
|
115
|
+
{
|
|
116
|
+
bucketName,
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
await delay(Timespan.second(3));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Checks if a bucket exists.
|
|
125
|
+
*
|
|
126
|
+
* @param bucket - The name of the bucket to check.
|
|
127
|
+
* @returns A promise that resolves to a boolean indicating whether the bucket exists or not.
|
|
128
|
+
*/
|
|
129
|
+
async isBucketExists(
|
|
130
|
+
bucket: string,
|
|
131
|
+
opts?: { domain?: string },
|
|
132
|
+
): Promise<boolean> {
|
|
133
|
+
try {
|
|
134
|
+
await this.nats.jetstream(opts).streams.get(`OBJ_${bucket}`);
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ObjectInfo,
|
|
3
|
+
ObjectResult,
|
|
4
|
+
ObjectStore,
|
|
5
|
+
ObjectStoreMeta,
|
|
6
|
+
ObjectStorePutOpts,
|
|
7
|
+
PurgeResponse,
|
|
8
|
+
} from 'nats';
|
|
9
|
+
|
|
10
|
+
export class ObjectStorage {
|
|
11
|
+
constructor(private readonly storage: ObjectStore) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Возвращает метаданные и стрим с объектом
|
|
15
|
+
*/
|
|
16
|
+
public get(key: string): Promise<ObjectResult | null> {
|
|
17
|
+
return this.storage.get(key);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Удаляет объект
|
|
22
|
+
*/
|
|
23
|
+
public delete(key: string): Promise<PurgeResponse> {
|
|
24
|
+
return this.storage.delete(key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Загружает объект в хранилище
|
|
29
|
+
*/
|
|
30
|
+
public put(
|
|
31
|
+
meta: ObjectStoreMeta,
|
|
32
|
+
rs: ReadableStream<Uint8Array>,
|
|
33
|
+
opts?: ObjectStorePutOpts,
|
|
34
|
+
): Promise<ObjectInfo> {
|
|
35
|
+
return this.storage.put(meta, rs, opts);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Возвращает массив всех объектов в хранилище
|
|
40
|
+
*/
|
|
41
|
+
public list(): Promise<ObjectInfo[]> {
|
|
42
|
+
return this.storage.list();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Возвращает сырой object storage Nats. Используйте этот метод
|
|
47
|
+
* только в крайнем случае! Рекомендуется работать с оберткой
|
|
48
|
+
* для безопасного взаимодействия.
|
|
49
|
+
*/
|
|
50
|
+
public getRawStorage(): ObjectStore {
|
|
51
|
+
return this.storage;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/patch.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
|
+
// @ts-nocheck issue - https://github.com/nats-io/nats.js/issues/155
|
|
3
|
+
import type {
|
|
4
|
+
MsgHdrs,
|
|
5
|
+
ObjectInfo,
|
|
6
|
+
ObjectResult,
|
|
7
|
+
ObjectStoreMetaOptions,
|
|
8
|
+
} from 'nats';
|
|
9
|
+
import { AckPolicy, consumerOpts, deferred, MsgHdrsImpl } from 'nats';
|
|
10
|
+
import type { ServerObjectInfo } from 'nats/lib/jetstream/objectstore';
|
|
11
|
+
import { digestType, ObjectStoreImpl } from 'nats/lib/jetstream/objectstore';
|
|
12
|
+
import { JSONCodec } from 'nats/lib/nats-base-client/codec';
|
|
13
|
+
import { QueuedIteratorImpl } from 'nats/lib/nats-base-client/queued_iterator';
|
|
14
|
+
import { SHA256 } from 'nats/lib/nats-base-client/sha256';
|
|
15
|
+
|
|
16
|
+
function emptyReadableStream(): ReadableStream {
|
|
17
|
+
return new ReadableStream({
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
19
|
+
pull(c) {
|
|
20
|
+
c.enqueue(new Uint8Array(0));
|
|
21
|
+
c.close();
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class ObjectInfoImpl implements ObjectInfo {
|
|
27
|
+
info: ServerObjectInfo;
|
|
28
|
+
hdrs!: MsgHdrs;
|
|
29
|
+
constructor(oi: ServerObjectInfo) {
|
|
30
|
+
this.info = oi;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get name(): string {
|
|
34
|
+
return this.info.name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get description(): string {
|
|
38
|
+
return this.info.description ?? '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get headers(): MsgHdrs {
|
|
42
|
+
if (!this.hdrs) {
|
|
43
|
+
this.hdrs = MsgHdrsImpl.fromRecord(this.info.headers || {});
|
|
44
|
+
}
|
|
45
|
+
return this.hdrs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get options(): ObjectStoreMetaOptions | undefined {
|
|
49
|
+
return this.info.options;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get bucket(): string {
|
|
53
|
+
return this.info.bucket;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get chunks(): number {
|
|
57
|
+
return this.info.chunks;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get deleted(): boolean {
|
|
61
|
+
return this.info.deleted ?? false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get digest(): string {
|
|
65
|
+
return this.info.digest;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get mtime(): string {
|
|
69
|
+
return this.info.mtime;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get nuid(): string {
|
|
73
|
+
return this.info.nuid;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get size(): number {
|
|
77
|
+
return this.info.size;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get revision(): number {
|
|
81
|
+
return this.info.revision;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get metadata(): Record<string, string> {
|
|
85
|
+
return this.info.metadata || {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
89
|
+
isLink() {
|
|
90
|
+
return (
|
|
91
|
+
this.info.options?.link !== undefined && this.info.options?.link !== null
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ObjectStoreImpl.prototype.get = async function get(
|
|
97
|
+
name: string,
|
|
98
|
+
): Promise<ObjectResult | null> {
|
|
99
|
+
const info = await this.rawInfo(name);
|
|
100
|
+
if (info === null) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (info.deleted) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (info.options && info.options.link) {
|
|
109
|
+
const ln = info.options.link.name || ' ';
|
|
110
|
+
if (ln === '') {
|
|
111
|
+
throw new Error('link is a bucket');
|
|
112
|
+
}
|
|
113
|
+
const os =
|
|
114
|
+
info.options.link.bucket === this.name
|
|
115
|
+
? this
|
|
116
|
+
: await ObjectStoreImpl.create(this.js, info.options.link.bucket);
|
|
117
|
+
|
|
118
|
+
return os.get(ln);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const d = deferred<Error | null>();
|
|
122
|
+
|
|
123
|
+
const r: Partial<ObjectResult> = {
|
|
124
|
+
info: new ObjectInfoImpl(info),
|
|
125
|
+
error: d,
|
|
126
|
+
};
|
|
127
|
+
if (info.size === 0) {
|
|
128
|
+
r.data = emptyReadableStream();
|
|
129
|
+
d.resolve(null);
|
|
130
|
+
return r as ObjectResult;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let controller: ReadableStreamDefaultController;
|
|
134
|
+
|
|
135
|
+
const oc = consumerOpts();
|
|
136
|
+
|
|
137
|
+
oc.orderedConsumer();
|
|
138
|
+
const sha = new SHA256();
|
|
139
|
+
const subj = `$O.${this.name}.C.${info.nuid}`;
|
|
140
|
+
let sub;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// eslint-disable-next-line deprecation/deprecation
|
|
144
|
+
sub = await this.js.subscribe(subj, oc);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error.message !== 'no stream matches subject') {
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
const oc = consumerOpts({
|
|
150
|
+
ack_policy: AckPolicy.None,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
oc.orderedConsumer();
|
|
154
|
+
oc.bindStream('OBJ_' + this.name);
|
|
155
|
+
|
|
156
|
+
// HACK: for mirrored ObjectStore
|
|
157
|
+
// eslint-disable-next-line deprecation/deprecation
|
|
158
|
+
sub = await this.js.subscribe(subj, oc);
|
|
159
|
+
}
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
161
|
+
(async () => {
|
|
162
|
+
for await (const jm of sub) {
|
|
163
|
+
if (jm.data.length > 0) {
|
|
164
|
+
sha.update(jm.data);
|
|
165
|
+
controller!.enqueue(jm.data);
|
|
166
|
+
}
|
|
167
|
+
if (jm.info.pending === 0) {
|
|
168
|
+
const hash = sha.digest('base64');
|
|
169
|
+
|
|
170
|
+
// go pads the hash - which should be multiple of 3 - otherwise pads with '='
|
|
171
|
+
const pad = hash.length % 3;
|
|
172
|
+
const padding = pad > 0 ? '='.repeat(pad) : '';
|
|
173
|
+
const digest = `${digestType}${hash}${padding}`;
|
|
174
|
+
if (digest === info.digest) {
|
|
175
|
+
controller!.close();
|
|
176
|
+
} else {
|
|
177
|
+
controller!.error(
|
|
178
|
+
new Error(
|
|
179
|
+
`received a corrupt object, digests do not match received: ${info.digest} calculated ${digest}`,
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
sub.unsubscribe();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
})()
|
|
187
|
+
.then(() => {
|
|
188
|
+
d.resolve();
|
|
189
|
+
})
|
|
190
|
+
.catch((error) => {
|
|
191
|
+
controller!.error(error);
|
|
192
|
+
d.reject(error);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
r.data = new ReadableStream({
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
197
|
+
start(c) {
|
|
198
|
+
controller = c;
|
|
199
|
+
},
|
|
200
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
201
|
+
cancel() {
|
|
202
|
+
sub.unsubscribe();
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return r as ObjectResult;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
ObjectStoreImpl.prototype.watch = async function watch(
|
|
210
|
+
opts: Partial<{
|
|
211
|
+
ignoreDeletes?: boolean;
|
|
212
|
+
includeHistory?: boolean;
|
|
213
|
+
}> = {},
|
|
214
|
+
): Promise<QueuedIterator<ObjectInfo | null>> {
|
|
215
|
+
opts.includeHistory = opts.includeHistory ?? false;
|
|
216
|
+
opts.ignoreDeletes = opts.ignoreDeletes ?? false;
|
|
217
|
+
let initialized = false;
|
|
218
|
+
const qi = new QueuedIteratorImpl<ObjectInfo | null>();
|
|
219
|
+
const subj = this._metaSubjectAll();
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
await this.jsm.streams.getMessage(this.stream, { last_by_subj: subj });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error.code === '404') {
|
|
225
|
+
qi.push(null);
|
|
226
|
+
initialized = true;
|
|
227
|
+
} else {
|
|
228
|
+
qi.stop(error);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const jc = JSONCodec<ObjectInfo>();
|
|
232
|
+
const copts = consumerOpts();
|
|
233
|
+
|
|
234
|
+
copts.orderedConsumer();
|
|
235
|
+
if (opts.includeHistory) {
|
|
236
|
+
copts.deliverLastPerSubject();
|
|
237
|
+
} else {
|
|
238
|
+
// FIXME: Go's implementation doesn't seem correct - if history is not desired
|
|
239
|
+
// the watch should only be giving notifications on new entries
|
|
240
|
+
initialized = true;
|
|
241
|
+
copts.deliverNew();
|
|
242
|
+
}
|
|
243
|
+
copts.callback((err: NatsError | null, jm: JsMsg | null) => {
|
|
244
|
+
if (err) {
|
|
245
|
+
qi.stop(err);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (jm !== null) {
|
|
249
|
+
const oi = jc.decode(jm.data);
|
|
250
|
+
if (oi.deleted && opts.ignoreDeletes === true) {
|
|
251
|
+
// do nothing
|
|
252
|
+
} else {
|
|
253
|
+
qi.push(oi);
|
|
254
|
+
}
|
|
255
|
+
if (jm.info?.pending === 0 && !initialized) {
|
|
256
|
+
initialized = true;
|
|
257
|
+
qi.push(null);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
let sub;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// eslint-disable-next-line deprecation/deprecation
|
|
266
|
+
sub = await this.js.subscribe(subj, copts);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error.message !== 'no stream matches subject') {
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
copts.bindStream('OBJ_' + this.name);
|
|
273
|
+
|
|
274
|
+
// HACK: for mirrored ObjectStore
|
|
275
|
+
// eslint-disable-next-line deprecation/deprecation
|
|
276
|
+
sub = await this.js.subscribe(subj, copts);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
qi._data = sub;
|
|
280
|
+
qi.iterClosed.then(() => {
|
|
281
|
+
sub.unsubscribe();
|
|
282
|
+
});
|
|
283
|
+
sub.closed
|
|
284
|
+
.then(() => {
|
|
285
|
+
qi.stop();
|
|
286
|
+
})
|
|
287
|
+
.catch((error) => {
|
|
288
|
+
qi.stop(error);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return qi;
|
|
292
|
+
};
|
package/src/tokens.fn.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const createObjectStorageServiceToken = (
|
|
2
|
+
connectionName?: string,
|
|
3
|
+
): string =>
|
|
4
|
+
`NATS_OBJECT_STORAGE_SERVICE${connectionName ? `_${connectionName}` : ''}`;
|
|
5
|
+
|
|
6
|
+
export const createObjectStorageToken = (
|
|
7
|
+
bucketName: string,
|
|
8
|
+
connectionName?: string,
|
|
9
|
+
): string =>
|
|
10
|
+
`NATS_OBJECT_STORAGE_${bucketName}${connectionName ? `_${connectionName}` : ''}`;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Reflector } from '@nestjs/core';
|
|
2
|
+
import type { ObjectInfo } from 'nats';
|
|
3
|
+
|
|
4
|
+
export type WatchConfig = {
|
|
5
|
+
bucket: string;
|
|
6
|
+
ignoreDeletes?: boolean;
|
|
7
|
+
includeHistory?: boolean;
|
|
8
|
+
connectionName?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface ObjectStorageWatcher {
|
|
12
|
+
onDelete(entry: ObjectInfo | null): Promise<void>;
|
|
13
|
+
onPut(entry: ObjectInfo | null): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Key used to mark class as a watcher for object storage.
|
|
18
|
+
*/
|
|
19
|
+
export const WATCH_NATS_OBJECT_STORAGE_KEY = 'WATCH_NATS_OBJECT_STORAGE';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Decorator used to mark class as a watcher for object storage.
|
|
23
|
+
* Needs to be used in conjunction with the `ObjectStorageWatcher` interface.
|
|
24
|
+
*
|
|
25
|
+
* @param {WatchConfig} config - The configuration for watching object storage.
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
export const WatchObjectStorage = Reflector.createDecorator<WatchConfig>({
|
|
29
|
+
key: WATCH_NATS_OBJECT_STORAGE_KEY,
|
|
30
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Inject } from '@nestjs/common';
|
|
2
|
+
import { DiscoveryService } from '@nestjs/core';
|
|
3
|
+
import { InjectLogger } from '@rsdk/core';
|
|
4
|
+
import { ILogger } from '@rsdk/logging';
|
|
5
|
+
import type { ObjectInfo, ObjectStore } from 'nats';
|
|
6
|
+
|
|
7
|
+
import { NatsObjectStorageService } from './nats-object-storage.service';
|
|
8
|
+
import type {
|
|
9
|
+
ObjectStorageWatcher,
|
|
10
|
+
WatchConfig,
|
|
11
|
+
} from './watch-object-storage.decorator';
|
|
12
|
+
import { WatchObjectStorage } from './watch-object-storage.decorator';
|
|
13
|
+
|
|
14
|
+
export class WatchObjectStorageService {
|
|
15
|
+
constructor(
|
|
16
|
+
@InjectLogger(WatchObjectStorageService)
|
|
17
|
+
private readonly logger: ILogger,
|
|
18
|
+
private readonly discovery: DiscoveryService,
|
|
19
|
+
private readonly client: NatsObjectStorageService,
|
|
20
|
+
@Inject('CONNECTION_NAME')
|
|
21
|
+
private readonly connectionName?: string,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Runs the watches for the object storage service.
|
|
26
|
+
* Waits for buckets to exist and starts the watch operation for each bucket.
|
|
27
|
+
*/
|
|
28
|
+
public async runWatches(): Promise<void> {
|
|
29
|
+
const watchers = await this.discoverWatchers();
|
|
30
|
+
|
|
31
|
+
await Promise.all(
|
|
32
|
+
watchers.map(async ({ instance, metadata }) => {
|
|
33
|
+
await this.client.waitForBucketExists(metadata.bucket, 5000);
|
|
34
|
+
await this.runWatch(metadata, instance);
|
|
35
|
+
this.logger.info('Nats object storage watcher started', { metadata });
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Stops all watches.
|
|
42
|
+
*
|
|
43
|
+
* @note This method is not implemented yet.
|
|
44
|
+
*/
|
|
45
|
+
public async stopWatches(): Promise<void> {
|
|
46
|
+
// TODO: Implement
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Register watcher callback for object storage
|
|
51
|
+
*/
|
|
52
|
+
public async watch(
|
|
53
|
+
storage: ObjectStore,
|
|
54
|
+
callback: (entry: ObjectInfo | null) => Promise<void>,
|
|
55
|
+
opts?: {
|
|
56
|
+
ignoreDeletes?: boolean;
|
|
57
|
+
includeHistory?: boolean;
|
|
58
|
+
},
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
for await (const entry of await storage.watch(opts)) {
|
|
61
|
+
callback(entry).catch((error) => {
|
|
62
|
+
this.logger.error('Error processing object storage entry', {
|
|
63
|
+
entry: entry,
|
|
64
|
+
err: error,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Runs the watch operation for the given metadata and watcher.
|
|
72
|
+
*
|
|
73
|
+
* @param config - The watch configuration.
|
|
74
|
+
* @param watcher - The ObjectStorageWatcher instance.
|
|
75
|
+
* @returns A Promise that resolves when the watch operation is complete.
|
|
76
|
+
*/
|
|
77
|
+
private async runWatch(
|
|
78
|
+
{ bucket, ...options }: WatchConfig,
|
|
79
|
+
watcher: ObjectStorageWatcher,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
const storage = await this.client.bind({ bucketName: bucket });
|
|
82
|
+
|
|
83
|
+
this.watch(
|
|
84
|
+
storage.getRawStorage(),
|
|
85
|
+
async (event) => {
|
|
86
|
+
if (!event) return;
|
|
87
|
+
|
|
88
|
+
await (event.deleted ? watcher.onDelete(event) : watcher.onPut(event));
|
|
89
|
+
},
|
|
90
|
+
options,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Discovers and returns an array of ObjectStorageWatcher instances
|
|
96
|
+
* along with their corresponding metadata.
|
|
97
|
+
*
|
|
98
|
+
* @returns An array of objects containing the ObjectStorageWatcher
|
|
99
|
+
* instance and its metadata.
|
|
100
|
+
*/
|
|
101
|
+
private discoverWatchers(): {
|
|
102
|
+
instance: ObjectStorageWatcher;
|
|
103
|
+
metadata: WatchConfig;
|
|
104
|
+
}[] {
|
|
105
|
+
return this.discovery.getProviders().reduce(
|
|
106
|
+
(acc, watcher) => {
|
|
107
|
+
/**
|
|
108
|
+
* NOTE: This is a fix for TypeError in call .getMetadataByDecorator() above
|
|
109
|
+
* because some tokens are not instances of classes and have no constructor.
|
|
110
|
+
* */
|
|
111
|
+
const clsRef = watcher.instance?.constructor ?? watcher.metatype;
|
|
112
|
+
if (!clsRef) {
|
|
113
|
+
return acc;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const metadata = this.discovery.getMetadataByDecorator(
|
|
117
|
+
WatchObjectStorage,
|
|
118
|
+
watcher,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (metadata && metadata.connectionName === this.connectionName) {
|
|
122
|
+
acc.push({ instance: watcher.instance, metadata });
|
|
123
|
+
}
|
|
124
|
+
return acc;
|
|
125
|
+
},
|
|
126
|
+
[] as { instance: ObjectStorageWatcher; metadata: WatchConfig }[],
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|