tapeworm_persistence_store_hybrid 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ import BluebirdPromise from 'bluebird';
2
+ import { Commit } from 'tapeworm';
3
+ import { LoggingOptions, Partition, queryStreamCallback, truncateStreamFromCallback } from './HybridPersistence';
4
+ import { Snapshot } from './utils';
5
+ declare class HybridPartition implements Partition {
6
+ private localPartition;
7
+ private remotePartition;
8
+ private snapshotLifeTime;
9
+ private loggingOptions;
10
+ constructor(localPartition: Partition, remotePartition: Partition, autoCleanLocalPartition: boolean, snapshotLifeTime: number, loggingOptions: LoggingOptions);
11
+ private cleanPartition;
12
+ open(): BluebirdPromise<this>;
13
+ append(commit: Commit<Record<string, any>>): BluebirdPromise<import("./utils").Commit<Record<string, any>>[]>;
14
+ storeSnapshot(streamId: string, snapshot: Snapshot, version: number): BluebirdPromise<void>;
15
+ private checkIsSnapshotToOld;
16
+ loadSnapshot(streamId: string, callback?: (err: Error, snapshot: Snapshot) => void): BluebirdPromise<Snapshot>;
17
+ queryStream(streamId: string, fromEventSequence?: number | queryStreamCallback, callback?: queryStreamCallback): BluebirdPromise<any[]>;
18
+ removeSnapshot(streamId: string): BluebirdPromise<void>;
19
+ truncateStreamFrom(streamId: string, commit: Commit<Record<string, any>>, remove: boolean | truncateStreamFromCallback, callback?: truncateStreamFromCallback): BluebirdPromise<void>;
20
+ applyCommitHeader(streamId: string, commit: Commit<Record<string, any>>, remove: any, callback: () => void): BluebirdPromise<void>;
21
+ }
22
+ export default HybridPartition;
@@ -0,0 +1,38 @@
1
+ import BluebirdPromise from 'bluebird';
2
+ import { Commit, Snapshot } from './utils';
3
+ export declare type truncateStreamFromCallback = (error: Error, result: Record<string, any>) => void;
4
+ declare type TruncateStreamFrom = (streamId: string, commitSequence: number, remove: boolean | truncateStreamFromCallback, callback?: truncateStreamFromCallback) => BluebirdPromise<void>;
5
+ export declare type queryStreamCallback = (err: Error, steam: Commit<Record<string, any>>[]) => void;
6
+ declare type QueryStream = (streamId: string, fromEventSequence?: number | queryStreamCallback, callback?: queryStreamCallback) => BluebirdPromise<Commit<Record<string, any>>[]>;
7
+ export declare type Partition = {
8
+ open: () => BluebirdPromise<Partition>;
9
+ storeSnapshot: (streamId: string, snapshot: Snapshot['snapshot'], version: number) => BluebirdPromise<void>;
10
+ loadSnapshot: (streamId: string, callback?: (err: Error, snapshot: Snapshot | undefined) => void) => BluebirdPromise<Snapshot | undefined>;
11
+ queryStream: QueryStream;
12
+ append: (commit: Commit<Record<string, any>>) => BluebirdPromise<Commit<Record<string, any>>[]>;
13
+ removeSnapshot: (streamId: string) => BluebirdPromise<void>;
14
+ removeSnapshots?: (streamIds: string[]) => BluebirdPromise<void>;
15
+ truncateStreamFrom: TruncateStreamFrom;
16
+ applyCommitHeader: (streamId: string, commit: Commit<Record<string, any>>, remove: any, callback: () => void) => BluebirdPromise<void>;
17
+ querySnapshotsOlderThanMaxDate?: (dateTime: string) => BluebirdPromise<Snapshot[]>;
18
+ };
19
+ export interface LoggingOptions {
20
+ loggingEnabled: boolean;
21
+ loadSnapshotMaxTime?: number;
22
+ }
23
+ export declare type Persistence = {
24
+ openPartition: (partitionId?: string) => BluebirdPromise<Partition>;
25
+ };
26
+ declare class HybridPersistence {
27
+ private localPersistence;
28
+ private remotePersistence;
29
+ private autoCleanLocalPartition;
30
+ private snapshotLifeTime;
31
+ private loggingOptions;
32
+ private partitions;
33
+ constructor(localPersistence: Persistence, remotePersistence: Persistence, autoCleanLocalPartition: boolean, snapshotLifeTime: number, loggingOptions: LoggingOptions);
34
+ openPartition(partitionId?: string): BluebirdPromise<unknown>;
35
+ private getPartition;
36
+ private setPartition;
37
+ }
38
+ export default HybridPersistence;
@@ -0,0 +1,3 @@
1
+ import HybridPartition from './HybridPartition';
2
+ import HybridPersistence from './HybridPersistence';
3
+ export { HybridPartition, HybridPersistence };
@@ -0,0 +1,24 @@
1
+ interface Event<Payload = Record<string, any>> {
2
+ type: string;
3
+ aggregateId: string;
4
+ correlationId: string;
5
+ payload: Payload;
6
+ id?: string;
7
+ dateTime?: string;
8
+ headers?: Record<string, any>;
9
+ version?: number;
10
+ }
11
+ export interface Commit<Payload> {
12
+ id: string;
13
+ partitionId: string;
14
+ streamId: string;
15
+ commitSequence: number;
16
+ events: Event<Payload>[];
17
+ }
18
+ export interface Snapshot {
19
+ id: string;
20
+ version: number;
21
+ storedDateTime?: string;
22
+ snapshot?: Record<string, any>;
23
+ }
24
+ export {};
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "tapeworm_persistence_store_hybrid",
3
- "version": "1.0.1",
4
- "description": "",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "prettier": "prettier-config-surikaterna",
8
- "scripts": {
9
- "test": "jest",
10
- "prepare": "npm test && npm run build",
11
- "build": "tsc"
12
- },
13
- "repository": {
14
- "type": "git",
15
- "url": "git+https://github.com/surikaterna/tapeworm_persistence_store_hybrid.git"
16
- },
17
- "author": "Gustav Albertsson",
18
- "license": "ISC",
19
- "bugs": {
20
- "url": "https://github.com/surikaterna/tapeworm_persistence_store_hybrid/issues"
21
- },
22
- "homepage": "https://github.com/surikaterna/tapeworm_persistence_store_hybrid#readme",
23
- "devDependencies": {
24
- "@types/bluebird": "^3.5.36",
25
- "@types/jest": "^27.0.1",
26
- "fake-indexeddb": "^3.1.3",
27
- "jest": "^27.3.1",
28
- "prettier": "^2.3.2",
29
- "prettier-config-surikaterna": "^1.0.1",
30
- "tapeworm": "^0.3.5",
31
- "tapeworm_persistence_store_indexeddb": "^0.2.6",
32
- "ts-jest": "^27.0.5",
33
- "typescript": "^4.4.2"
34
- },
35
- "dependencies": {
36
- "bluebird": "^3.7.2",
37
- "slf": "^1.0.3"
38
- }
39
- }
1
+ {
2
+ "name": "tapeworm_persistence_store_hybrid",
3
+ "version": "1.0.2",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "prettier": "prettier-config-surikaterna",
8
+ "scripts": {
9
+ "test": "jest",
10
+ "prepare": "npm test && npm run build",
11
+ "build": "tsc"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/surikaterna/tapeworm_persistence_store_hybrid.git"
16
+ },
17
+ "author": "Gustav Albertsson",
18
+ "license": "ISC",
19
+ "bugs": {
20
+ "url": "https://github.com/surikaterna/tapeworm_persistence_store_hybrid/issues"
21
+ },
22
+ "homepage": "https://github.com/surikaterna/tapeworm_persistence_store_hybrid#readme",
23
+ "devDependencies": {
24
+ "@types/bluebird": "^3.5.36",
25
+ "@types/jest": "^27.0.1",
26
+ "fake-indexeddb": "^3.1.3",
27
+ "jest": "^27.3.1",
28
+ "prettier": "^2.3.2",
29
+ "prettier-config-surikaterna": "^1.0.1",
30
+ "tapeworm": "^0.3.5",
31
+ "tapeworm_persistence_store_indexeddb": "^0.2.6",
32
+ "ts-jest": "^27.0.5",
33
+ "typescript": "^4.4.2"
34
+ },
35
+ "dependencies": {
36
+ "bluebird": "^3.7.2",
37
+ "slf": "^1.0.3"
38
+ }
39
+ }
@@ -0,0 +1,162 @@
1
+ import BluebirdPromise from 'bluebird';
2
+ // @ts-ignore
3
+ import { Commit } from 'tapeworm';
4
+ import { LoggingOptions, Partition, queryStreamCallback, truncateStreamFromCallback } from './HybridPersistence';
5
+ import { LoggerFactory } from 'slf';
6
+ import { Snapshot } from './utils';
7
+
8
+ const log = LoggerFactory.getLogger('tapeworm-persistence-hybrid:hybrid-partition');
9
+
10
+ const DEFAULT_LOAD_SNAPSHOT_MAX_TIME = 10;
11
+
12
+ class HybridPartition implements Partition {
13
+ private loggingOptions: LoggingOptions;
14
+
15
+ /**
16
+ * @param localPartition
17
+ * @param remotePartition
18
+ * @param autoCleanLocalPartition If set to true the partition will automatically clean out data older than dataLifeTime minutes dataLifeTime.
19
+ * @param snapshotLifeTime Number of minutes before data will be considered old.
20
+ * @param loggingOptions Options for logging
21
+ */
22
+ constructor(
23
+ private localPartition: Partition,
24
+ private remotePartition: Partition,
25
+ autoCleanLocalPartition: boolean,
26
+ private snapshotLifeTime: number,
27
+ loggingOptions: LoggingOptions
28
+ ) {
29
+ this.loggingOptions = loggingOptions;
30
+ if (!this.loggingOptions.loadSnapshotMaxTime) {
31
+ this.loggingOptions.loadSnapshotMaxTime = DEFAULT_LOAD_SNAPSHOT_MAX_TIME;
32
+ }
33
+
34
+ if (autoCleanLocalPartition) {
35
+ if (!localPartition.querySnapshotsOlderThanMaxDate) {
36
+ throw new Error('Unable to start auto clean of local partition since it does not support it.');
37
+ }
38
+
39
+ log.info('Starting auto clean of local partition');
40
+ setInterval(() => {
41
+ this.cleanPartition();
42
+ }, 1000 * 60 * 30); //Clean every 30 minutes
43
+ }
44
+ }
45
+
46
+ private cleanPartition() {
47
+ log.info('Auto cleaning partition');
48
+ const snapshotTTLDate = new Date();
49
+ snapshotTTLDate.setMinutes(snapshotTTLDate.getMinutes() - this.snapshotLifeTime);
50
+ this.localPartition.querySnapshotsOlderThanMaxDate(snapshotTTLDate.toISOString()).then((snapshots) => {
51
+ log.info('cleaning %s snapshots', snapshots.length);
52
+ const snapshotIdsToRemove = snapshots.map((snapshot) => snapshot.id);
53
+ this.localPartition.removeSnapshots(snapshotIdsToRemove);
54
+ snapshotIdsToRemove.forEach((snapshotId) => this.localPartition.truncateStreamFrom(snapshotId, 0, true));
55
+ });
56
+ }
57
+
58
+ open() {
59
+ return BluebirdPromise.resolve(this);
60
+ }
61
+
62
+ append(commit: Commit<Record<string, any>>) {
63
+ return this.localPartition.append(commit);
64
+ }
65
+
66
+ storeSnapshot(streamId: string, snapshot: Snapshot, version: number) {
67
+ return this.localPartition.storeSnapshot(streamId, snapshot, version);
68
+ }
69
+
70
+ private checkIsSnapshotToOld(snapshot?: Snapshot) {
71
+ if (!snapshot) {
72
+ return true;
73
+ }
74
+
75
+ if (snapshot.storedDateTime) {
76
+ const storedDateEpoch = new Date(snapshot.storedDateTime).getTime();
77
+ const nowEpoch = new Date().getTime();
78
+
79
+ const elapsedTimeInMilliSeconds = nowEpoch - storedDateEpoch;
80
+ const snapshotLifetimeMilliSeconds = this.snapshotLifeTime * 60 * 1000;
81
+
82
+ return elapsedTimeInMilliSeconds > snapshotLifetimeMilliSeconds;
83
+ }
84
+
85
+ return false;
86
+ }
87
+
88
+ loadSnapshot(streamId: string, callback?: (err: Error, snapshot: Snapshot) => void) {
89
+ const startTime = Date.now();
90
+ return new BluebirdPromise<Snapshot>((resolve) => {
91
+ this.localPartition.loadSnapshot(streamId).then((localSnapshot) => {
92
+ const isSnapshotToOld = this.checkIsSnapshotToOld(localSnapshot);
93
+ if (localSnapshot && !isSnapshotToOld) {
94
+ log.info('loadSnapshot: Using local snapshot');
95
+ callback?.(null, localSnapshot);
96
+ resolve(localSnapshot);
97
+ return;
98
+ }
99
+
100
+ this.remotePartition
101
+ .loadSnapshot(streamId)
102
+ .then((remoteSnapshot) => {
103
+ log.info('loadSnapshot: Using remote snapshot');
104
+ if (remoteSnapshot) {
105
+ this.localPartition.storeSnapshot(remoteSnapshot.id, remoteSnapshot.snapshot, remoteSnapshot.version);
106
+ this.localPartition.truncateStreamFrom(remoteSnapshot.id, 0, true);
107
+ }
108
+ callback?.(null, remoteSnapshot);
109
+ resolve(remoteSnapshot);
110
+ })
111
+ .catch((error) => {
112
+ log.warn('Failed to load snapshot with stream id %s from remote partition', streamId);
113
+ });
114
+ });
115
+ }).finally(() => {
116
+ const endTime = Date.now();
117
+ const queryTime = (endTime - startTime) / 1000;
118
+ if (this.loggingOptions.loggingEnabled && queryTime > this.loggingOptions.loadSnapshotMaxTime) {
119
+ log.warn('Loading snapshot with id %s, took %s seconds. Allowed max time is %s seconds.', streamId, queryTime, this.loggingOptions.loadSnapshotMaxTime);
120
+ }
121
+ });
122
+ }
123
+
124
+ queryStream(streamId: string, fromEventSequence?: number | queryStreamCallback, callback?: queryStreamCallback) {
125
+ log.debug('queryStream, streamId %s ,fromEventSequence %s', streamId, fromEventSequence);
126
+ return new BluebirdPromise<Commit<Record<string, any>>[]>((resolve) => {
127
+ this.localPartition.loadSnapshot(streamId).then((localSnapshot) => {
128
+ let currentPartition = this.localPartition;
129
+ const isSnapshotToOld = this.checkIsSnapshotToOld(localSnapshot);
130
+
131
+ if (!localSnapshot || isSnapshotToOld) {
132
+ currentPartition = this.remotePartition;
133
+ }
134
+
135
+ currentPartition.queryStream(streamId, fromEventSequence).then((queryStream) => {
136
+ callback?.(null, queryStream);
137
+ resolve(queryStream);
138
+ return;
139
+ });
140
+ });
141
+ });
142
+ }
143
+
144
+ removeSnapshot(streamId: string) {
145
+ return this.localPartition.removeSnapshot(streamId);
146
+ }
147
+
148
+ truncateStreamFrom(
149
+ streamId: string,
150
+ commit: Commit<Record<string, any>>,
151
+ remove: boolean | truncateStreamFromCallback,
152
+ callback?: truncateStreamFromCallback
153
+ ) {
154
+ return this.localPartition.truncateStreamFrom(streamId, commit, remove, callback);
155
+ }
156
+
157
+ applyCommitHeader(streamId: string, commit: Commit<Record<string, any>>, remove: any, callback: () => void) {
158
+ return this.localPartition.applyCommitHeader(streamId, commit, remove, callback);
159
+ }
160
+ }
161
+
162
+ export default HybridPartition;
@@ -0,0 +1,82 @@
1
+ import BluebirdPromise from 'bluebird';
2
+ import HybridPartition from './HybridPartition';
3
+ import { Commit, Snapshot } from './utils';
4
+
5
+ export type truncateStreamFromCallback = (error: Error, result: Record<string, any>) => void;
6
+ type TruncateStreamFrom = (
7
+ streamId: string,
8
+ commitSequence: number,
9
+ remove: boolean | truncateStreamFromCallback,
10
+ callback?: truncateStreamFromCallback
11
+ ) => BluebirdPromise<void>;
12
+
13
+ export type queryStreamCallback = (err: Error, steam: Commit<Record<string, any>>[]) => void;
14
+ type QueryStream = (
15
+ streamId: string,
16
+ fromEventSequence?: number | queryStreamCallback,
17
+ callback?: queryStreamCallback
18
+ ) => BluebirdPromise<Commit<Record<string, any>>[]>;
19
+
20
+ export type Partition = {
21
+ open: () => BluebirdPromise<Partition>;
22
+ storeSnapshot: (streamId: string, snapshot: Snapshot['snapshot'], version: number) => BluebirdPromise<void>;
23
+ loadSnapshot: (streamId: string, callback?: (err: Error, snapshot: Snapshot | undefined) => void) => BluebirdPromise<Snapshot | undefined>;
24
+ queryStream: QueryStream;
25
+ append: (commit: Commit<Record<string, any>>) => BluebirdPromise<Commit<Record<string, any>>[]>;
26
+ removeSnapshot: (streamId: string) => BluebirdPromise<void>;
27
+ removeSnapshots?: (streamIds: string[]) => BluebirdPromise<void>;
28
+ truncateStreamFrom: TruncateStreamFrom;
29
+ applyCommitHeader: (streamId: string, commit: Commit<Record<string, any>>, remove: any, callback: () => void) => BluebirdPromise<void>;
30
+ querySnapshotsOlderThanMaxDate?: (dateTime: string) => BluebirdPromise<Snapshot[]>;
31
+ };
32
+
33
+ export interface LoggingOptions {
34
+ loggingEnabled: boolean;
35
+ loadSnapshotMaxTime?: number;
36
+ }
37
+
38
+ export type Persistence = {
39
+ openPartition: (partitionId?: string) => BluebirdPromise<Partition>;
40
+ };
41
+
42
+ const DEFAULT_PARTITION = 'master';
43
+
44
+ class HybridPersistence {
45
+ private partitions: Record<string, Partition> = {};
46
+
47
+ constructor(
48
+ private localPersistence: Persistence,
49
+ private remotePersistence: Persistence,
50
+ private autoCleanLocalPartition: boolean,
51
+ private snapshotLifeTime: number,
52
+ private loggingOptions: LoggingOptions
53
+ ) {}
54
+
55
+ openPartition(partitionId: string = DEFAULT_PARTITION) {
56
+ return new BluebirdPromise(async (resolve) => {
57
+ const partition = this.getPartition(partitionId);
58
+ if (partition) {
59
+ resolve(partition);
60
+ } else {
61
+ const localPartition = await this.localPersistence.openPartition(partitionId);
62
+ const remotePartition = await this.remotePersistence.openPartition(partitionId);
63
+ const newPartition = new HybridPartition(localPartition, remotePartition, this.autoCleanLocalPartition, this.snapshotLifeTime, this.loggingOptions);
64
+ this.setPartition(partitionId, newPartition);
65
+ newPartition.open().then(() => {
66
+ resolve(newPartition);
67
+ });
68
+ }
69
+ });
70
+ }
71
+
72
+ private getPartition(partitionId: string = DEFAULT_PARTITION) {
73
+ return this.partitions[partitionId];
74
+ }
75
+
76
+ private setPartition(partitionId?: string, partition?: Partition) {
77
+ this.partitions[partitionId || DEFAULT_PARTITION] = partition;
78
+ return partition;
79
+ }
80
+ }
81
+
82
+ export default HybridPersistence;
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import HybridPartition from './HybridPartition';
2
+ import HybridPersistence from './HybridPersistence';
3
+
4
+ export { HybridPartition, HybridPersistence };
package/src/utils.ts ADDED
@@ -0,0 +1,25 @@
1
+ interface Event<Payload = Record<string, any>> {
2
+ type: string;
3
+ aggregateId: string;
4
+ correlationId: string;
5
+ payload: Payload;
6
+ id?: string;
7
+ dateTime?: string;
8
+ headers?: Record<string, any>;
9
+ version?: number;
10
+ }
11
+
12
+ export interface Commit<Payload> {
13
+ id: string;
14
+ partitionId: string;
15
+ streamId: string;
16
+ commitSequence: number;
17
+ events: Event<Payload>[];
18
+ }
19
+
20
+ export interface Snapshot {
21
+ id: string;
22
+ version: number;
23
+ storedDateTime?: string;
24
+ snapshot?: Record<string, any>;
25
+ }