@forge/cache 0.1.0-next.0
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/README.md +18 -0
- package/out/__test__/cache.test.d.ts +2 -0
- package/out/__test__/cache.test.d.ts.map +1 -0
- package/out/__test__/cache.test.js +144 -0
- package/out/cache.d.ts +23 -0
- package/out/cache.d.ts.map +1 -0
- package/out/cache.js +88 -0
- package/out/index.d.ts +2 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +5 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Library for Forge environment.
|
|
2
|
+
|
|
3
|
+
Usage example:
|
|
4
|
+
```typescript
|
|
5
|
+
import * as cache from "./rockmelon-api";
|
|
6
|
+
|
|
7
|
+
const cacheClient = cache.connect();
|
|
8
|
+
|
|
9
|
+
const cacheClient = cache.connect();
|
|
10
|
+
|
|
11
|
+
await cacheClient.set("hello", "3", { ttlSeconds: 10 });
|
|
12
|
+
|
|
13
|
+
const result = await cacheClient.setIfNotExists("hello", "3", { ttlSeconds: 10 });
|
|
14
|
+
|
|
15
|
+
const result = await cacheClient.get("hello");
|
|
16
|
+
|
|
17
|
+
const result = await cacheClient.incrementAndGet("hello");
|
|
18
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.test.d.ts","sourceRoot":"","sources":["../../src/__test__/cache.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const api_1 = require("@forge/api");
|
|
4
|
+
const cache_1 = require("../cache");
|
|
5
|
+
describe('createFetch', () => {
|
|
6
|
+
let fetchMock;
|
|
7
|
+
let orgFetch;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
fetchMock = jest.fn();
|
|
10
|
+
orgFetch = global['fetch'];
|
|
11
|
+
global['fetch'] = fetchMock;
|
|
12
|
+
});
|
|
13
|
+
afterAll(() => {
|
|
14
|
+
global['fetch'] = orgFetch;
|
|
15
|
+
});
|
|
16
|
+
it.each([
|
|
17
|
+
['dev.services.atlassian.com', 'rockmelon-storage.dev.atl-paas.net'],
|
|
18
|
+
['stg.services.atlassian.com', 'rockmelon-storage.staging.atl-paas.net'],
|
|
19
|
+
['services.atlassian.com', 'rockmelon-storage.prod.atl-paas.net']
|
|
20
|
+
])('creates a fetch that adds the right headers and url for the %s environment', async (proxyUrl, hostUrl) => {
|
|
21
|
+
var _a;
|
|
22
|
+
global['__forge_runtime__'] = {
|
|
23
|
+
proxy: {
|
|
24
|
+
token: 'token',
|
|
25
|
+
url: proxyUrl
|
|
26
|
+
},
|
|
27
|
+
rmsStoreUrl: 'url'
|
|
28
|
+
};
|
|
29
|
+
const fetch = (0, cache_1.createFetch)();
|
|
30
|
+
await fetch('/path');
|
|
31
|
+
const [absoluteUrl, options] = (_a = fetchMock.mock.lastCall) !== null && _a !== void 0 ? _a : ['', {}];
|
|
32
|
+
expect(absoluteUrl).toEqual('url/path');
|
|
33
|
+
expect((options === null || options === void 0 ? void 0 : options.agent)['keepAlive']).toBeTruthy();
|
|
34
|
+
expect(options === null || options === void 0 ? void 0 : options.headers).toMatchObject({
|
|
35
|
+
Authorization: 'Bearer token',
|
|
36
|
+
Host: hostUrl
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('Cache', () => {
|
|
41
|
+
function buildCache() {
|
|
42
|
+
const fetch = jest.fn();
|
|
43
|
+
const cache = new cache_1.Cache({
|
|
44
|
+
timer: () => ({
|
|
45
|
+
measure: () => ({
|
|
46
|
+
stop: () => {
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
}, fetch);
|
|
51
|
+
return { cache, fetch };
|
|
52
|
+
}
|
|
53
|
+
describe('set', () => {
|
|
54
|
+
it('handles success', async () => {
|
|
55
|
+
const { cache, fetch } = buildCache();
|
|
56
|
+
fetch.mockResolvedValueOnce({ ok: true, text: async () => JSON.stringify({}), status: 200 });
|
|
57
|
+
await cache.set('key', 'value');
|
|
58
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
59
|
+
});
|
|
60
|
+
it('handles success with ttl', async () => {
|
|
61
|
+
const { cache, fetch } = buildCache();
|
|
62
|
+
fetch.mockResolvedValueOnce({ ok: true, text: async () => JSON.stringify({}), status: 200 });
|
|
63
|
+
await cache.set('key', 'value', { ttlSeconds: 100 });
|
|
64
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
65
|
+
});
|
|
66
|
+
it('handles failure', async () => {
|
|
67
|
+
const { cache, fetch } = buildCache();
|
|
68
|
+
fetch.mockResolvedValueOnce({ ok: false, text: async () => 'Not allowed', status: 403 });
|
|
69
|
+
await expect(cache.set('key', 'value', { ttlSeconds: 100 })).rejects.toMatchError(new api_1.HttpError('403: Not allowed'));
|
|
70
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('setIfNotExists', () => {
|
|
74
|
+
it('handles success', async () => {
|
|
75
|
+
const { cache, fetch } = buildCache();
|
|
76
|
+
fetch.mockResolvedValueOnce({ ok: true, text: async () => JSON.stringify({ response: 'OK' }), status: 200 });
|
|
77
|
+
const result = await cache.setIfNotExists('key', 'value');
|
|
78
|
+
expect(result).toEqual('OK');
|
|
79
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
80
|
+
});
|
|
81
|
+
it('handles success when key exists', async () => {
|
|
82
|
+
const { cache, fetch } = buildCache();
|
|
83
|
+
fetch.mockResolvedValueOnce({ ok: true, text: async () => JSON.stringify({ response: null }), status: 200 });
|
|
84
|
+
const result = await cache.setIfNotExists('key', 'value');
|
|
85
|
+
expect(result).toBeNull();
|
|
86
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
87
|
+
});
|
|
88
|
+
it('handles success with ttl', async () => {
|
|
89
|
+
const { cache, fetch } = buildCache();
|
|
90
|
+
fetch.mockResolvedValueOnce({ ok: true, text: async () => JSON.stringify({ response: 'OK' }), status: 200 });
|
|
91
|
+
await cache.setIfNotExists('key', 'value', { ttlSeconds: 100 });
|
|
92
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
93
|
+
});
|
|
94
|
+
it('handles failure', async () => {
|
|
95
|
+
const { cache, fetch } = buildCache();
|
|
96
|
+
fetch.mockResolvedValueOnce({ ok: false, text: async () => 'Not allowed', status: 403 });
|
|
97
|
+
await expect(cache.setIfNotExists('key', 'value', { ttlSeconds: 100 })).rejects.toMatchError(new api_1.HttpError('403: Not allowed'));
|
|
98
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('get', () => {
|
|
102
|
+
it('handles success', async () => {
|
|
103
|
+
const { cache, fetch } = buildCache();
|
|
104
|
+
fetch.mockResolvedValueOnce({ ok: true, text: async () => JSON.stringify({ value: 'asdfasdf' }), status: 200 });
|
|
105
|
+
const result = await cache.get('key');
|
|
106
|
+
expect(result).toEqual('asdfasdf');
|
|
107
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
108
|
+
});
|
|
109
|
+
it('handles failure', async () => {
|
|
110
|
+
const { cache, fetch } = buildCache();
|
|
111
|
+
fetch.mockResolvedValueOnce({ ok: false, text: async () => 'Not allowed', status: 403 });
|
|
112
|
+
await expect(cache.get('key')).rejects.toMatchError(new api_1.HttpError('403: Not allowed'));
|
|
113
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('incrementAndGet', () => {
|
|
117
|
+
it('handles success', async () => {
|
|
118
|
+
const { cache, fetch } = buildCache();
|
|
119
|
+
fetch.mockResolvedValueOnce({ ok: true, text: async () => JSON.stringify({ response: 0 }), status: 200 });
|
|
120
|
+
const result = await cache.incrementAndGet('key');
|
|
121
|
+
expect(result).toEqual(0);
|
|
122
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
123
|
+
});
|
|
124
|
+
it('handles success when key exists', async () => {
|
|
125
|
+
const { cache, fetch } = buildCache();
|
|
126
|
+
fetch.mockResolvedValueOnce({ ok: true, text: async () => JSON.stringify({ response: 11 }), status: 200 });
|
|
127
|
+
const result = await cache.incrementAndGet('key');
|
|
128
|
+
expect(result).toEqual(11);
|
|
129
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
130
|
+
});
|
|
131
|
+
it('handles success with ttl', async () => {
|
|
132
|
+
const { cache, fetch } = buildCache();
|
|
133
|
+
fetch.mockResolvedValueOnce({ ok: true, text: async () => JSON.stringify({ response: 13 }), status: 200 });
|
|
134
|
+
await cache.incrementAndGet('key', { ttlSeconds: 100 });
|
|
135
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
136
|
+
});
|
|
137
|
+
it('handles failure', async () => {
|
|
138
|
+
const { cache, fetch } = buildCache();
|
|
139
|
+
fetch.mockResolvedValueOnce({ ok: false, text: async () => 'Not allowed', status: 403 });
|
|
140
|
+
await expect(cache.incrementAndGet('key')).rejects.toMatchError(new api_1.HttpError('403: Not allowed'));
|
|
141
|
+
expect(fetch.mock.lastCall).toMatchSnapshot();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
package/out/cache.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RequestInit, Response as nodeFetchResponse } from 'node-fetch';
|
|
2
|
+
import { Metrics } from '@forge/metrics';
|
|
3
|
+
export declare type Response = Pick<nodeFetchResponse, 'text' | 'ok' | 'status'>;
|
|
4
|
+
export declare function getResponseBody(response: Response): Promise<any>;
|
|
5
|
+
export declare class Cache {
|
|
6
|
+
private metrics;
|
|
7
|
+
private client;
|
|
8
|
+
constructor(metrics: Metrics, client: (path: string, options?: RequestInit) => Promise<Response>);
|
|
9
|
+
private buildRequest;
|
|
10
|
+
set(key: string, value: string, opt?: {
|
|
11
|
+
ttlSeconds: number;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
setIfNotExists(key: string, value: string, opt?: {
|
|
14
|
+
ttlSeconds: number;
|
|
15
|
+
}): Promise<'OK' | null>;
|
|
16
|
+
get(key: string): Promise<string>;
|
|
17
|
+
incrementAndGet(key: string, opt?: {
|
|
18
|
+
ttlSeconds: number;
|
|
19
|
+
}): Promise<number>;
|
|
20
|
+
}
|
|
21
|
+
export declare function createFetch(): (path: string, options?: RequestInit) => Promise<Response>;
|
|
22
|
+
export declare function connect(): Cache;
|
|
23
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAEA,OAAkB,EAAE,WAAW,EAAE,QAAQ,IAAI,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnF,OAAO,EAAmB,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAG1D,oBAAY,QAAQ,GAAG,IAAI,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC,CAAC;AAGzE,wBAAsB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,CAYtE;AAED,qBAAa,KAAK;IACJ,OAAO,CAAC,OAAO;IAAW,OAAO,CAAC,MAAM;gBAAhC,OAAO,EAAE,OAAO,EAAU,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC;IAEhH,OAAO,CAAC,YAAY;IAUP,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5E,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAS9F,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASjC,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;CAQzF;AAcD,wBAAgB,WAAW,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAwBxF;AAGD,wBAAgB,OAAO,UAEtB"}
|
package/out/cache.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.connect = exports.createFetch = exports.Cache = exports.getResponseBody = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const https_1 = require("https");
|
|
6
|
+
const api_1 = require("@forge/api");
|
|
7
|
+
const metrics_1 = require("@forge/metrics");
|
|
8
|
+
async function getResponseBody(response) {
|
|
9
|
+
const responseText = await response.text();
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
throw new api_1.HttpError(`${response.status}: ${responseText}`);
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(responseText);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
throw new Error(`Unexpected error. Response text was not a valid JSON: ${responseText}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.getResponseBody = getResponseBody;
|
|
21
|
+
class Cache {
|
|
22
|
+
constructor(metrics, client) {
|
|
23
|
+
this.metrics = metrics;
|
|
24
|
+
this.client = client;
|
|
25
|
+
}
|
|
26
|
+
buildRequest(requestBody) {
|
|
27
|
+
return {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
body: JSON.stringify(requestBody),
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json'
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async set(key, value, opt) {
|
|
36
|
+
const timer = this.metrics.timer('cache.set').measure();
|
|
37
|
+
const response = await this.client('/rms/store/set', this.buildRequest(Object.assign({ key, value }, opt)));
|
|
38
|
+
timer.stop();
|
|
39
|
+
await getResponseBody(response);
|
|
40
|
+
}
|
|
41
|
+
async setIfNotExists(key, value, opt) {
|
|
42
|
+
const timer = this.metrics.timer('cache.setIfNotExists').measure();
|
|
43
|
+
const response = await this.client('/rms/store/setnx', this.buildRequest(Object.assign({ key, value }, opt)));
|
|
44
|
+
timer.stop();
|
|
45
|
+
const { response: result } = await getResponseBody(response);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
async get(key) {
|
|
49
|
+
const timer = this.metrics.timer('cache.get').measure();
|
|
50
|
+
const response = await this.client('/rms/store/get', this.buildRequest({ key }));
|
|
51
|
+
timer.stop();
|
|
52
|
+
const { value: result } = await getResponseBody(response);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
async incrementAndGet(key, opt) {
|
|
56
|
+
const timer = this.metrics.timer('cache.incrementAndGet').measure();
|
|
57
|
+
const response = await this.client('/rms/store/incr', this.buildRequest(Object.assign({ key }, opt)));
|
|
58
|
+
timer.stop();
|
|
59
|
+
const { response: result } = await getResponseBody(response);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.Cache = Cache;
|
|
64
|
+
function determineHost(proxyUrl) {
|
|
65
|
+
if (proxyUrl.includes('dev.services.atlassian.com')) {
|
|
66
|
+
return 'rockmelon-storage.dev.atl-paas.net';
|
|
67
|
+
}
|
|
68
|
+
else if (proxyUrl.includes('stg.services.atlassian.com')) {
|
|
69
|
+
return 'rockmelon-storage.staging.atl-paas.net';
|
|
70
|
+
}
|
|
71
|
+
return 'rockmelon-storage.prod.atl-paas.net';
|
|
72
|
+
}
|
|
73
|
+
function createFetch() {
|
|
74
|
+
const { proxy, rmsStoreUrl } = (0, api_1.getRuntime)();
|
|
75
|
+
const host = determineHost(proxy.url);
|
|
76
|
+
const agent = new https_1.Agent({ keepAlive: true });
|
|
77
|
+
if (!rmsStoreUrl) {
|
|
78
|
+
throw new Error('rms URL not found.');
|
|
79
|
+
}
|
|
80
|
+
return async function (path, options) {
|
|
81
|
+
return await global['fetch']((0, path_1.join)(rmsStoreUrl, path), Object.assign(Object.assign({}, options), { agent, headers: Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options.headers), { Authorization: `Bearer ${proxy.token}`, Host: host }) }));
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
exports.createFetch = createFetch;
|
|
85
|
+
function connect() {
|
|
86
|
+
return new Cache(metrics_1.internalMetrics, createFetch());
|
|
87
|
+
}
|
|
88
|
+
exports.connect = connect;
|
package/out/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC"}
|
package/out/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forge/cache",
|
|
3
|
+
"version": "0.1.0-next.0",
|
|
4
|
+
"description": "Forge Cache methods",
|
|
5
|
+
"author": "Atlassian",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"main": "out/index.js",
|
|
8
|
+
"types": "out/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"out"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "yarn run clean && yarn run compile",
|
|
14
|
+
"clean": "rm -rf ./out && rm -f tsconfig.tsbuildinfo",
|
|
15
|
+
"compile": "tsc -b -v"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "14.18.44",
|
|
19
|
+
"@types/node-fetch": "^2.5.7",
|
|
20
|
+
"node-fetch": "2.6.7"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@forge/api": "^2.16.0-next.1",
|
|
24
|
+
"@forge/metrics": "^0.1.9-next.1"
|
|
25
|
+
}
|
|
26
|
+
}
|