0z2i6v3u5t 1.0.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/.devcontainer/devcontainer.json +4 -0
- package/.devcontainer/setup.sh +11 -0
- package/.dockerignore +2 -0
- package/.github/CONTRIBUTING.md +52 -0
- package/.github/FUNDING.yml +3 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +59 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +43 -0
- package/.github/dependabot.yml +17 -0
- package/.github/workflows/codeql.yml +76 -0
- package/.github/workflows/publish_docs.yml +25 -0
- package/.github/workflows/test.yml +78 -0
- package/.nvmrc +1 -0
- package/.prettierignore +1 -0
- package/.prettierrc +1 -0
- package/.vscode/launch.json +42 -0
- package/CODE_OF_CONDUCT.md +76 -0
- package/Dockerfile +17 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/SECURITY.md +5 -0
- package/__tests__/actions/cacheTest.ts +58 -0
- package/__tests__/actions/randomNumber.ts +26 -0
- package/__tests__/actions/recursiveAction.ts +16 -0
- package/__tests__/actions/sleepTest.ts +24 -0
- package/__tests__/actions/status.ts +17 -0
- package/__tests__/actions/swagger.ts +76 -0
- package/__tests__/actions/validationTest.ts +63 -0
- package/__tests__/cli/cli.ts +126 -0
- package/__tests__/core/api.ts +632 -0
- package/__tests__/core/cache.ts +400 -0
- package/__tests__/core/chatRoom.ts +589 -0
- package/__tests__/core/cli.ts +349 -0
- package/__tests__/core/cluster.ts +132 -0
- package/__tests__/core/config.ts +78 -0
- package/__tests__/core/errors.ts +112 -0
- package/__tests__/core/log.ts +23 -0
- package/__tests__/core/middleware.ts +427 -0
- package/__tests__/core/plugins/partialPlugin.ts +94 -0
- package/__tests__/core/plugins/withPlugin.ts +88 -0
- package/__tests__/core/plugins/withoutPlugin.ts +81 -0
- package/__tests__/core/process.ts +42 -0
- package/__tests__/core/specHelper.ts +330 -0
- package/__tests__/core/staticFile/compression.ts +99 -0
- package/__tests__/core/staticFile/staticFile.ts +180 -0
- package/__tests__/core/tasks/customQueueFunction.ts +67 -0
- package/__tests__/core/tasks/fullWorkerFlow.ts +199 -0
- package/__tests__/core/tasks/tasks.ts +605 -0
- package/__tests__/integration/browser.ts +133 -0
- package/__tests__/integration/ioredis-mock.ts +194 -0
- package/__tests__/integration/sendBuffer.ts +97 -0
- package/__tests__/integration/sendFile.ts +24 -0
- package/__tests__/integration/sharedFingerprint.ts +82 -0
- package/__tests__/integration/taskFlow.ts +110 -0
- package/__tests__/jest.ts +5 -0
- package/__tests__/modules/action.ts +103 -0
- package/__tests__/modules/config.ts +19 -0
- package/__tests__/modules/utils/ensureNoTsHeaderOrSpecFiles.ts +24 -0
- package/__tests__/servers/web/allowedRequestHosts.ts +88 -0
- package/__tests__/servers/web/enableMultiples.ts +83 -0
- package/__tests__/servers/web/fileUpload.ts +79 -0
- package/__tests__/servers/web/jsonp.ts +57 -0
- package/__tests__/servers/web/nonMultiples.ts +83 -0
- package/__tests__/servers/web/rawBody.ts +208 -0
- package/__tests__/servers/web/returnErrorCodes.ts +55 -0
- package/__tests__/servers/web/routes/deepRoutes.ts +96 -0
- package/__tests__/servers/web/routes/routes.ts +579 -0
- package/__tests__/servers/web/routes/veryDeepRoutes.ts +92 -0
- package/__tests__/servers/web/web.ts +1031 -0
- package/__tests__/servers/websocket.ts +795 -0
- package/__tests__/tasks/runAction.ts +37 -0
- package/__tests__/template.ts.example +20 -0
- package/__tests__/testCliCommands/hello.ts +44 -0
- package/__tests__/testPlugin/public/plugin.html +1 -0
- package/__tests__/testPlugin/src/actions/pluginAction.ts +14 -0
- package/__tests__/testPlugin/src/bin/hello.ts +22 -0
- package/__tests__/testPlugin/src/initializers/pluginInitializer.ts +17 -0
- package/__tests__/testPlugin/src/tasks/pluginTask.ts +15 -0
- package/__tests__/testPlugin/tsconfig.json +10 -0
- package/__tests__/utils/utils.ts +492 -0
- package/app.json +23 -0
- package/bin/deploy-docs +39 -0
- package/client/ActionheroWebsocketClient.js +277 -0
- package/docker-compose.yml +73 -0
- package/package.json +24 -0
- package/public/chat.html +194 -0
- package/public/css/cosmo.css +12 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +115 -0
- package/public/javascript/.gitkeep +0 -0
- package/public/linkedSession.html +80 -0
- package/public/logo/actionhero-small.png +0 -0
- package/public/logo/actionhero.png +0 -0
- package/public/pixel.gif +0 -0
- package/public/simple.html +2 -0
- package/public/swagger.html +32 -0
- package/public/websocketLoadTest.html +322 -0
- package/src/actions/cacheTest.ts +58 -0
- package/src/actions/createChatRoom.ts +20 -0
- package/src/actions/randomNumber.ts +17 -0
- package/src/actions/recursiveAction.ts +13 -0
- package/src/actions/sendFile.ts +12 -0
- package/src/actions/sleepTest.ts +40 -0
- package/src/actions/status.ts +73 -0
- package/src/actions/swagger.ts +155 -0
- package/src/actions/validationTest.ts +36 -0
- package/src/bin/actionhero.ts +225 -0
- package/src/bin/methods/actions/list.ts +30 -0
- package/src/bin/methods/console.ts +26 -0
- package/src/bin/methods/generate/action.ts +58 -0
- package/src/bin/methods/generate/cli.ts +51 -0
- package/src/bin/methods/generate/initializer.ts +54 -0
- package/src/bin/methods/generate/plugin.ts +57 -0
- package/src/bin/methods/generate/server.ts +38 -0
- package/src/bin/methods/generate/task.ts +68 -0
- package/src/bin/methods/generate.ts +176 -0
- package/src/bin/methods/task/enqueue.ts +35 -0
- package/src/classes/action.ts +98 -0
- package/src/classes/actionProcessor.ts +463 -0
- package/src/classes/api.ts +51 -0
- package/src/classes/cli.ts +67 -0
- package/src/classes/config.ts +15 -0
- package/src/classes/connection.ts +321 -0
- package/src/classes/exceptionReporter.ts +9 -0
- package/src/classes/initializer.ts +59 -0
- package/src/classes/initializers.ts +5 -0
- package/src/classes/input.ts +9 -0
- package/src/classes/inputs.ts +34 -0
- package/src/classes/process/actionheroVersion.ts +15 -0
- package/src/classes/process/env.ts +16 -0
- package/src/classes/process/id.ts +34 -0
- package/src/classes/process/pid.ts +32 -0
- package/src/classes/process/projectRoot.ts +16 -0
- package/src/classes/process/typescript.ts +47 -0
- package/src/classes/process.ts +479 -0
- package/src/classes/server.ts +251 -0
- package/src/classes/task.ts +87 -0
- package/src/config/api.ts +107 -0
- package/src/config/errors.ts +162 -0
- package/src/config/logger.ts +113 -0
- package/src/config/plugins.ts +37 -0
- package/src/config/redis.ts +78 -0
- package/src/config/routes.ts +44 -0
- package/src/config/tasks.ts +84 -0
- package/src/config/web.ts +136 -0
- package/src/config/websocket.ts +62 -0
- package/src/index.ts +46 -0
- package/src/initializers/actions.ts +125 -0
- package/src/initializers/chatRoom.ts +214 -0
- package/src/initializers/connections.ts +124 -0
- package/src/initializers/exceptions.ts +155 -0
- package/src/initializers/params.ts +52 -0
- package/src/initializers/redis.ts +191 -0
- package/src/initializers/resque.ts +248 -0
- package/src/initializers/routes.ts +229 -0
- package/src/initializers/servers.ts +134 -0
- package/src/initializers/specHelper.ts +195 -0
- package/src/initializers/staticFile.ts +253 -0
- package/src/initializers/tasks.ts +188 -0
- package/src/modules/action.ts +89 -0
- package/src/modules/cache.ts +326 -0
- package/src/modules/chatRoom.ts +321 -0
- package/src/modules/config.ts +246 -0
- package/src/modules/log.ts +62 -0
- package/src/modules/redis.ts +93 -0
- package/src/modules/route.ts +59 -0
- package/src/modules/specHelper.ts +182 -0
- package/src/modules/task.ts +527 -0
- package/src/modules/utils/argv.ts +3 -0
- package/src/modules/utils/arrayStartingMatch.ts +21 -0
- package/src/modules/utils/arrayUnique.ts +15 -0
- package/src/modules/utils/collapseObjectToArray.ts +33 -0
- package/src/modules/utils/deepCopy.ts +3 -0
- package/src/modules/utils/ensureNoTsHeaderOrSpecFiles.ts +19 -0
- package/src/modules/utils/eventLoopDelay.ts +34 -0
- package/src/modules/utils/fileUtils.ts +119 -0
- package/src/modules/utils/filterObjectForLogging.ts +51 -0
- package/src/modules/utils/filterResponseForLogging.ts +53 -0
- package/src/modules/utils/getExternalIPAddress.ts +17 -0
- package/src/modules/utils/hashMerge.ts +63 -0
- package/src/modules/utils/isPlainObject.ts +45 -0
- package/src/modules/utils/isRunning.ts +7 -0
- package/src/modules/utils/parseCookies.ts +20 -0
- package/src/modules/utils/parseHeadersForClientAddress.ts +53 -0
- package/src/modules/utils/parseIPv6URI.ts +24 -0
- package/src/modules/utils/replaceDistWithSrc.ts +9 -0
- package/src/modules/utils/safeGlob.ts +6 -0
- package/src/modules/utils/sleep.ts +8 -0
- package/src/modules/utils/sortGlobalMiddleware.ts +17 -0
- package/src/modules/utils/sourceRelativeLinkPath.ts +29 -0
- package/src/modules/utils.ts +66 -0
- package/src/server.ts +20 -0
- package/src/servers/web.ts +894 -0
- package/src/servers/websocket.ts +304 -0
- package/src/tasks/runAction.ts +29 -0
- package/tea.yaml +9 -0
- package/templates/README.md.template +17 -0
- package/templates/action.ts.template +15 -0
- package/templates/boot.js.template +9 -0
- package/templates/cli.ts.template +15 -0
- package/templates/gitignore.template +23 -0
- package/templates/initializer.ts.template +17 -0
- package/templates/package-plugin.json.template +12 -0
- package/templates/package.json.template +45 -0
- package/templates/projectMap.txt +39 -0
- package/templates/projectServer.ts.template +20 -0
- package/templates/server.ts.template +37 -0
- package/templates/task.ts.template +16 -0
- package/templates/test/action.ts.template +13 -0
- package/templates/test/task.ts.template +20 -0
- package/tsconfig.json +11 -0
@@ -0,0 +1,188 @@
|
|
1
|
+
import * as path from "path";
|
2
|
+
import { Plugin } from "node-resque";
|
3
|
+
import * as TaskModule from "./../modules/task";
|
4
|
+
import { api, config, log, utils, task, Initializer } from "../index";
|
5
|
+
import { Task } from "../classes/task";
|
6
|
+
import { safeGlobSync } from "../modules/utils/safeGlob";
|
7
|
+
|
8
|
+
const taskModule = task;
|
9
|
+
|
10
|
+
export interface TaskApi {
|
11
|
+
tasks: { [key: string]: Task };
|
12
|
+
jobs: { [key: string]: any };
|
13
|
+
middleware: { [key: string]: TaskModule.task.TaskMiddleware };
|
14
|
+
globalMiddleware: Array<string>;
|
15
|
+
loadFile: TasksInitializer["loadFile"];
|
16
|
+
jobWrapper: TasksInitializer["jobWrapper"];
|
17
|
+
loadTasks: TasksInitializer["loadTasks"];
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Tools for enqueuing and inspecting the task system (delayed jobs).
|
22
|
+
*/
|
23
|
+
export class TasksInitializer extends Initializer {
|
24
|
+
constructor() {
|
25
|
+
super();
|
26
|
+
this.name = "tasks";
|
27
|
+
this.loadPriority = 699;
|
28
|
+
this.startPriority = 975;
|
29
|
+
}
|
30
|
+
|
31
|
+
loadFile = async (fullFilePath: string, reload = false) => {
|
32
|
+
let task;
|
33
|
+
let collection = await import(fullFilePath);
|
34
|
+
for (const i in collection) {
|
35
|
+
const TaskClass = collection[i];
|
36
|
+
task = new TaskClass();
|
37
|
+
task.validate();
|
38
|
+
|
39
|
+
if (api.tasks.tasks[task.name] && !reload) {
|
40
|
+
log(
|
41
|
+
`an existing task with the same name \`${task.name}\` will be overridden by the file ${fullFilePath}`,
|
42
|
+
"crit",
|
43
|
+
);
|
44
|
+
}
|
45
|
+
|
46
|
+
api.tasks.tasks[task.name] = task;
|
47
|
+
api.tasks.jobs[task.name] = api.tasks.jobWrapper(task.name);
|
48
|
+
log(
|
49
|
+
`task ${reload ? "(re)" : ""} loaded: ${task.name}, ${fullFilePath}`,
|
50
|
+
"debug",
|
51
|
+
);
|
52
|
+
}
|
53
|
+
};
|
54
|
+
|
55
|
+
jobWrapper = (taskName: string) => {
|
56
|
+
const task = api.tasks.tasks[taskName];
|
57
|
+
|
58
|
+
const middleware = task.middleware || [];
|
59
|
+
const plugins = task.plugins || [];
|
60
|
+
const pluginOptions = task.pluginOptions || {};
|
61
|
+
|
62
|
+
if (task.frequency > 0) {
|
63
|
+
if (plugins.indexOf("JobLock") < 0) {
|
64
|
+
plugins.push("JobLock");
|
65
|
+
pluginOptions.JobLock = { reEnqueue: false };
|
66
|
+
}
|
67
|
+
if (plugins.indexOf("QueueLock") < 0) {
|
68
|
+
plugins.push("QueueLock");
|
69
|
+
}
|
70
|
+
if (plugins.indexOf("DelayQueueLock") < 0) {
|
71
|
+
plugins.push("DelayQueueLock");
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
// load middleware into plugins
|
76
|
+
const processMiddleware = (m: string) => {
|
77
|
+
if (api.tasks.middleware[m]) {
|
78
|
+
class NodeResquePlugin extends Plugin {
|
79
|
+
constructor(...args: ConstructorParameters<typeof Plugin>) {
|
80
|
+
super(...args);
|
81
|
+
}
|
82
|
+
|
83
|
+
async beforeEnqueue() {
|
84
|
+
return api.tasks.middleware[m].preEnqueue
|
85
|
+
? api.tasks.middleware[m].preEnqueue.call(this)
|
86
|
+
: true;
|
87
|
+
}
|
88
|
+
|
89
|
+
async afterEnqueue() {
|
90
|
+
return api.tasks.middleware[m].postEnqueue
|
91
|
+
? api.tasks.middleware[m].postEnqueue.call(this)
|
92
|
+
: true;
|
93
|
+
}
|
94
|
+
|
95
|
+
async beforePerform() {
|
96
|
+
return api.tasks.middleware[m].preProcessor
|
97
|
+
? api.tasks.middleware[m].preProcessor.call(this)
|
98
|
+
: true;
|
99
|
+
}
|
100
|
+
|
101
|
+
async afterPerform() {
|
102
|
+
return api.tasks.middleware[m].postProcessor
|
103
|
+
? api.tasks.middleware[m].postProcessor.call(this)
|
104
|
+
: true;
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
//@ts-ignore
|
109
|
+
plugins.push(NodeResquePlugin);
|
110
|
+
}
|
111
|
+
};
|
112
|
+
|
113
|
+
api.tasks.globalMiddleware.forEach(processMiddleware);
|
114
|
+
middleware.forEach(processMiddleware);
|
115
|
+
|
116
|
+
return {
|
117
|
+
plugins,
|
118
|
+
pluginOptions,
|
119
|
+
perform: async function () {
|
120
|
+
const combinedArgs = [].concat(Array.prototype.slice.call(arguments));
|
121
|
+
combinedArgs.push(this);
|
122
|
+
let response = null;
|
123
|
+
try {
|
124
|
+
response = await task.run.apply(task, combinedArgs);
|
125
|
+
await taskModule.enqueueRecurrentTask(taskName);
|
126
|
+
} catch (error) {
|
127
|
+
if (task.frequency > 0 && task.reEnqueuePeriodicTaskIfException) {
|
128
|
+
await taskModule.enqueueRecurrentTask(taskName);
|
129
|
+
}
|
130
|
+
throw error;
|
131
|
+
}
|
132
|
+
return response;
|
133
|
+
},
|
134
|
+
};
|
135
|
+
};
|
136
|
+
|
137
|
+
loadTasks = async (reload: boolean) => {
|
138
|
+
for (const p of config.general.paths.task) {
|
139
|
+
await Promise.all(
|
140
|
+
utils
|
141
|
+
.ensureNoTsHeaderOrSpecFiles(
|
142
|
+
safeGlobSync(path.join(p, "**", "**/*(*.js|*.ts)")),
|
143
|
+
)
|
144
|
+
.map((f) => api.tasks.loadFile(f, reload)),
|
145
|
+
);
|
146
|
+
}
|
147
|
+
|
148
|
+
for (const plugin of Object.values(config.plugins)) {
|
149
|
+
if (plugin.tasks !== false) {
|
150
|
+
const pluginPath = path.normalize(plugin.path);
|
151
|
+
|
152
|
+
// old style at the root of the project
|
153
|
+
let files = safeGlobSync(path.join(pluginPath, "tasks", "**", "*.js"));
|
154
|
+
|
155
|
+
files = files.concat(
|
156
|
+
safeGlobSync(path.join(pluginPath, "dist", "tasks", "**", "*.js")),
|
157
|
+
);
|
158
|
+
|
159
|
+
utils.ensureNoTsHeaderOrSpecFiles(files).forEach((f) => {
|
160
|
+
api.tasks.loadFile(f, reload);
|
161
|
+
});
|
162
|
+
}
|
163
|
+
}
|
164
|
+
};
|
165
|
+
|
166
|
+
async initialize() {
|
167
|
+
api.tasks = {
|
168
|
+
tasks: {},
|
169
|
+
jobs: {},
|
170
|
+
middleware: {},
|
171
|
+
globalMiddleware: [],
|
172
|
+
loadFile: this.loadFile,
|
173
|
+
jobWrapper: this.jobWrapper,
|
174
|
+
loadTasks: this.loadTasks,
|
175
|
+
};
|
176
|
+
|
177
|
+
await api.tasks.loadTasks(false);
|
178
|
+
|
179
|
+
// we want to start the queue now, so that it's available for other initializers and CLI commands
|
180
|
+
await api.resque.startQueue();
|
181
|
+
}
|
182
|
+
|
183
|
+
async start() {
|
184
|
+
if (config.tasks.scheduler === true) {
|
185
|
+
await taskModule.enqueueAllRecurrentTasks();
|
186
|
+
}
|
187
|
+
}
|
188
|
+
}
|
@@ -0,0 +1,89 @@
|
|
1
|
+
import { Connection } from "../classes/connection";
|
2
|
+
import { ActionProcessor } from "../classes/actionProcessor";
|
3
|
+
import { api, utils, config } from "../index";
|
4
|
+
|
5
|
+
export namespace action {
|
6
|
+
/**
|
7
|
+
* var middleware = {
|
8
|
+
* name: 'userId checker',
|
9
|
+
* global: false,
|
10
|
+
* priority: 1000,
|
11
|
+
* preProcessor: async (data) => {
|
12
|
+
* if(!data.params.userId){
|
13
|
+
* throw new Error('All actions require a userId')
|
14
|
+
* }
|
15
|
+
* },
|
16
|
+
* postProcessor: async (data) => {
|
17
|
+
* if(data.thing.stuff == false){ data.toRender = false }
|
18
|
+
* }
|
19
|
+
*}
|
20
|
+
*/
|
21
|
+
export interface ActionMiddleware {
|
22
|
+
/**Unique name for the middleware. */
|
23
|
+
name: string;
|
24
|
+
/**Is this middleware applied to all actions? */
|
25
|
+
global: boolean;
|
26
|
+
/**Module load order. Defaults to `api.config.general.defaultMiddlewarePriority`. */
|
27
|
+
priority?: number;
|
28
|
+
/**Called before the action runs. Has access to all params, before sanitization. Can modify the data object for use in actions. */
|
29
|
+
preProcessor?: Function;
|
30
|
+
/**Called after the action runs. */
|
31
|
+
postProcessor?: Function;
|
32
|
+
}
|
33
|
+
|
34
|
+
/**
|
35
|
+
* Add a middleware component available to pre or post-process actions.
|
36
|
+
*/
|
37
|
+
export function addMiddleware(data: ActionMiddleware) {
|
38
|
+
if (!data.name) {
|
39
|
+
throw new Error("middleware.name is required");
|
40
|
+
}
|
41
|
+
if (!data.priority) {
|
42
|
+
data.priority = config.general.defaultMiddlewarePriority;
|
43
|
+
}
|
44
|
+
data.priority = Number(data.priority);
|
45
|
+
api.actions.middleware[data.name] = data;
|
46
|
+
if (data.global === true) {
|
47
|
+
api.actions.globalMiddleware.push(data.name);
|
48
|
+
utils.sortGlobalMiddleware(
|
49
|
+
api.actions.globalMiddleware,
|
50
|
+
api.actions.middleware,
|
51
|
+
);
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Run an Action in-line, perhaps from within another Action or Task.
|
57
|
+
*/
|
58
|
+
export async function run<ActionClass>(
|
59
|
+
actionName: string,
|
60
|
+
actionVersion?: string | number,
|
61
|
+
params: { [key: string]: any } = {},
|
62
|
+
connectionProperties = {},
|
63
|
+
) {
|
64
|
+
const connection = new Connection({
|
65
|
+
type: "in-line-action",
|
66
|
+
remotePort: "0",
|
67
|
+
remoteIP: "0",
|
68
|
+
rawConnection: {},
|
69
|
+
canChat: false,
|
70
|
+
});
|
71
|
+
|
72
|
+
connection.params = params;
|
73
|
+
Object.assign(connection, connectionProperties);
|
74
|
+
|
75
|
+
try {
|
76
|
+
const actionProcessor = new ActionProcessor(connection);
|
77
|
+
const data = await actionProcessor.processAction(
|
78
|
+
actionName,
|
79
|
+
actionVersion,
|
80
|
+
);
|
81
|
+
|
82
|
+
if (data.response.error) throw new Error(data.response.error);
|
83
|
+
|
84
|
+
return data.response;
|
85
|
+
} finally {
|
86
|
+
await connection.destroy();
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
@@ -0,0 +1,326 @@
|
|
1
|
+
import * as fs from "fs";
|
2
|
+
import { api, id, utils, config } from "../index";
|
3
|
+
|
4
|
+
export namespace cache {
|
5
|
+
export enum CacheErrorMessages {
|
6
|
+
locked = "Object locked",
|
7
|
+
notFound = "Object not found",
|
8
|
+
}
|
9
|
+
|
10
|
+
export interface CacheObject {
|
11
|
+
key: string;
|
12
|
+
value: any;
|
13
|
+
createdAt: number;
|
14
|
+
}
|
15
|
+
|
16
|
+
export interface CacheOptions {
|
17
|
+
expireTimeMS?: number;
|
18
|
+
retry?: boolean | number;
|
19
|
+
}
|
20
|
+
|
21
|
+
export const redisPrefix: string = config.general.cachePrefix;
|
22
|
+
export const lockPrefix: string = config.general.lockPrefix;
|
23
|
+
export const lockDuration: number = config.general.lockDuration;
|
24
|
+
export const scanCount: number = config.redis.scanCount || 1000;
|
25
|
+
export const lockRetry: number = 100;
|
26
|
+
|
27
|
+
export function client() {
|
28
|
+
if (api.redis.clients && api.redis.clients.client) {
|
29
|
+
return api.redis.clients.client;
|
30
|
+
} else {
|
31
|
+
throw new Error("redis not connected, cache cannot be used");
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
let lockNameOverride: string;
|
36
|
+
export function lockName() {
|
37
|
+
if (lockNameOverride) return lockNameOverride;
|
38
|
+
return id;
|
39
|
+
}
|
40
|
+
|
41
|
+
export function overrideLockName(name: string) {
|
42
|
+
lockNameOverride = name;
|
43
|
+
}
|
44
|
+
|
45
|
+
/**
|
46
|
+
* A generic method to find all keys which match a pattern.
|
47
|
+
* @pattern likely has a * at the end of the string. Other arguments are for recursion and not required.
|
48
|
+
*/
|
49
|
+
export async function getKeys(
|
50
|
+
pattern: string,
|
51
|
+
count: number = scanCount,
|
52
|
+
keysAry: string[] = [],
|
53
|
+
cursor = 0,
|
54
|
+
): Promise<Array<string>> {
|
55
|
+
// return client().keys(redisPrefix + "*");
|
56
|
+
|
57
|
+
const [newCursor, matches] = await client().scan(
|
58
|
+
cursor,
|
59
|
+
"MATCH",
|
60
|
+
pattern,
|
61
|
+
"COUNT",
|
62
|
+
count,
|
63
|
+
);
|
64
|
+
|
65
|
+
if (matches && matches.length > 0) keysAry = keysAry.concat(matches);
|
66
|
+
if (newCursor === "0") return keysAry;
|
67
|
+
return cache.getKeys(pattern, count, keysAry, parseInt(newCursor));
|
68
|
+
}
|
69
|
+
|
70
|
+
/**
|
71
|
+
* Returns all the keys in redis which are under this Actionhero namespace. Potentially very slow.
|
72
|
+
*/
|
73
|
+
export async function keys(optionalScopePrefix = ""): Promise<Array<string>> {
|
74
|
+
// return client().keys(redisPrefix + "*");
|
75
|
+
return getKeys(redisPrefix + optionalScopePrefix + "*");
|
76
|
+
}
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Returns all the locks in redis which are under this Actionhero namespace. Potentially slow.
|
80
|
+
*/
|
81
|
+
export async function locks(
|
82
|
+
optionalScopePrefix = "",
|
83
|
+
): Promise<Array<string>> {
|
84
|
+
// return client().keys(lockPrefix + "*");
|
85
|
+
return getKeys(lockPrefix + optionalScopePrefix + "*");
|
86
|
+
}
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Returns the number of keys in redis which are under this Actionhero namespace. Potentially very slow.
|
90
|
+
*/
|
91
|
+
export async function size(pattern = redisPrefix + "*") {
|
92
|
+
let length = 0;
|
93
|
+
const keys = await cache.getKeys(pattern);
|
94
|
+
if (keys) length = keys.length;
|
95
|
+
return length;
|
96
|
+
}
|
97
|
+
|
98
|
+
/**
|
99
|
+
* Removes all keys in redis which are under this Actionhero namespace. Potentially very slow.
|
100
|
+
* Returns the deleted keys.
|
101
|
+
*/
|
102
|
+
export async function clear(pattern = redisPrefix + "*") {
|
103
|
+
const keys = await cache.getKeys(pattern);
|
104
|
+
|
105
|
+
const pipelineArgs: Array<[string, string]> = [];
|
106
|
+
keys.forEach((key: string) => {
|
107
|
+
pipelineArgs.push(["del", key]);
|
108
|
+
});
|
109
|
+
|
110
|
+
await client().pipeline(pipelineArgs).exec();
|
111
|
+
|
112
|
+
return keys;
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Write the current concents of redis (only the keys in Actionhero's namespace) to a file.
|
117
|
+
*/
|
118
|
+
export async function dumpWrite(file: string) {
|
119
|
+
const data: Record<string, any> = {};
|
120
|
+
const jobs: Array<Promise<void>> = [];
|
121
|
+
const keys = await cache.keys();
|
122
|
+
|
123
|
+
keys.forEach((key: string) => {
|
124
|
+
jobs.push(
|
125
|
+
client()
|
126
|
+
.get(key)
|
127
|
+
.then((content) => {
|
128
|
+
data[key] = content;
|
129
|
+
}),
|
130
|
+
);
|
131
|
+
});
|
132
|
+
|
133
|
+
await Promise.all(jobs);
|
134
|
+
|
135
|
+
fs.writeFileSync(file, JSON.stringify(data));
|
136
|
+
return keys.length;
|
137
|
+
}
|
138
|
+
|
139
|
+
/**
|
140
|
+
* Load in contents for redis (and api.cache) saved to a file
|
141
|
+
* Warning! Any existing keys in redis (under this Actionhero namespace) will be removed.
|
142
|
+
*/
|
143
|
+
export async function dumpRead(file: string) {
|
144
|
+
const jobs: Array<Promise<void>> = [];
|
145
|
+
await cache.clear();
|
146
|
+
const fileData = fs.readFileSync(file).toString();
|
147
|
+
const data = JSON.parse(fileData);
|
148
|
+
const count = Object.keys(data).length;
|
149
|
+
|
150
|
+
const saveDumpedElement = async (key: string, content: any) => {
|
151
|
+
await client().set(key, content, "KEEPTTL");
|
152
|
+
};
|
153
|
+
|
154
|
+
Object.keys(data).forEach((key) => {
|
155
|
+
const content = data[key];
|
156
|
+
jobs.push(saveDumpedElement(key, content));
|
157
|
+
});
|
158
|
+
|
159
|
+
await Promise.all(jobs);
|
160
|
+
return count;
|
161
|
+
}
|
162
|
+
|
163
|
+
/**
|
164
|
+
* Load an item from the cache. Will throw an error if the item named by `key` cannot be found.
|
165
|
+
* Automatically handles `api.cache.redisPrefix`
|
166
|
+
*/
|
167
|
+
export async function load(
|
168
|
+
key: string,
|
169
|
+
options: CacheOptions = {},
|
170
|
+
): Promise<CacheObject> {
|
171
|
+
let cacheObj: CacheObject;
|
172
|
+
|
173
|
+
let lockOk = await cache.checkLock(key, options.retry);
|
174
|
+
if (lockOk !== true) throw new Error(CacheErrorMessages.locked);
|
175
|
+
|
176
|
+
let cachedStringifiedObjet = await client().get(`${redisPrefix}${key}`);
|
177
|
+
try {
|
178
|
+
cacheObj = JSON.parse(cachedStringifiedObjet);
|
179
|
+
} catch (e) {}
|
180
|
+
|
181
|
+
if (!cacheObj) throw new Error(CacheErrorMessages.notFound);
|
182
|
+
|
183
|
+
lockOk = await cache.checkLock(key, options.retry);
|
184
|
+
if (lockOk !== true) throw new Error(CacheErrorMessages.locked);
|
185
|
+
|
186
|
+
if (options.expireTimeMS) {
|
187
|
+
await client().pexpire(redisPrefix + key, options.expireTimeMS);
|
188
|
+
return {
|
189
|
+
key,
|
190
|
+
value: cacheObj.value,
|
191
|
+
createdAt: cacheObj.createdAt,
|
192
|
+
};
|
193
|
+
} else {
|
194
|
+
return {
|
195
|
+
key,
|
196
|
+
value: cacheObj.value,
|
197
|
+
createdAt: cacheObj.createdAt,
|
198
|
+
};
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
/**
|
203
|
+
* Delete an item in the cache. Will throw an error if the item named by `key` is locked.
|
204
|
+
* Automatically handles `api.cache.redisPrefix`
|
205
|
+
*/
|
206
|
+
export async function destroy(key: string): Promise<boolean> {
|
207
|
+
const lockOk = await cache.checkLock(key, null);
|
208
|
+
if (!lockOk) throw new Error(CacheErrorMessages.locked);
|
209
|
+
|
210
|
+
const count = await client().del(redisPrefix + key);
|
211
|
+
let response = true;
|
212
|
+
if (count !== 1) {
|
213
|
+
response = false;
|
214
|
+
}
|
215
|
+
return response;
|
216
|
+
}
|
217
|
+
|
218
|
+
/**
|
219
|
+
* Save an item in the cache. If an item is already in the cache with the same key, it will be overwritten. Throws an error if the object is already in the cache and is locked.
|
220
|
+
* Automatically handles `api.cache.redisPrefix`
|
221
|
+
*/
|
222
|
+
export async function save(
|
223
|
+
key: string,
|
224
|
+
value: any,
|
225
|
+
expireTimeMS?: number,
|
226
|
+
): Promise<boolean> {
|
227
|
+
const cacheObj = {
|
228
|
+
value: value,
|
229
|
+
createdAt: new Date().getTime(),
|
230
|
+
};
|
231
|
+
|
232
|
+
const lockOk = await cache.checkLock(key, null);
|
233
|
+
if (!lockOk) throw new Error(CacheErrorMessages.locked);
|
234
|
+
|
235
|
+
await client().set(redisPrefix + key, JSON.stringify(cacheObj));
|
236
|
+
if (expireTimeMS) {
|
237
|
+
await client().pexpire(redisPrefix + key, expireTimeMS);
|
238
|
+
}
|
239
|
+
return true;
|
240
|
+
}
|
241
|
+
|
242
|
+
/**
|
243
|
+
* Push an item to a shared queue/list in redis.
|
244
|
+
* Automatically handles `api.cache.redisPrefix`
|
245
|
+
*/
|
246
|
+
export async function push(key: string, item: any): Promise<boolean> {
|
247
|
+
const object = JSON.stringify({ data: item });
|
248
|
+
await client().rpush(redisPrefix + key, object);
|
249
|
+
return true;
|
250
|
+
}
|
251
|
+
|
252
|
+
/**
|
253
|
+
* Pop (get) an item to a shared queue/list in redis.
|
254
|
+
* Automatically handles `api.cache.redisPrefix`
|
255
|
+
*/
|
256
|
+
export async function pop(key: string): Promise<boolean> {
|
257
|
+
const object = await client().lpop(redisPrefix + key);
|
258
|
+
if (!object) {
|
259
|
+
return null;
|
260
|
+
}
|
261
|
+
const item = JSON.parse(object);
|
262
|
+
return item.data;
|
263
|
+
}
|
264
|
+
|
265
|
+
/**
|
266
|
+
* Check how many items are stored in a shared queue/list in redis.
|
267
|
+
*/
|
268
|
+
export async function listLength(key: string): Promise<number> {
|
269
|
+
return client().llen(redisPrefix + key);
|
270
|
+
}
|
271
|
+
|
272
|
+
/**
|
273
|
+
* Lock an item in redis (can be a list or a saved item) to this Actionhero process.
|
274
|
+
*/
|
275
|
+
export async function lock(
|
276
|
+
key: string,
|
277
|
+
expireTimeMS: number = lockDuration,
|
278
|
+
): Promise<boolean> {
|
279
|
+
const lockOk = await cache.checkLock(key, null);
|
280
|
+
if (!lockOk) {
|
281
|
+
return false;
|
282
|
+
}
|
283
|
+
|
284
|
+
const result = await client().setnx(lockPrefix + key, lockName());
|
285
|
+
if (!result) {
|
286
|
+
return false;
|
287
|
+
} // value was already set, so we cannot obtain the lock
|
288
|
+
|
289
|
+
await client().expire(lockPrefix + key, Math.ceil(expireTimeMS / 1000));
|
290
|
+
|
291
|
+
return true;
|
292
|
+
}
|
293
|
+
|
294
|
+
/**
|
295
|
+
* Unlock an item in redis (can be a list or a saved item) which was previously locked by this Actionhero process.
|
296
|
+
*/
|
297
|
+
export async function unlock(key: string): Promise<boolean> {
|
298
|
+
const lockOk = await cache.checkLock(key, null);
|
299
|
+
|
300
|
+
if (!lockOk) {
|
301
|
+
return false;
|
302
|
+
}
|
303
|
+
|
304
|
+
await client().del(lockPrefix + key);
|
305
|
+
return true;
|
306
|
+
}
|
307
|
+
|
308
|
+
export async function checkLock(
|
309
|
+
key: string,
|
310
|
+
retry: boolean | number = false,
|
311
|
+
startTime: number = new Date().getTime(),
|
312
|
+
): Promise<boolean> {
|
313
|
+
const lockedBy = await client().get(lockPrefix + key);
|
314
|
+
if (lockedBy === lockName() || lockedBy === null) {
|
315
|
+
return true;
|
316
|
+
} else {
|
317
|
+
const delta = new Date().getTime() - startTime;
|
318
|
+
if (!retry) return false;
|
319
|
+
else if (typeof retry === "boolean") return retry;
|
320
|
+
else if (delta > retry) return false;
|
321
|
+
|
322
|
+
await utils.sleep(lockRetry);
|
323
|
+
return cache.checkLock(key, retry, startTime);
|
324
|
+
}
|
325
|
+
}
|
326
|
+
}
|