@logux/server 0.14.0 → 0.15.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 +1 -1
- package/README.md +15 -18
- package/add-http-pages/hello.html +34 -26
- package/add-sync-map/index.d.ts +1 -3
- package/add-sync-map/index.js +4 -10
- package/base-server/index.d.ts +88 -30
- package/base-server/index.js +194 -114
- package/context/index.d.ts +35 -3
- package/context/index.js +17 -6
- package/create-reporter/index.js +40 -32
- package/human-formatter/index.js +12 -7
- package/human-formatter/utils.js +14 -0
- package/index.d.ts +1 -1
- package/package.json +20 -22
- package/server/index.d.ts +9 -5
- package/server/index.js +16 -7
- package/server-client/index.d.ts +25 -1
- package/server-client/index.js +42 -9
- package/test-client/index.d.ts +5 -2
- package/test-client/index.js +8 -8
- package/test-server/index.d.ts +9 -6
- package/test-server/index.js +4 -3
- package/url-pattern/index.js +28 -0
package/human-formatter/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import os from 'node:os'
|
|
2
2
|
import { stripVTControlCharacters, styleText } from 'node:util'
|
|
3
3
|
|
|
4
|
-
import { mulberry32, onceXmur3 } from './utils.js'
|
|
4
|
+
import { isBinary, mulberry32, onceXmur3 } from './utils.js'
|
|
5
5
|
|
|
6
|
+
const BINARY_MARK = '[binary]'
|
|
6
7
|
const INDENT = ' '
|
|
7
8
|
const PADDING = ' '
|
|
8
9
|
const SEPARATOR = os.EOL + os.EOL
|
|
@@ -119,7 +120,9 @@ function formatNodeId(nodeId) {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
function formatValue(value) {
|
|
122
|
-
if (
|
|
123
|
+
if (isBinary(value)) {
|
|
124
|
+
return styleText('bold', BINARY_MARK)
|
|
125
|
+
} else if (typeof value === 'string') {
|
|
123
126
|
return '"' + styleText('bold', value) + '"'
|
|
124
127
|
} else if (Array.isArray(value)) {
|
|
125
128
|
return formatArray(value)
|
|
@@ -145,10 +148,7 @@ function formatActionId(id) {
|
|
|
145
148
|
if (p.length === 1) {
|
|
146
149
|
return p
|
|
147
150
|
}
|
|
148
|
-
return (
|
|
149
|
-
`${styleText('bold', splitAndColorize(4, p[0]))} ` +
|
|
150
|
-
`${formatNodeId(p[1])} ${styleText('bold', p[2])}`
|
|
151
|
-
)
|
|
151
|
+
return `${styleText('bold', splitAndColorize(4, p[0]))} ${formatNodeId(p[1])}`
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
function formatParams(params, parent) {
|
|
@@ -164,7 +164,12 @@ function formatParams(params, parent) {
|
|
|
164
164
|
|
|
165
165
|
let start = PADDING + rightPag(`${name}: `, maxName + 2)
|
|
166
166
|
|
|
167
|
-
if (
|
|
167
|
+
if (isBinary(value)) {
|
|
168
|
+
return start + styleText('bold', BINARY_MARK)
|
|
169
|
+
} else if (
|
|
170
|
+
name === 'Node ID' ||
|
|
171
|
+
(parent === 'Meta' && name === 'server')
|
|
172
|
+
) {
|
|
168
173
|
return start + formatNodeId(value)
|
|
169
174
|
} else if (
|
|
170
175
|
parent === 'Meta' &&
|
package/human-formatter/utils.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
export const BINARY = {
|
|
2
|
+
toJSON() {
|
|
3
|
+
return '[binary]'
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isBinary(value) {
|
|
8
|
+
return (
|
|
9
|
+
value === BINARY ||
|
|
10
|
+
ArrayBuffer.isView(value) ||
|
|
11
|
+
value instanceof ArrayBuffer
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
1
15
|
export function onceXmur3(str) {
|
|
2
16
|
let h = 1779033703 ^ str.length
|
|
3
17
|
for (let i = 0; i < str.length; i++) {
|
package/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export {
|
|
|
16
16
|
ServerMeta,
|
|
17
17
|
wasNot403
|
|
18
18
|
} from './base-server/index.js'
|
|
19
|
-
export {
|
|
19
|
+
export { Context, ConnectContext, ChannelContext } from './context/index.js'
|
|
20
20
|
export { filterMeta } from './filter-meta/index.js'
|
|
21
21
|
export {
|
|
22
22
|
del,
|
package/package.json
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logux/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Build own Logux server",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"server",
|
|
8
|
-
"framework",
|
|
9
|
-
"websocket",
|
|
10
|
-
"proxy",
|
|
6
|
+
"collaborative",
|
|
11
7
|
"crdt",
|
|
12
|
-
"event sourcing",
|
|
13
8
|
"distributed systems",
|
|
14
|
-
"
|
|
9
|
+
"event sourcing",
|
|
10
|
+
"framework",
|
|
11
|
+
"logux",
|
|
12
|
+
"proxy",
|
|
13
|
+
"server",
|
|
14
|
+
"websocket"
|
|
15
15
|
],
|
|
16
|
-
"author": "Andrey Sitnik <andrey@sitnik.ru>",
|
|
17
|
-
"license": "MIT",
|
|
18
16
|
"homepage": "https://logux.org/",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Andrey Sitnik <andrey@sitnik.es>",
|
|
19
19
|
"repository": "logux/server",
|
|
20
|
-
"engines": {
|
|
21
|
-
"node": "^20.0.0 || >=22.0.0"
|
|
22
|
-
},
|
|
23
20
|
"type": "module",
|
|
24
21
|
"types": "./index.d.ts",
|
|
25
22
|
"exports": {
|
|
@@ -27,14 +24,15 @@
|
|
|
27
24
|
"./package.json": "./package.json"
|
|
28
25
|
},
|
|
29
26
|
"dependencies": {
|
|
30
|
-
"@logux/actions": "^0.
|
|
31
|
-
"@logux/core": "^0.
|
|
32
|
-
"cookie": "^
|
|
33
|
-
"fastq": "^1.
|
|
34
|
-
"nanoevents": "^
|
|
35
|
-
"nanoid": "^
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
"@logux/actions": "^0.6.0",
|
|
28
|
+
"@logux/core": "^0.11.0",
|
|
29
|
+
"cookie": "^2.0.1",
|
|
30
|
+
"fastq": "^1.20.1",
|
|
31
|
+
"nanoevents": "^10.0.0",
|
|
32
|
+
"nanoid": "^6.0.1",
|
|
33
|
+
"ws": "^8.21.3"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": "^22.5.0 || ^24.0.0 || >=26.0.0"
|
|
39
37
|
}
|
|
40
38
|
}
|
package/server/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Log } from '@logux/core'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
2
4
|
BaseServer,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
BaseServerOptions,
|
|
6
|
+
Logger,
|
|
7
|
+
ServerMeta
|
|
5
8
|
} from '../base-server/index.js'
|
|
6
9
|
|
|
7
10
|
export interface LogStream {
|
|
@@ -62,8 +65,9 @@ export interface ServerOptions extends BaseServerOptions {
|
|
|
62
65
|
* ```
|
|
63
66
|
*/
|
|
64
67
|
export class Server<
|
|
65
|
-
Headers extends object = unknown
|
|
66
|
-
|
|
68
|
+
Headers extends object = unknown,
|
|
69
|
+
ClientData extends object = unknown
|
|
70
|
+
> extends BaseServer<Headers, Log<ServerMeta>, ClientData> {
|
|
67
71
|
/**
|
|
68
72
|
* Server options.
|
|
69
73
|
*
|
package/server/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { glob } from 'node:fs/promises'
|
|
2
|
+
import { join, matchesGlob, relative } from 'node:path'
|
|
2
3
|
import { styleText } from 'node:util'
|
|
3
|
-
import { glob } from 'tinyglobby'
|
|
4
4
|
|
|
5
5
|
import { BaseServer } from '../base-server/index.js'
|
|
6
6
|
import { createReporter } from '../create-reporter/index.js'
|
|
@@ -120,14 +120,23 @@ export class Server extends BaseServer {
|
|
|
120
120
|
files = ['modules/*/index.js', 'modules/*.js', '!**/*.{test,spec}.js']
|
|
121
121
|
) {
|
|
122
122
|
if (!Array.isArray(files)) files = [files]
|
|
123
|
-
let
|
|
124
|
-
|
|
123
|
+
let ignore = files.filter(i => i.startsWith('!')).map(i => i.slice(1))
|
|
124
|
+
let patterns = files.filter(i => !i.startsWith('!'))
|
|
125
|
+
|
|
126
|
+
let matches = new Set()
|
|
127
|
+
for await (let entry of glob(patterns, {
|
|
125
128
|
cwd: this.options.root,
|
|
126
|
-
|
|
127
|
-
})
|
|
129
|
+
withFileTypes: true
|
|
130
|
+
})) {
|
|
131
|
+
if (!entry.isFile()) continue
|
|
132
|
+
let file = join(entry.parentPath, entry.name)
|
|
133
|
+
let path = relative(this.options.root, file)
|
|
134
|
+
if (ignore.some(i => matchesGlob(path, i))) continue
|
|
135
|
+
matches.add(file)
|
|
136
|
+
}
|
|
128
137
|
|
|
129
138
|
await Promise.all(
|
|
130
|
-
matches.map(async file => {
|
|
139
|
+
[...matches].map(async file => {
|
|
131
140
|
let serverModule = (await import(file)).default
|
|
132
141
|
if (typeof serverModule === 'function') {
|
|
133
142
|
await serverModule(this)
|
package/server-client/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { BaseServer } from '../base-server/index.js'
|
|
|
9
9
|
* const client = server.connected.get(0)
|
|
10
10
|
* ```
|
|
11
11
|
*/
|
|
12
|
-
export class ServerClient {
|
|
12
|
+
export class ServerClient<ClientData extends object = unknown> {
|
|
13
13
|
/**
|
|
14
14
|
* Server, which received client.
|
|
15
15
|
*/
|
|
@@ -30,6 +30,23 @@ export class ServerClient {
|
|
|
30
30
|
*/
|
|
31
31
|
connection: ServerConnection
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Open structure to save data for the whole connection.
|
|
35
|
+
*
|
|
36
|
+
* ```js
|
|
37
|
+
* server.auth(async ({ client, userId, token }) => {
|
|
38
|
+
* let session = await findSession(token)
|
|
39
|
+
* if (!session) return false
|
|
40
|
+
* client.data.sessionId = session.id
|
|
41
|
+
* return true
|
|
42
|
+
* })
|
|
43
|
+
* server.on('disconnected', client => {
|
|
44
|
+
* touchSession(client.data.sessionId)
|
|
45
|
+
* })
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
data: ClientData
|
|
49
|
+
|
|
33
50
|
/**
|
|
34
51
|
* HTTP headers of WS connection.
|
|
35
52
|
*
|
|
@@ -100,4 +117,11 @@ export class ServerClient {
|
|
|
100
117
|
* Disconnect client.
|
|
101
118
|
*/
|
|
102
119
|
destroy(): void
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Wait until the client will confirm all sent actions.
|
|
123
|
+
*
|
|
124
|
+
* @returns Promise with `false` if the client was disconnected.
|
|
125
|
+
*/
|
|
126
|
+
drain(): Promise<boolean>
|
|
103
127
|
}
|
package/server-client/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LoguxError, parseId } from '@logux/core'
|
|
2
|
-
import
|
|
2
|
+
import { parseCookie } from 'cookie'
|
|
3
3
|
import fastq from 'fastq'
|
|
4
4
|
|
|
5
5
|
import { ALLOWED_META } from '../allowed-meta/index.js'
|
|
@@ -11,6 +11,8 @@ async function onSend(action, meta) {
|
|
|
11
11
|
return [action, filterMeta(meta)]
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function ignore() {}
|
|
15
|
+
|
|
14
16
|
function reportDetails(client) {
|
|
15
17
|
return {
|
|
16
18
|
connectionId: client.key,
|
|
@@ -70,7 +72,9 @@ export class ServerClient {
|
|
|
70
72
|
this.userId = undefined
|
|
71
73
|
this.clientId = undefined
|
|
72
74
|
this.nodeId = undefined
|
|
75
|
+
this.data = {}
|
|
73
76
|
this.processing = false
|
|
77
|
+
this.sending = new Set()
|
|
74
78
|
this.connection = connection
|
|
75
79
|
this.key = key.toString()
|
|
76
80
|
if (connection.ws) {
|
|
@@ -89,6 +93,7 @@ export class ServerClient {
|
|
|
89
93
|
onSend,
|
|
90
94
|
ping: app.options.ping,
|
|
91
95
|
subprotocol: app.options.subprotocol,
|
|
96
|
+
syncBatch: app.options.syncBatch,
|
|
92
97
|
timeout: app.options.timeout
|
|
93
98
|
})
|
|
94
99
|
if (this.app.env === 'development') {
|
|
@@ -99,11 +104,12 @@ export class ServerClient {
|
|
|
99
104
|
this.node.syncSinceQuery = async lastSynced => {
|
|
100
105
|
let context = new Context(app, this)
|
|
101
106
|
let entries = await app.connectLoader(context, lastSynced)
|
|
102
|
-
let added =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
let added = 0
|
|
108
|
+
for (let entry of entries) {
|
|
109
|
+
// `added` should be taken before `filterMeta()` removes it
|
|
110
|
+
if (entry[1].added > added) added = entry[1].added
|
|
111
|
+
entry[1] = filterMeta(entry[1])
|
|
112
|
+
}
|
|
107
113
|
return { added, entries }
|
|
108
114
|
}
|
|
109
115
|
}
|
|
@@ -157,7 +163,7 @@ export class ServerClient {
|
|
|
157
163
|
try {
|
|
158
164
|
result = await this.app.authenticator({
|
|
159
165
|
client: this,
|
|
160
|
-
cookie:
|
|
166
|
+
cookie: parseCookie(headers.cookie || ''),
|
|
161
167
|
headers: this.node.remoteHeaders,
|
|
162
168
|
token,
|
|
163
169
|
userId: this.userId
|
|
@@ -239,6 +245,23 @@ export class ServerClient {
|
|
|
239
245
|
this.app.connected.delete(this.key)
|
|
240
246
|
}
|
|
241
247
|
|
|
248
|
+
async drain() {
|
|
249
|
+
if (this.sending.size > 0) await Promise.all(this.sending)
|
|
250
|
+
if (this.destroyed || !this.node.connected) return false
|
|
251
|
+
if (this.node.syncing === 0) return true
|
|
252
|
+
return new Promise(resolve => {
|
|
253
|
+
let unbind = this.node.on('state', () => {
|
|
254
|
+
if (this.node.state === 'synchronized') {
|
|
255
|
+
unbind()
|
|
256
|
+
resolve(true)
|
|
257
|
+
} else if (!this.node.connected) {
|
|
258
|
+
unbind()
|
|
259
|
+
resolve(false)
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
242
265
|
onReceive(action, meta) {
|
|
243
266
|
if (this.app.actionToQueue.has(meta.id)) {
|
|
244
267
|
return Promise.resolve(false)
|
|
@@ -263,8 +286,10 @@ export class ServerClient {
|
|
|
263
286
|
|
|
264
287
|
if (isChannel) {
|
|
265
288
|
for (let channel of this.app.channels) {
|
|
266
|
-
let
|
|
267
|
-
|
|
289
|
+
let match = channel.regexp
|
|
290
|
+
? action.channel.match(channel.regexp)
|
|
291
|
+
: channel.pattern(action.channel)
|
|
292
|
+
if (match) {
|
|
268
293
|
queueName = channel.queue
|
|
269
294
|
break
|
|
270
295
|
}
|
|
@@ -299,4 +324,12 @@ export class ServerClient {
|
|
|
299
324
|
})
|
|
300
325
|
})
|
|
301
326
|
}
|
|
327
|
+
|
|
328
|
+
track(sending) {
|
|
329
|
+
let done = Promise.resolve(sending).then(ignore, ignore)
|
|
330
|
+
this.sending.add(done)
|
|
331
|
+
void done.then(() => {
|
|
332
|
+
this.sending.delete(done)
|
|
333
|
+
})
|
|
334
|
+
}
|
|
302
335
|
}
|
package/test-client/index.d.ts
CHANGED
|
@@ -141,7 +141,7 @@ export class TestClient {
|
|
|
141
141
|
* @param test Function, where do you expect action will be received
|
|
142
142
|
* @returns Promise with all received actions
|
|
143
143
|
*/
|
|
144
|
-
collect(test: () => Promise<
|
|
144
|
+
collect(test: () => Promise<unknown>): Promise<Action[]>
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
147
|
* Connect to test server.
|
|
@@ -180,7 +180,10 @@ export class TestClient {
|
|
|
180
180
|
* @param meta Optional action’s meta.
|
|
181
181
|
* @returns Promise until `logux/processed` answer.
|
|
182
182
|
*/
|
|
183
|
-
process
|
|
183
|
+
process<TypeAction extends Action = AnyAction>(
|
|
184
|
+
action: TypeAction,
|
|
185
|
+
meta?: Partial<ServerMeta>
|
|
186
|
+
): Promise<Action[]>
|
|
184
187
|
|
|
185
188
|
/**
|
|
186
189
|
* Collect actions received from server during the `test` call.
|
package/test-client/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ClientNode, TestPair } from '@logux/core'
|
|
2
|
-
import
|
|
2
|
+
import { stringifyCookie } from 'cookie'
|
|
3
3
|
import { setTimeout } from 'node:timers/promises'
|
|
4
4
|
|
|
5
5
|
import { filterMeta } from '../filter-meta/index.js'
|
|
@@ -34,9 +34,9 @@ export class TestClient {
|
|
|
34
34
|
this.node.setLocalHeaders(opts.headers)
|
|
35
35
|
}
|
|
36
36
|
if (opts.cookie) {
|
|
37
|
-
this.pair.right.ws.upgradeReq.headers.cookie =
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
this.pair.right.ws.upgradeReq.headers.cookie = stringifyCookie(
|
|
38
|
+
opts.cookie
|
|
39
|
+
)
|
|
40
40
|
}
|
|
41
41
|
server.unbind.push(() => {
|
|
42
42
|
this.node.destroy()
|
|
@@ -46,7 +46,7 @@ export class TestClient {
|
|
|
46
46
|
async collect(test) {
|
|
47
47
|
let added = []
|
|
48
48
|
let unbind = this.node.log.on('add', (action, meta) => {
|
|
49
|
-
if (
|
|
49
|
+
if (meta.id.split(' ')[1] !== this.nodeId) {
|
|
50
50
|
added.push(action)
|
|
51
51
|
}
|
|
52
52
|
})
|
|
@@ -67,8 +67,8 @@ export class TestClient {
|
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
this.server.addClient(this.pair.right)
|
|
70
|
-
this.node.connection.connect()
|
|
71
|
-
this.node.waitFor('synchronized').then(() => {
|
|
70
|
+
void this.node.connection.connect()
|
|
71
|
+
void this.node.waitFor('synchronized').then(() => {
|
|
72
72
|
this.node.throwsError = true
|
|
73
73
|
unbind()
|
|
74
74
|
resolve()
|
|
@@ -136,7 +136,7 @@ export class TestClient {
|
|
|
136
136
|
async received(test) {
|
|
137
137
|
let actions = []
|
|
138
138
|
let unbind = this.log.on('add', (action, meta) => {
|
|
139
|
-
if (
|
|
139
|
+
if (meta.id.split(' ')[1] !== this.nodeId) {
|
|
140
140
|
actions.push(action)
|
|
141
141
|
}
|
|
142
142
|
})
|
package/test-server/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { TestLog, TestTime } from '@logux/core'
|
|
1
|
+
import type { Log, TestLog, TestTime } from '@logux/core'
|
|
2
2
|
|
|
3
|
-
import { BaseServer } from '../base-server/index.js'
|
|
4
3
|
import type {
|
|
4
|
+
BaseServer,
|
|
5
5
|
BaseServerOptions,
|
|
6
6
|
Logger,
|
|
7
7
|
ServerMeta
|
|
@@ -9,8 +9,10 @@ import type {
|
|
|
9
9
|
import type { LoggerOptions } from '../server/index.js'
|
|
10
10
|
import type { TestClient, TestClientOptions } from '../test-client/index.js'
|
|
11
11
|
|
|
12
|
-
export interface TestServerOptions
|
|
13
|
-
|
|
12
|
+
export interface TestServerOptions extends Omit<
|
|
13
|
+
BaseServerOptions,
|
|
14
|
+
'minSubprotocol' | 'subprotocol'
|
|
15
|
+
> {
|
|
14
16
|
/**
|
|
15
17
|
* Disable built-in auth.
|
|
16
18
|
*/
|
|
@@ -46,8 +48,9 @@ export interface TestServerOptions
|
|
|
46
48
|
* ```
|
|
47
49
|
*/
|
|
48
50
|
export class TestServer<
|
|
49
|
-
Headers extends object = unknown
|
|
50
|
-
|
|
51
|
+
Headers extends object = unknown,
|
|
52
|
+
ClientData extends object = unknown
|
|
53
|
+
> extends BaseServer<Headers, Log<ServerMeta>, ClientData> {
|
|
51
54
|
/**
|
|
52
55
|
* fetch() compatible API to test HTTP endpoints.
|
|
53
56
|
*
|
package/test-server/index.js
CHANGED
|
@@ -34,8 +34,8 @@ export class TestServer extends BaseServer {
|
|
|
34
34
|
|
|
35
35
|
this.fetch = this.fetch.bind(this)
|
|
36
36
|
|
|
37
|
-
this.on('fatal', () => {
|
|
38
|
-
this.destroy()
|
|
37
|
+
this.on('fatal', async () => {
|
|
38
|
+
await this.destroy()
|
|
39
39
|
})
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -72,7 +72,8 @@ export class TestServer extends BaseServer {
|
|
|
72
72
|
if (e.message === 'Server undid action') {
|
|
73
73
|
if (e.action.reason !== reason) {
|
|
74
74
|
throw new Error(
|
|
75
|
-
`Undo was with ${e.action.reason} reason, not ${reason}
|
|
75
|
+
`Undo was with ${e.action.reason} reason, not ${reason}`,
|
|
76
|
+
{ cause: e }
|
|
76
77
|
)
|
|
77
78
|
}
|
|
78
79
|
} else {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const PARAM = /:(\w+)/g
|
|
2
|
+
const SPECIAL = /[.*+?^${}()|[\]\\]/g
|
|
3
|
+
|
|
4
|
+
function escape(text) {
|
|
5
|
+
return text.replace(SPECIAL, '\\$&')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createPattern(pattern) {
|
|
9
|
+
let names = []
|
|
10
|
+
let source = ''
|
|
11
|
+
let last = 0
|
|
12
|
+
for (let param of pattern.matchAll(PARAM)) {
|
|
13
|
+
source += escape(pattern.slice(last, param.index)) + '([^/]+)'
|
|
14
|
+
names.push(param[1])
|
|
15
|
+
last = param.index + param[0].length
|
|
16
|
+
}
|
|
17
|
+
let regexp = new RegExp('^' + source + escape(pattern.slice(last)) + '$')
|
|
18
|
+
|
|
19
|
+
return url => {
|
|
20
|
+
let match = regexp.exec(url)
|
|
21
|
+
if (!match) return null
|
|
22
|
+
let params = {}
|
|
23
|
+
for (let i = 0; i < names.length; i++) {
|
|
24
|
+
if (names[i] !== '__proto__') params[names[i]] = match[i + 1]
|
|
25
|
+
}
|
|
26
|
+
return params
|
|
27
|
+
}
|
|
28
|
+
}
|