@lsdsoftware/utils 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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/connection-manager.d.ts +13 -0
- package/dist/connection-manager.js +70 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +13 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +24 -0
- package/dist/line-split-stream.d.ts +3 -0
- package/dist/line-split-stream.js +28 -0
- package/dist/line-split-stream.test.d.ts +6 -0
- package/dist/line-split-stream.test.js +58 -0
- package/dist/message-dispatcher.d.ts +29 -0
- package/dist/message-dispatcher.js +72 -0
- package/dist/rate-limiter.d.ts +7 -0
- package/dist/rate-limiter.js +43 -0
- package/dist/rate-limiter.test.d.ts +4 -0
- package/dist/rate-limiter.test.js +39 -0
- package/dist/state-machine.d.ts +7 -0
- package/dist/state-machine.js +36 -0
- package/dist/state-machine.test.d.ts +4 -0
- package/dist/state-machine.test.js +69 -0
- package/package.json +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Sarsa Parilla
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Useful JavaScript utilities
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## State Machine
|
|
5
|
+
Make implementing state machines less error prone.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { makeStateMachine } from "@lsdsoftware/utils"
|
|
9
|
+
|
|
10
|
+
const sm = makeStateMachine({
|
|
11
|
+
IDLE: {
|
|
12
|
+
startIt() {
|
|
13
|
+
//do something
|
|
14
|
+
return "BUSY"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
BUSY: {
|
|
18
|
+
stopIt() {
|
|
19
|
+
//stop doing it
|
|
20
|
+
return "IDLE"
|
|
21
|
+
},
|
|
22
|
+
stopAfterDelay() {
|
|
23
|
+
return "STOPPING"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
STOPPING: {
|
|
27
|
+
onTransitionIn(this: any) {
|
|
28
|
+
//do some clean up
|
|
29
|
+
this.timer = setTimeout(() => sm.trigger("onDone"), 3000)
|
|
30
|
+
},
|
|
31
|
+
onDone() {
|
|
32
|
+
return "IDLE"
|
|
33
|
+
},
|
|
34
|
+
stopIt() {
|
|
35
|
+
console.log("Already stopping, be patient!")
|
|
36
|
+
//return void to stay in same state
|
|
37
|
+
},
|
|
38
|
+
forceIt(this: any) {
|
|
39
|
+
clearTimeout(this.timer)
|
|
40
|
+
return "IDLE"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
sm.trigger("startIt")
|
|
46
|
+
sm.getState() //BUSY
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Message Dispatcher
|
|
52
|
+
Dispatch messages to handlers. This utility assumes messages are one of three types: request, response, or notification, and follow a predefined format (see type definition below).
|
|
53
|
+
|
|
54
|
+
Requests and notifications are dispatched to provided request handlers, and responses are generated from their return values. Responses are dispatched to response listeners. The caller registers a response listener by calling the waitForResponse method, which returns a promise.
|
|
55
|
+
|
|
56
|
+
When constructing a dispatcher, caller provides a _myAddress_ parameter used to filter messages. Only requests and notifications whose _to_ attribute matches _myAddress_ will be processed.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { makeMessageDispatcher } from "@lsdsoftware/utils"
|
|
60
|
+
|
|
61
|
+
const requestHandlers = {
|
|
62
|
+
method1({paramA, paramB}, sender) {
|
|
63
|
+
//do something
|
|
64
|
+
return result
|
|
65
|
+
},
|
|
66
|
+
async method2({x, y, z}, sender) {
|
|
67
|
+
//do something
|
|
68
|
+
return result
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const dispatcher = makeMessageDispatcher("myAddress", requestHandlers)
|
|
73
|
+
|
|
74
|
+
//sample usage
|
|
75
|
+
|
|
76
|
+
//processing requests and responses
|
|
77
|
+
window.addEventListener("message", event => {
|
|
78
|
+
const sender = {window: event.source, origin: event.origin}
|
|
79
|
+
const sendResponse = response => sender.window.postMessage(response, sender.origin)
|
|
80
|
+
dispatcher.dispatch(event.data, sender, sendResponse)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
//sending requests
|
|
84
|
+
const id = String(Math.random())
|
|
85
|
+
const request = {to: "someAddress", type: "request", id, method: "someMethod", args: {}}
|
|
86
|
+
iframeWindow.postMessage(request, "*")
|
|
87
|
+
dispatcher.waitForResponse(id)
|
|
88
|
+
.then(result => console.log(result))
|
|
89
|
+
.catch(console.error)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## Rate Limiter
|
|
95
|
+
Basic rate limiter using the token bucket algorithm.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { RateLimiter } from "@lsdsoftware/utils"
|
|
99
|
+
|
|
100
|
+
const limiter = new RateLimiter({tokensPerInterval: 5, interval: 60*1000})
|
|
101
|
+
|
|
102
|
+
function handleRequest(userId, req) {
|
|
103
|
+
if (limiter.tryRemoveTokens(userId, 1)) return processRequest(req)
|
|
104
|
+
else throw "Rate limit exceeded"
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## Connection Manager
|
|
111
|
+
Takes a connect() method and:
|
|
112
|
+
- Only call it to create a connection when needed
|
|
113
|
+
- Automatically retry on failure
|
|
114
|
+
- Automatically reconnect if the previous connection was closed
|
|
115
|
+
- Properly handle shutdown sequence
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { makeConnectionManager } from "@lsdsoftware/utils"
|
|
119
|
+
|
|
120
|
+
const conMgr = new ConnectionManager({
|
|
121
|
+
async connect() {
|
|
122
|
+
//...
|
|
123
|
+
return connection
|
|
124
|
+
},
|
|
125
|
+
retryDelay: 10*1000
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
//wherever you need the connection
|
|
129
|
+
const connection = await conMgr.get()
|
|
130
|
+
|
|
131
|
+
//shutdown
|
|
132
|
+
conMgr.shutdown()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
## Line Split Stream
|
|
138
|
+
A Transform stream that transforms a byte stream into lines of text (by GPT-4)
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { makeLineSplitStream } from "@lsdsoftware/utils"
|
|
142
|
+
|
|
143
|
+
const splitter = makeLineSplitStream()
|
|
144
|
+
splitter.on('data', line => console.log('Received line:', line))
|
|
145
|
+
|
|
146
|
+
// Simulating input
|
|
147
|
+
splitter.write('This is a line\nThis is another line\nAnd this is a line as well')
|
|
148
|
+
splitter.end() // End the stream to see the _flush effect.
|
|
149
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface Closeable {
|
|
2
|
+
close(): void;
|
|
3
|
+
once(event: "close", callback: Function): void;
|
|
4
|
+
}
|
|
5
|
+
export interface ConnectionManager<Connection> {
|
|
6
|
+
get(): Promise<Connection>;
|
|
7
|
+
shutdown(): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function makeConnectionManager<Connection extends Closeable>({ connect, retryDelay }: {
|
|
10
|
+
connect(): Promise<Connection>;
|
|
11
|
+
retryDelay: number;
|
|
12
|
+
}): ConnectionManager<Connection>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.makeConnectionManager = void 0;
|
|
13
|
+
function makeConnectionManager({ connect, retryDelay }) {
|
|
14
|
+
let connectionPromise;
|
|
15
|
+
let shutdownFlag = false;
|
|
16
|
+
return {
|
|
17
|
+
get() {
|
|
18
|
+
if (!connectionPromise)
|
|
19
|
+
connectionPromise = start();
|
|
20
|
+
return connectionPromise;
|
|
21
|
+
},
|
|
22
|
+
shutdown() {
|
|
23
|
+
shutdownFlag = true;
|
|
24
|
+
connectionPromise === null || connectionPromise === void 0 ? void 0 : connectionPromise.then(con => con.close()).catch(err => "OK");
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function start() {
|
|
28
|
+
return new Promise(fulfill => {
|
|
29
|
+
let firstTime = true;
|
|
30
|
+
keepAlive(promise => {
|
|
31
|
+
if (firstTime) {
|
|
32
|
+
fulfill(promise);
|
|
33
|
+
firstTime = false;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
connectionPromise = promise;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function keepAlive(onUpdate) {
|
|
42
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
try {
|
|
44
|
+
while (true) {
|
|
45
|
+
const promise = connectUntilSucceed();
|
|
46
|
+
onUpdate(promise);
|
|
47
|
+
const connection = yield promise;
|
|
48
|
+
yield new Promise(f => connection.once("close", f));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function connectUntilSucceed() {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
while (true) {
|
|
58
|
+
if (shutdownFlag)
|
|
59
|
+
throw new Error("Shutting down");
|
|
60
|
+
try {
|
|
61
|
+
return yield connect();
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
yield new Promise(f => setTimeout(f, retryDelay));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.makeConnectionManager = makeConnectionManager;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { makeLineSplitStream } from "./line-split-stream";
|
|
2
|
+
import { makeStateMachine } from "./state-machine";
|
|
3
|
+
import { makeConnectionManager } from "./connection-manager";
|
|
4
|
+
import { makeMessageDispatcher } from "./message-dispatcher";
|
|
5
|
+
import { makeRateLimiter } from "./rate-limiter";
|
|
6
|
+
export { makeLineSplitStream, makeStateMachine, makeConnectionManager, makeMessageDispatcher, makeRateLimiter, };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeRateLimiter = exports.makeMessageDispatcher = exports.makeConnectionManager = exports.makeStateMachine = exports.makeLineSplitStream = void 0;
|
|
4
|
+
const line_split_stream_1 = require("./line-split-stream");
|
|
5
|
+
Object.defineProperty(exports, "makeLineSplitStream", { enumerable: true, get: function () { return line_split_stream_1.makeLineSplitStream; } });
|
|
6
|
+
const state_machine_1 = require("./state-machine");
|
|
7
|
+
Object.defineProperty(exports, "makeStateMachine", { enumerable: true, get: function () { return state_machine_1.makeStateMachine; } });
|
|
8
|
+
const connection_manager_1 = require("./connection-manager");
|
|
9
|
+
Object.defineProperty(exports, "makeConnectionManager", { enumerable: true, get: function () { return connection_manager_1.makeConnectionManager; } });
|
|
10
|
+
const message_dispatcher_1 = require("./message-dispatcher");
|
|
11
|
+
Object.defineProperty(exports, "makeMessageDispatcher", { enumerable: true, get: function () { return message_dispatcher_1.makeMessageDispatcher; } });
|
|
12
|
+
const rate_limiter_1 = require("./rate-limiter");
|
|
13
|
+
Object.defineProperty(exports, "makeRateLimiter", { enumerable: true, get: function () { return rate_limiter_1.makeRateLimiter; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const line_split_stream_test_1 = require("./line-split-stream.test");
|
|
13
|
+
const rate_limiter_test_1 = require("./rate-limiter.test");
|
|
14
|
+
const state_machine_test_1 = require("./state-machine.test");
|
|
15
|
+
run(Object.assign(Object.assign(Object.assign({}, line_split_stream_test_1.default), rate_limiter_test_1.default), state_machine_test_1.default))
|
|
16
|
+
.catch(console.error);
|
|
17
|
+
function run(tests) {
|
|
18
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19
|
+
for (const name in tests) {
|
|
20
|
+
console.log("Running test", name);
|
|
21
|
+
yield tests[name]();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeLineSplitStream = void 0;
|
|
4
|
+
const stream_1 = require("stream");
|
|
5
|
+
function makeLineSplitStream() {
|
|
6
|
+
// Buffer for the part of the chunk that doesn't form a complete line.
|
|
7
|
+
let remainder = '';
|
|
8
|
+
return new stream_1.Transform({
|
|
9
|
+
transform(chunk, encoding, callback) {
|
|
10
|
+
// Convert chunk to string and add it to the remainder.
|
|
11
|
+
const chunkStr = remainder + chunk.toString();
|
|
12
|
+
const lines = chunkStr.split('\n');
|
|
13
|
+
// Keep the last line in remainder if it doesn't end with a newline character.
|
|
14
|
+
remainder = lines.pop();
|
|
15
|
+
// Push each complete line.
|
|
16
|
+
for (const line of lines)
|
|
17
|
+
this.push(line + '\n');
|
|
18
|
+
callback();
|
|
19
|
+
},
|
|
20
|
+
flush(callback) {
|
|
21
|
+
// When the stream is ending, push any remainder as a line if it's not empty.
|
|
22
|
+
if (remainder)
|
|
23
|
+
this.push(remainder + '\n');
|
|
24
|
+
callback();
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
exports.makeLineSplitStream = makeLineSplitStream;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const line_split_stream_1 = require("./line-split-stream");
|
|
13
|
+
const assert = require("assert");
|
|
14
|
+
exports.default = {
|
|
15
|
+
lineSplitStream1() {
|
|
16
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
17
|
+
const splitter = (0, line_split_stream_1.makeLineSplitStream)();
|
|
18
|
+
splitter.write('This is a line\nThis is another line\nAnd this is a line as well');
|
|
19
|
+
splitter.end();
|
|
20
|
+
const lines = yield new Promise(fulfill => {
|
|
21
|
+
let accum = [];
|
|
22
|
+
splitter.on('data', line => accum.push(line));
|
|
23
|
+
splitter.on('end', () => fulfill(accum));
|
|
24
|
+
});
|
|
25
|
+
assert(lines[0] == "This is a line\n" &&
|
|
26
|
+
lines[1] == "This is another line\n" &&
|
|
27
|
+
lines[2] == "And this is a line as well\n");
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
lineSplitStream2() {
|
|
31
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
const splitter = (0, line_split_stream_1.makeLineSplitStream)();
|
|
33
|
+
splitter.write('\nThis is a line\n');
|
|
34
|
+
splitter.end();
|
|
35
|
+
const lines = yield new Promise(fulfill => {
|
|
36
|
+
let accum = [];
|
|
37
|
+
splitter.on('data', line => accum.push(line));
|
|
38
|
+
splitter.on('end', () => fulfill(accum));
|
|
39
|
+
});
|
|
40
|
+
assert(lines[0] == "\n" &&
|
|
41
|
+
lines[1] == "This is a line\n");
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
lineSplitStream3() {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
const splitter = (0, line_split_stream_1.makeLineSplitStream)();
|
|
47
|
+
splitter.write('\n\n');
|
|
48
|
+
splitter.end();
|
|
49
|
+
const lines = yield new Promise(fulfill => {
|
|
50
|
+
let accum = [];
|
|
51
|
+
splitter.on('data', line => accum.push(line));
|
|
52
|
+
splitter.on('end', () => fulfill(accum));
|
|
53
|
+
});
|
|
54
|
+
assert(lines[0] == "\n" &&
|
|
55
|
+
lines[1] == "\n");
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type Message = Request | Notification | Response;
|
|
2
|
+
interface Request {
|
|
3
|
+
to: string;
|
|
4
|
+
type: "request";
|
|
5
|
+
id: "string";
|
|
6
|
+
method: string;
|
|
7
|
+
args: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
interface Notification {
|
|
10
|
+
to: string;
|
|
11
|
+
type: "notification";
|
|
12
|
+
method: string;
|
|
13
|
+
args: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
interface Response {
|
|
16
|
+
type: "response";
|
|
17
|
+
id: string;
|
|
18
|
+
error: unknown;
|
|
19
|
+
result: unknown;
|
|
20
|
+
}
|
|
21
|
+
interface RequestHandler {
|
|
22
|
+
(args: Record<string, unknown>, sender: unknown): unknown;
|
|
23
|
+
}
|
|
24
|
+
export declare function makeMessageDispatcher(myAddress: string, requestHandlers: Record<string, RequestHandler>): {
|
|
25
|
+
waitForResponse<T>(requestId: string): Promise<T>;
|
|
26
|
+
dispatch(message: Message, sender: unknown, sendResponse: (res: Response) => void): boolean | void;
|
|
27
|
+
updateRequestHandlers(newHandlers: typeof requestHandlers): void;
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeMessageDispatcher = void 0;
|
|
4
|
+
function makeMessageDispatcher(myAddress, requestHandlers) {
|
|
5
|
+
const pendingRequests = new Map();
|
|
6
|
+
return {
|
|
7
|
+
waitForResponse(requestId) {
|
|
8
|
+
let pending = pendingRequests.get(requestId);
|
|
9
|
+
if (!pending)
|
|
10
|
+
pendingRequests.set(requestId, pending = makePending());
|
|
11
|
+
return pending.promise;
|
|
12
|
+
},
|
|
13
|
+
dispatch(message, sender, sendResponse) {
|
|
14
|
+
switch (message.type) {
|
|
15
|
+
case "request": return handleRequest(message, sender, sendResponse);
|
|
16
|
+
case "notification": return handleNotification(message, sender);
|
|
17
|
+
case "response": return handleResponse(message);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
updateRequestHandlers(newHandlers) {
|
|
21
|
+
requestHandlers = newHandlers;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
function makePending() {
|
|
25
|
+
const pending = {};
|
|
26
|
+
pending.promise = new Promise((fulfill, reject) => {
|
|
27
|
+
pending.fulfill = fulfill;
|
|
28
|
+
pending.reject = reject;
|
|
29
|
+
});
|
|
30
|
+
return pending;
|
|
31
|
+
}
|
|
32
|
+
function handleRequest(req, sender, sendResponse) {
|
|
33
|
+
if (req.to == myAddress) {
|
|
34
|
+
if (requestHandlers[req.method]) {
|
|
35
|
+
Promise.resolve()
|
|
36
|
+
.then(() => requestHandlers[req.method](req.args, sender))
|
|
37
|
+
.then(result => sendResponse({ type: "response", id: req.id, result, error: undefined }), error => sendResponse({ type: "response", id: req.id, result: undefined, error }));
|
|
38
|
+
//let caller know that sendResponse will be called asynchronously
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.error("No handler for method", req);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function handleNotification(ntf, sender) {
|
|
47
|
+
if (ntf.to == myAddress) {
|
|
48
|
+
if (requestHandlers[ntf.method]) {
|
|
49
|
+
Promise.resolve()
|
|
50
|
+
.then(() => requestHandlers[ntf.method](ntf.args, sender))
|
|
51
|
+
.catch(error => console.error("Failed to handle notification", ntf, error));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.error("No handler for method", ntf);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function handleResponse(res) {
|
|
59
|
+
const pending = pendingRequests.get(res.id);
|
|
60
|
+
if (pending) {
|
|
61
|
+
pendingRequests.delete(res.id);
|
|
62
|
+
if (res.error)
|
|
63
|
+
pending.reject(res.error);
|
|
64
|
+
else
|
|
65
|
+
pending.fulfill(res.result);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.error("Stray response", res);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.makeMessageDispatcher = makeMessageDispatcher;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeRateLimiter = void 0;
|
|
4
|
+
class Bucket {
|
|
5
|
+
constructor(count, expire) {
|
|
6
|
+
this.count = count;
|
|
7
|
+
this.expire = expire;
|
|
8
|
+
}
|
|
9
|
+
isValid() {
|
|
10
|
+
return this.expire > Date.now();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function makeRateLimiter({ interval, tokensPerInterval }) {
|
|
14
|
+
const buckets = new Map();
|
|
15
|
+
let lastCleanup = Date.now();
|
|
16
|
+
return {
|
|
17
|
+
getTokensRemaining(key) {
|
|
18
|
+
const bucket = buckets.get(key);
|
|
19
|
+
return (bucket === null || bucket === void 0 ? void 0 : bucket.isValid()) ? bucket.count : tokensPerInterval;
|
|
20
|
+
},
|
|
21
|
+
tryRemoveTokens(key, numTokens) {
|
|
22
|
+
if (Date.now() - lastCleanup > 2 * interval) {
|
|
23
|
+
lastCleanup = Date.now();
|
|
24
|
+
for (const [key, bucket] of buckets)
|
|
25
|
+
if (!bucket.isValid())
|
|
26
|
+
buckets.delete(key);
|
|
27
|
+
}
|
|
28
|
+
let bucket = buckets.get(key);
|
|
29
|
+
if (!(bucket === null || bucket === void 0 ? void 0 : bucket.isValid())) {
|
|
30
|
+
bucket = new Bucket(tokensPerInterval, Date.now() + interval);
|
|
31
|
+
buckets.set(key, bucket);
|
|
32
|
+
}
|
|
33
|
+
if (numTokens <= bucket.count) {
|
|
34
|
+
bucket.count -= numTokens;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
exports.makeRateLimiter = makeRateLimiter;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const rate_limiter_1 = require("./rate-limiter");
|
|
13
|
+
const assert = require("assert");
|
|
14
|
+
exports.default = {
|
|
15
|
+
rateLimiter1() {
|
|
16
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
17
|
+
const limiter = (0, rate_limiter_1.makeRateLimiter)({ tokensPerInterval: 5, interval: 2000 });
|
|
18
|
+
assert(limiter.getTokensRemaining("k1") == 5);
|
|
19
|
+
assert(limiter.tryRemoveTokens("k1", 3) == true);
|
|
20
|
+
assert(limiter.getTokensRemaining("k1") == 2);
|
|
21
|
+
assert(limiter.tryRemoveTokens("k1", 4) == false);
|
|
22
|
+
assert(limiter.tryRemoveTokens("k2", 4) == true);
|
|
23
|
+
yield new Promise(f => setTimeout(f, 1500));
|
|
24
|
+
assert(limiter.tryRemoveTokens("k1", 4) == false);
|
|
25
|
+
assert(limiter.getTokensRemaining("k1") == 2);
|
|
26
|
+
yield new Promise(f => setTimeout(f, 1000));
|
|
27
|
+
assert(limiter.tryRemoveTokens("k1", 4) == true);
|
|
28
|
+
//new bucket, won't expire until 2 seconds from now
|
|
29
|
+
assert(limiter.getTokensRemaining("k1") == 1);
|
|
30
|
+
assert(limiter.tryRemoveTokens("k1", 2) == false);
|
|
31
|
+
yield new Promise(f => setTimeout(f, 1900));
|
|
32
|
+
assert(limiter.tryRemoveTokens("k1", 2) == false);
|
|
33
|
+
assert(limiter.getTokensRemaining("k1") == 1);
|
|
34
|
+
yield new Promise(f => setTimeout(f, 200));
|
|
35
|
+
assert(limiter.tryRemoveTokens("k1", 2) == true);
|
|
36
|
+
assert(limiter.getTokensRemaining("k1") == 3);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type EventHandler<StateName> = (...args: any) => StateName | void;
|
|
2
|
+
export type State<StateName extends string, EventName extends string> = Partial<Record<EventName | "onTransitionIn", EventHandler<StateName>>>;
|
|
3
|
+
export declare function makeStateMachine<StateName extends string, EventName extends string>(states: Record<StateName | "IDLE", State<StateName, EventName>>): {
|
|
4
|
+
trigger(eventName: EventName, ...args: any): void;
|
|
5
|
+
getState(): StateName | "IDLE";
|
|
6
|
+
};
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeStateMachine = void 0;
|
|
4
|
+
function makeStateMachine(states) {
|
|
5
|
+
var _a, _b;
|
|
6
|
+
let currentStateName = "IDLE";
|
|
7
|
+
(_b = (_a = states[currentStateName]).onTransitionIn) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
8
|
+
let lock = 0;
|
|
9
|
+
return {
|
|
10
|
+
trigger(eventName, ...args) {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
if (lock)
|
|
13
|
+
throw new Error("Cannot trigger an event synchronously while inside an event handler");
|
|
14
|
+
lock++;
|
|
15
|
+
try {
|
|
16
|
+
const currentState = states[currentStateName];
|
|
17
|
+
if (!(eventName in currentState))
|
|
18
|
+
throw new Error("Missing handler " + currentStateName + "." + eventName);
|
|
19
|
+
const nextStateName = currentState[eventName](...args);
|
|
20
|
+
if (nextStateName) {
|
|
21
|
+
if (!(nextStateName in states))
|
|
22
|
+
throw new Error("Missing state " + nextStateName);
|
|
23
|
+
currentStateName = nextStateName;
|
|
24
|
+
(_b = (_a = states[currentStateName]).onTransitionIn) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
lock--;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
getState() {
|
|
32
|
+
return currentStateName;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
exports.makeStateMachine = makeStateMachine;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const state_machine_1 = require("./state-machine");
|
|
13
|
+
const assert = require("assert");
|
|
14
|
+
exports.default = {
|
|
15
|
+
stateMachine1() {
|
|
16
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
17
|
+
const sm = (0, state_machine_1.makeStateMachine)({
|
|
18
|
+
IDLE: {
|
|
19
|
+
play() {
|
|
20
|
+
return "PLAYING";
|
|
21
|
+
},
|
|
22
|
+
stop() { },
|
|
23
|
+
goto(state) {
|
|
24
|
+
return state;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
PLAYING: {
|
|
28
|
+
play() { },
|
|
29
|
+
stop() {
|
|
30
|
+
return "STOPPING";
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
STOPPING: {
|
|
34
|
+
onTransitionIn() {
|
|
35
|
+
this.timer = setTimeout(() => sm.trigger("onStop"), 2000);
|
|
36
|
+
},
|
|
37
|
+
onStop() {
|
|
38
|
+
return "IDLE";
|
|
39
|
+
},
|
|
40
|
+
play() {
|
|
41
|
+
clearTimeout(this.timer);
|
|
42
|
+
return "PLAYING";
|
|
43
|
+
},
|
|
44
|
+
stop() { }
|
|
45
|
+
},
|
|
46
|
+
STUCK: {}
|
|
47
|
+
});
|
|
48
|
+
assert(sm.getState() == "IDLE");
|
|
49
|
+
sm.trigger("play");
|
|
50
|
+
assert(sm.getState() == "PLAYING");
|
|
51
|
+
sm.trigger("stop");
|
|
52
|
+
assert(sm.getState() == "STOPPING");
|
|
53
|
+
yield sleep(1000);
|
|
54
|
+
sm.trigger("play");
|
|
55
|
+
assert(sm.getState() == "PLAYING");
|
|
56
|
+
yield sleep(3000);
|
|
57
|
+
assert(sm.getState() == "PLAYING");
|
|
58
|
+
sm.trigger("stop");
|
|
59
|
+
assert(sm.getState() == "STOPPING");
|
|
60
|
+
yield sleep(3000);
|
|
61
|
+
assert(sm.getState() == "IDLE");
|
|
62
|
+
sm.trigger("goto", "STUCK");
|
|
63
|
+
assert(sm.getState() == "STUCK");
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
function sleep(ms) {
|
|
68
|
+
return new Promise(f => setTimeout(f, ms));
|
|
69
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lsdsoftware/utils",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Useful JavaScript utilities",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/*"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"test": "node dist/index.test"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/lsdsoftware/utils.git"
|
|
16
|
+
},
|
|
17
|
+
"author": "Hai Phan <hai.phan@gmail.com>",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/lsdsoftware/utils/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/lsdsoftware/utils#readme",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.11.20",
|
|
25
|
+
"typescript": "^5.3.3"
|
|
26
|
+
}
|
|
27
|
+
}
|