@logux/server 0.10.7 → 0.12.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/add-sync-map/index.d.ts +243 -0
- package/add-sync-map/index.js +207 -0
- package/base-server/index.d.ts +11 -3
- package/base-server/index.js +17 -2
- package/bind-backend-proxy/index.js +1 -1
- package/bind-control-server/index.js +11 -0
- package/create-reporter/index.js +33 -17
- package/human-formatter/index.js +96 -17
- package/human-formatter/utils.js +20 -0
- package/index.d.ts +9 -0
- package/index.js +6 -0
- package/package.json +15 -14
- package/test-client/index.d.ts +5 -3
- package/test-client/index.js +4 -1
- package/test-server/index.d.ts +12 -0
- package/test-server/index.js +18 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LoguxSubscribeAction,
|
|
3
|
+
SyncMapChangeAction,
|
|
4
|
+
SyncMapCreateAction,
|
|
5
|
+
SyncMapDeleteAction,
|
|
6
|
+
SyncMapValues,
|
|
7
|
+
SyncMapTypes
|
|
8
|
+
} from '@logux/actions'
|
|
9
|
+
|
|
10
|
+
import { BaseServer, ServerMeta } from '../base-server/index.js'
|
|
11
|
+
import { Context } from '../context/index.js'
|
|
12
|
+
|
|
13
|
+
declare const WITH_TIME: unique symbol
|
|
14
|
+
|
|
15
|
+
export type WithTime<Value extends SyncMapTypes | SyncMapTypes[]> = {
|
|
16
|
+
value: Value
|
|
17
|
+
time: number
|
|
18
|
+
[WITH_TIME]: true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type WithoutTime<Value extends SyncMapTypes | SyncMapTypes[]> = {
|
|
22
|
+
value: Value
|
|
23
|
+
time: undefined
|
|
24
|
+
[WITH_TIME]: false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SyncMapData<Value extends SyncMapValues> = { id: string } & {
|
|
28
|
+
[Key in keyof Value]: WithTime<Value[Key]> | WithoutTime<Value[Key]>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Add last changed time to value to use in conflict resolution.
|
|
33
|
+
*
|
|
34
|
+
* If you do not know the time, use {@link NoConflictResolution}.
|
|
35
|
+
*
|
|
36
|
+
* @param value The value.
|
|
37
|
+
* @param time UNIX milliseconds.
|
|
38
|
+
* @returns Wrapper.
|
|
39
|
+
*/
|
|
40
|
+
export function ChangedAt<Value extends SyncMapTypes | SyncMapTypes[]>(
|
|
41
|
+
value: Value,
|
|
42
|
+
time: number
|
|
43
|
+
): WithTime<Value>
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Mark that the value has no last changed date and conflict resolution
|
|
47
|
+
* can’t be applied.
|
|
48
|
+
*
|
|
49
|
+
* @param value The value.
|
|
50
|
+
* @returns Wrapper.
|
|
51
|
+
*/
|
|
52
|
+
export function NoConflictResolution<
|
|
53
|
+
Value extends SyncMapTypes | SyncMapTypes[]
|
|
54
|
+
>(
|
|
55
|
+
value: Value
|
|
56
|
+
): WithTime<Value>
|
|
57
|
+
|
|
58
|
+
interface SyncMapActionFilter<Value extends SyncMapValues> {
|
|
59
|
+
(
|
|
60
|
+
ctx: Context,
|
|
61
|
+
action: SyncMapCreateAction<Value> | SyncMapDeleteAction,
|
|
62
|
+
meta: ServerMeta
|
|
63
|
+
): Promise<boolean> | boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface SyncMapOperations<Value extends SyncMapValues> {
|
|
67
|
+
access(
|
|
68
|
+
ctx: Context,
|
|
69
|
+
id: string,
|
|
70
|
+
action:
|
|
71
|
+
| SyncMapCreateAction
|
|
72
|
+
| SyncMapChangeAction
|
|
73
|
+
| SyncMapDeleteAction
|
|
74
|
+
| LoguxSubscribeAction,
|
|
75
|
+
meta: ServerMeta
|
|
76
|
+
): Promise<boolean> | boolean
|
|
77
|
+
|
|
78
|
+
load?(
|
|
79
|
+
ctx: Context,
|
|
80
|
+
id: string,
|
|
81
|
+
since: number | undefined,
|
|
82
|
+
action: LoguxSubscribeAction,
|
|
83
|
+
meta: ServerMeta
|
|
84
|
+
): Promise<SyncMapData<Value>> | SyncMapData<Value> | false | Promise<false>
|
|
85
|
+
|
|
86
|
+
create?(
|
|
87
|
+
ctx: Context,
|
|
88
|
+
id: string,
|
|
89
|
+
fields: Value,
|
|
90
|
+
time: number,
|
|
91
|
+
action: SyncMapCreateAction<Value>,
|
|
92
|
+
meta: ServerMeta
|
|
93
|
+
): Promise<void | boolean> | void | boolean
|
|
94
|
+
|
|
95
|
+
change?(
|
|
96
|
+
ctx: Context,
|
|
97
|
+
id: string,
|
|
98
|
+
fields: Partial<Value>,
|
|
99
|
+
time: number,
|
|
100
|
+
action: SyncMapChangeAction<Value>,
|
|
101
|
+
meta: ServerMeta
|
|
102
|
+
): Promise<void | boolean> | void | boolean
|
|
103
|
+
|
|
104
|
+
delete?(
|
|
105
|
+
ctx: Context,
|
|
106
|
+
id: string,
|
|
107
|
+
action: SyncMapDeleteAction,
|
|
108
|
+
meta: ServerMeta
|
|
109
|
+
): Promise<void | boolean> | void | boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface SyncMapFilterOperations<Value extends SyncMapValues> {
|
|
113
|
+
access?(
|
|
114
|
+
ctx: Context,
|
|
115
|
+
filter: Partial<Value> | undefined,
|
|
116
|
+
action: LoguxSubscribeAction,
|
|
117
|
+
meta: ServerMeta
|
|
118
|
+
): Promise<boolean> | boolean
|
|
119
|
+
|
|
120
|
+
initial(
|
|
121
|
+
ctx: Context,
|
|
122
|
+
filter: Partial<Value> | undefined,
|
|
123
|
+
since: number | undefined,
|
|
124
|
+
action: LoguxSubscribeAction,
|
|
125
|
+
meta: ServerMeta
|
|
126
|
+
): Promise<SyncMapData<Value>[]> | SyncMapData<Value>[]
|
|
127
|
+
|
|
128
|
+
actions?(
|
|
129
|
+
ctx: Context,
|
|
130
|
+
filter: Partial<Value> | undefined,
|
|
131
|
+
action: LoguxSubscribeAction,
|
|
132
|
+
meta: ServerMeta
|
|
133
|
+
): Promise<SyncMapActionFilter<Value>> | SyncMapActionFilter<Value> | void
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Add callbacks for client’s `SyncMap`.
|
|
138
|
+
*
|
|
139
|
+
* ```js
|
|
140
|
+
* import { addSyncMap, isFirstTimeOlder, ChangedAt } from '@logux/server'
|
|
141
|
+
* import { LoguxNotFoundError } from '@logux/actions'
|
|
142
|
+
*
|
|
143
|
+
* addSyncMap(server, 'tasks', {
|
|
144
|
+
* async access (ctx, id) {
|
|
145
|
+
* const task = await Task.find(id)
|
|
146
|
+
* return ctx.userId === task.authorId
|
|
147
|
+
* },
|
|
148
|
+
*
|
|
149
|
+
* async load (ctx, id, since) {
|
|
150
|
+
* const task = await Task.find(id)
|
|
151
|
+
* if (!task) throw new LoguxNotFoundError()
|
|
152
|
+
* return {
|
|
153
|
+
* id: task.id,
|
|
154
|
+
* text: ChangedAt(task.text, task.textChanged),
|
|
155
|
+
* finished: ChangedAt(task.finished, task.finishedChanged),
|
|
156
|
+
* }
|
|
157
|
+
* },
|
|
158
|
+
*
|
|
159
|
+
* async create (ctx, id, fields, time) {
|
|
160
|
+
* await Task.create({
|
|
161
|
+
* id,
|
|
162
|
+
* text: fields.text,
|
|
163
|
+
* finished: fields.finished,
|
|
164
|
+
* authorId: ctx.userId,
|
|
165
|
+
* textChanged: time,
|
|
166
|
+
* finishedChanged: time
|
|
167
|
+
* })
|
|
168
|
+
* },
|
|
169
|
+
*
|
|
170
|
+
* async change (ctx, id, fields, time) {
|
|
171
|
+
* const task = await Task.find(id)
|
|
172
|
+
* if ('text' in fields) {
|
|
173
|
+
* if (task.textChanged < time) {
|
|
174
|
+
* await task.update({
|
|
175
|
+
* text: fields.text,
|
|
176
|
+
* textChanged: time
|
|
177
|
+
* })
|
|
178
|
+
* }
|
|
179
|
+
* }
|
|
180
|
+
* if ('finished' in fields) {
|
|
181
|
+
* if (task.finishedChanged < time) {
|
|
182
|
+
* await task.update({
|
|
183
|
+
* finished: fields.finished,
|
|
184
|
+
* finishedChanged: time
|
|
185
|
+
* })
|
|
186
|
+
* }
|
|
187
|
+
* }
|
|
188
|
+
* }
|
|
189
|
+
*
|
|
190
|
+
* async delete (ctx, id) {
|
|
191
|
+
* await Task.delete(id)
|
|
192
|
+
* }
|
|
193
|
+
* })
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* @param server Server instance.
|
|
197
|
+
* @param plural Prefix for channel names and action types.
|
|
198
|
+
* @param operations Callbacks.
|
|
199
|
+
*/
|
|
200
|
+
export function addSyncMap<Values extends SyncMapValues>(
|
|
201
|
+
server: BaseServer,
|
|
202
|
+
plural: string,
|
|
203
|
+
operations: SyncMapOperations<Values>
|
|
204
|
+
): void
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Add callbacks for client’s `useFilter`.
|
|
208
|
+
*
|
|
209
|
+
* ```js
|
|
210
|
+
* import { addSyncMapFilter, ChangedAt } from '@logux/server'
|
|
211
|
+
*
|
|
212
|
+
* addSyncMapFilter(server, 'tasks', {
|
|
213
|
+
* access (ctx, filter) {
|
|
214
|
+
* return true
|
|
215
|
+
* },
|
|
216
|
+
*
|
|
217
|
+
* initial (ctx, filter, since) {
|
|
218
|
+
* let tasks = await Tasks.where({ ...filter, authorId: ctx.userId })
|
|
219
|
+
* // You can return only data changed after `since`
|
|
220
|
+
* return tasks.map(task => ({
|
|
221
|
+
* id: task.id,
|
|
222
|
+
* text: ChangedAt(task.text, task.textChanged),
|
|
223
|
+
* finished: ChangedAt(task.finished, task.finishedChanged),
|
|
224
|
+
* }))
|
|
225
|
+
* },
|
|
226
|
+
*
|
|
227
|
+
* actions (filterCtx, filter) {
|
|
228
|
+
* return (actionCtx, action, meta) => {
|
|
229
|
+
* return actionCtx.userId === filterCtx.userId
|
|
230
|
+
* }
|
|
231
|
+
* }
|
|
232
|
+
* })
|
|
233
|
+
* ```
|
|
234
|
+
|
|
235
|
+
* @param server Server instance.
|
|
236
|
+
* @param plural Prefix for channel names and action types.
|
|
237
|
+
* @param operations Callbacks.
|
|
238
|
+
*/
|
|
239
|
+
export function addSyncMapFilter<Values extends SyncMapValues>(
|
|
240
|
+
server: BaseServer,
|
|
241
|
+
plural: string,
|
|
242
|
+
operations: SyncMapFilterOperations<Values>
|
|
243
|
+
): void
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const WITH_TIME = Symbol('WITH_TIME')
|
|
2
|
+
|
|
3
|
+
export function ChangedAt(value, time) {
|
|
4
|
+
return { value, time, [WITH_TIME]: true }
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function NoConflictResolution(value) {
|
|
8
|
+
return { value, [WITH_TIME]: false }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function addFinished(server, ctx, type, action, meta) {
|
|
12
|
+
await server.process(
|
|
13
|
+
{ ...action, type },
|
|
14
|
+
{ time: meta.time, excludeClients: [ctx.clientId] }
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resendFinished(server, plural, type, all = true) {
|
|
19
|
+
if (all) {
|
|
20
|
+
server.type(type, {
|
|
21
|
+
access() {
|
|
22
|
+
return false
|
|
23
|
+
},
|
|
24
|
+
resend(ctx, action) {
|
|
25
|
+
return [plural, `${plural}/${action.id}`]
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
} else {
|
|
29
|
+
server.type(type, {
|
|
30
|
+
access() {
|
|
31
|
+
return false
|
|
32
|
+
},
|
|
33
|
+
resend(ctx, action) {
|
|
34
|
+
return [`${plural}/${action.id}`]
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildFilter(filter) {
|
|
41
|
+
return (ctx, action) => {
|
|
42
|
+
if (action.type.endsWith('/created')) {
|
|
43
|
+
for (let key in filter) {
|
|
44
|
+
if (action.fields[key] !== filter[key]) return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function sendMap(server, changedType, data, since) {
|
|
52
|
+
let { id, ...other } = data
|
|
53
|
+
let byTime = new Map()
|
|
54
|
+
for (let key in other) {
|
|
55
|
+
if (other[key][WITH_TIME] === true) {
|
|
56
|
+
let time = other[key].time
|
|
57
|
+
if (!byTime.has(time)) byTime.set(time, {})
|
|
58
|
+
byTime.get(time)[key] = other[key].value
|
|
59
|
+
} else if (other[key][WITH_TIME] === false) {
|
|
60
|
+
if (!byTime.has('now')) byTime.set('now', {})
|
|
61
|
+
byTime.get('now')[key] = other[key].value
|
|
62
|
+
} else {
|
|
63
|
+
throw new Error('Wrap value into ChangedAt() or NoConflictResolution()')
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (let [time, fields] of byTime.entries()) {
|
|
67
|
+
let changedMeta
|
|
68
|
+
if (time !== 'now') {
|
|
69
|
+
changedMeta = { time }
|
|
70
|
+
if (time < since) continue
|
|
71
|
+
}
|
|
72
|
+
await server.process(
|
|
73
|
+
{
|
|
74
|
+
type: changedType,
|
|
75
|
+
id,
|
|
76
|
+
fields
|
|
77
|
+
},
|
|
78
|
+
changedMeta
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function addSyncMap(server, plural, operations) {
|
|
84
|
+
let createdType = `${plural}/created`
|
|
85
|
+
let changedType = `${plural}/changed`
|
|
86
|
+
let deletedType = `${plural}/deleted`
|
|
87
|
+
resendFinished(server, plural, createdType)
|
|
88
|
+
resendFinished(server, plural, changedType)
|
|
89
|
+
resendFinished(server, plural, deletedType, false)
|
|
90
|
+
|
|
91
|
+
if (operations.load) {
|
|
92
|
+
server.channel(`${plural}/:id`, {
|
|
93
|
+
access(ctx, action, meta) {
|
|
94
|
+
return operations.access(ctx, ctx.params.id, action, meta)
|
|
95
|
+
},
|
|
96
|
+
async load(ctx, action, meta) {
|
|
97
|
+
if (action.creating) return
|
|
98
|
+
let data = await operations.load(
|
|
99
|
+
ctx,
|
|
100
|
+
ctx.params.id,
|
|
101
|
+
action.since,
|
|
102
|
+
action,
|
|
103
|
+
meta
|
|
104
|
+
)
|
|
105
|
+
if (data !== false) {
|
|
106
|
+
await sendMap(
|
|
107
|
+
server,
|
|
108
|
+
changedType,
|
|
109
|
+
data,
|
|
110
|
+
action.since ? action.since.time : 0
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (operations.create) {
|
|
118
|
+
server.type(`${plural}/create`, {
|
|
119
|
+
access(ctx, action, meta) {
|
|
120
|
+
return operations.access(ctx, action.id, action, meta)
|
|
121
|
+
},
|
|
122
|
+
async process(ctx, action, meta) {
|
|
123
|
+
let result = await operations.create(
|
|
124
|
+
ctx,
|
|
125
|
+
action.id,
|
|
126
|
+
action.fields,
|
|
127
|
+
meta.time,
|
|
128
|
+
action,
|
|
129
|
+
meta
|
|
130
|
+
)
|
|
131
|
+
if (result !== false) {
|
|
132
|
+
await addFinished(server, ctx, createdType, action, meta)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (operations.change) {
|
|
139
|
+
server.type(`${plural}/change`, {
|
|
140
|
+
access(ctx, action, meta) {
|
|
141
|
+
return operations.access(ctx, action.id, action, meta)
|
|
142
|
+
},
|
|
143
|
+
async process(ctx, action, meta) {
|
|
144
|
+
let result = await operations.change(
|
|
145
|
+
ctx,
|
|
146
|
+
action.id,
|
|
147
|
+
action.fields,
|
|
148
|
+
meta.time,
|
|
149
|
+
action,
|
|
150
|
+
meta
|
|
151
|
+
)
|
|
152
|
+
if (result !== false) {
|
|
153
|
+
await addFinished(server, ctx, changedType, action, meta)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (operations.delete) {
|
|
160
|
+
server.type(`${plural}/delete`, {
|
|
161
|
+
access(ctx, action, meta) {
|
|
162
|
+
return operations.access(ctx, action.id, action, meta)
|
|
163
|
+
},
|
|
164
|
+
async process(ctx, action, meta) {
|
|
165
|
+
let result = await operations.delete(ctx, action.id, action, meta)
|
|
166
|
+
if (result !== false) {
|
|
167
|
+
await addFinished(server, ctx, deletedType, action, meta)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function addSyncMapFilter(server, plural, operations) {
|
|
175
|
+
let changedType = `${plural}/changed`
|
|
176
|
+
|
|
177
|
+
server.channel(plural, {
|
|
178
|
+
access(ctx, action, meta) {
|
|
179
|
+
return operations.access(ctx, action.filter, action, meta)
|
|
180
|
+
},
|
|
181
|
+
filter(ctx, action, meta) {
|
|
182
|
+
let filter = action.filter ? buildFilter(action.filter) : () => true
|
|
183
|
+
let custom = operations.actions
|
|
184
|
+
? operations.actions(ctx, action.filter, action, meta)
|
|
185
|
+
: () => true
|
|
186
|
+
return (ctx2, action2, meta2) => {
|
|
187
|
+
return filter(ctx2, action2, meta2) && custom(ctx2, action2, meta2)
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
async load(ctx, action, meta) {
|
|
191
|
+
let data = await operations.initial(
|
|
192
|
+
ctx,
|
|
193
|
+
action.filter,
|
|
194
|
+
action.since,
|
|
195
|
+
action,
|
|
196
|
+
meta
|
|
197
|
+
)
|
|
198
|
+
let since = action.since ? action.since.time : 0
|
|
199
|
+
await Promise.all(
|
|
200
|
+
data.map(async i => {
|
|
201
|
+
await server.subscribe(ctx.nodeId, `${plural}/${i.id}`)
|
|
202
|
+
await sendMap(server, changedType, i, since)
|
|
203
|
+
})
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
}
|
package/base-server/index.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ export interface ServerMeta extends Meta {
|
|
|
72
72
|
node?: string
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Client IDs, which will not
|
|
75
|
+
* Client IDs, which will not receive the action.
|
|
76
76
|
*/
|
|
77
77
|
excludeClients?: string[]
|
|
78
78
|
}
|
|
@@ -206,6 +206,14 @@ export interface BaseServerOptions {
|
|
|
206
206
|
* By default it cleans `Bearer [^\s"]+`.
|
|
207
207
|
*/
|
|
208
208
|
cleanFromLog?: RegExp
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Disable health check endpoint, control HTTP API, {@link Server#http}.
|
|
212
|
+
*
|
|
213
|
+
* The server will process only WebSocket connection and ignore all other
|
|
214
|
+
* HTTP request (so they can be processed by other HTTP server).
|
|
215
|
+
*/
|
|
216
|
+
disableHttpServer?: boolean
|
|
209
217
|
}
|
|
210
218
|
|
|
211
219
|
export interface AuthenticatorOptions<Headers extends object> {
|
|
@@ -750,8 +758,8 @@ export class BaseServer<
|
|
|
750
758
|
* and node ID. It should return a Promise with `true` or `false`.
|
|
751
759
|
*
|
|
752
760
|
* ```js
|
|
753
|
-
* server.auth(async ({ userId,
|
|
754
|
-
* const user = await findUserByToken(token)
|
|
761
|
+
* server.auth(async ({ userId, cookie }) => {
|
|
762
|
+
* const user = await findUserByToken(cookie.token)
|
|
755
763
|
* return !!user && userId === user.id
|
|
756
764
|
* })
|
|
757
765
|
* ```
|
package/base-server/index.js
CHANGED
|
@@ -153,6 +153,8 @@ export class BaseServer {
|
|
|
153
153
|
this.contexts = new WeakMap()
|
|
154
154
|
this.log = log
|
|
155
155
|
|
|
156
|
+
let cleaned = {}
|
|
157
|
+
|
|
156
158
|
this.on('preadd', (action, meta) => {
|
|
157
159
|
let isLogux = action.type.slice(0, 6) === 'logux/'
|
|
158
160
|
if (!meta.server) {
|
|
@@ -178,7 +180,12 @@ export class BaseServer {
|
|
|
178
180
|
})
|
|
179
181
|
this.on('add', async (action, meta) => {
|
|
180
182
|
let start = Date.now()
|
|
181
|
-
|
|
183
|
+
if (meta.reasons.length === 0) {
|
|
184
|
+
cleaned[meta.id] = true
|
|
185
|
+
this.emitter.emit('report', 'addClean', { action, meta })
|
|
186
|
+
} else {
|
|
187
|
+
this.emitter.emit('report', 'add', { action, meta })
|
|
188
|
+
}
|
|
182
189
|
|
|
183
190
|
if (this.destroying) return
|
|
184
191
|
|
|
@@ -254,6 +261,10 @@ export class BaseServer {
|
|
|
254
261
|
}
|
|
255
262
|
})
|
|
256
263
|
this.on('clean', (action, meta) => {
|
|
264
|
+
if (cleaned[meta.id]) {
|
|
265
|
+
delete cleaned[meta.id]
|
|
266
|
+
return
|
|
267
|
+
}
|
|
257
268
|
this.emitter.emit('report', 'clean', { actionId: meta.id })
|
|
258
269
|
})
|
|
259
270
|
|
|
@@ -371,6 +382,11 @@ export class BaseServer {
|
|
|
371
382
|
}
|
|
372
383
|
|
|
373
384
|
http(listener) {
|
|
385
|
+
if (this.options.disableHttpServer) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
'`server.http()` can not be called when `disableHttpServer` enabled'
|
|
388
|
+
)
|
|
389
|
+
}
|
|
374
390
|
this.httpListener = listener
|
|
375
391
|
}
|
|
376
392
|
|
|
@@ -658,7 +674,6 @@ export class BaseServer {
|
|
|
658
674
|
try {
|
|
659
675
|
await processor.process(ctx, action, meta)
|
|
660
676
|
latency = Date.now() - start
|
|
661
|
-
this.emitter.emit('report', 'processed', { actionId: meta.id, latency })
|
|
662
677
|
this.markAsProcessed(meta)
|
|
663
678
|
} catch (e) {
|
|
664
679
|
this.log.changeMeta(meta.id, { status: 'error' })
|
|
@@ -37,7 +37,7 @@ function send(backend, command, events) {
|
|
|
37
37
|
},
|
|
38
38
|
res => {
|
|
39
39
|
if (res.statusCode < 200 || res.statusCode > 299) {
|
|
40
|
-
events.error(new Error(`Backend
|
|
40
|
+
events.error(new Error(`Backend responded with ${res.statusCode} code`))
|
|
41
41
|
} else {
|
|
42
42
|
let data = false
|
|
43
43
|
res
|
|
@@ -17,6 +17,17 @@ function isValidBody(body) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function bindControlServer(app, custom) {
|
|
20
|
+
if (app.options.disableHttpServer) {
|
|
21
|
+
if (app.options.controlSecret) {
|
|
22
|
+
let err = new Error(
|
|
23
|
+
'`controlSecret` can be set together with `disableHttpServer` option'
|
|
24
|
+
)
|
|
25
|
+
err.logux = true
|
|
26
|
+
throw err
|
|
27
|
+
}
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
let masks = app.options.controlMask.split(/,\s*/).map(i => ip.cidrSubnet(i))
|
|
21
32
|
app.httpServer.on('request', async (req, res) => {
|
|
22
33
|
let urlString = req.url
|
package/create-reporter/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import pino from 'pino'
|
|
2
2
|
import os from 'os'
|
|
3
|
+
import { dirname, join, sep } from 'path'
|
|
4
|
+
import pico from 'picocolors'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
export const PATH_TO_PRETTIFYING_PINO_TRANSPORT = join(
|
|
9
|
+
__dirname,
|
|
10
|
+
'../human-formatter/index.js'
|
|
11
|
+
)
|
|
5
12
|
|
|
6
13
|
const ERROR_CODES = {
|
|
7
14
|
EADDRINUSE: e => {
|
|
@@ -105,9 +112,9 @@ const REPORTERS = {
|
|
|
105
112
|
|
|
106
113
|
add: () => ({ msg: 'Action was added' }),
|
|
107
114
|
|
|
108
|
-
|
|
115
|
+
addClean: () => ({ msg: 'Action was added and cleaned' }),
|
|
109
116
|
|
|
110
|
-
|
|
117
|
+
clean: () => ({ msg: 'Action was cleaned' }),
|
|
111
118
|
|
|
112
119
|
subscribed: () => ({ msg: 'Client was subscribed' }),
|
|
113
120
|
|
|
@@ -198,27 +205,36 @@ const REPORTERS = {
|
|
|
198
205
|
}
|
|
199
206
|
|
|
200
207
|
function createLogger(options) {
|
|
201
|
-
let stream = options.logger.stream || pino.destination()
|
|
202
|
-
let prettifier = {}
|
|
203
208
|
if (options.logger === 'human' || options.logger.type === 'human') {
|
|
204
209
|
let env = options.env || process.env.NODE_ENV || 'development'
|
|
205
|
-
let color =
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
let color =
|
|
211
|
+
env !== 'development' ? false : pico.createColors().isColorSupported
|
|
212
|
+
let basepath = options.root || process.cwd()
|
|
213
|
+
if (basepath.slice(-1) !== sep) basepath += sep
|
|
214
|
+
|
|
215
|
+
let logger = pino(
|
|
216
|
+
pino.transport({
|
|
217
|
+
target: PATH_TO_PRETTIFYING_PINO_TRANSPORT,
|
|
218
|
+
options: {
|
|
219
|
+
basepath,
|
|
220
|
+
color,
|
|
221
|
+
destination: options.logger.destination
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
// NOTE: needed only for tests
|
|
227
|
+
logger._basepath = basepath
|
|
228
|
+
logger._color = color
|
|
229
|
+
|
|
230
|
+
return logger
|
|
214
231
|
}
|
|
215
232
|
return pino(
|
|
216
233
|
{
|
|
217
234
|
name: 'logux-server',
|
|
218
|
-
timestamp: pino.stdTimeFunctions.isoTime
|
|
219
|
-
...prettifier
|
|
235
|
+
timestamp: pino.stdTimeFunctions.isoTime
|
|
220
236
|
},
|
|
221
|
-
stream
|
|
237
|
+
options.logger.stream || pino.destination()
|
|
222
238
|
)
|
|
223
239
|
}
|
|
224
240
|
|
package/human-formatter/index.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import stripAnsi from 'strip-ansi'
|
|
2
2
|
import yyyymmdd from 'yyyy-mm-dd'
|
|
3
3
|
import pico from 'picocolors'
|
|
4
|
-
import path from 'path'
|
|
5
4
|
import os from 'os'
|
|
5
|
+
import abstractTransport from 'pino-abstract-transport'
|
|
6
|
+
import { Transform } from 'stream'
|
|
7
|
+
import pino from 'pino'
|
|
8
|
+
import { once } from 'events'
|
|
9
|
+
|
|
10
|
+
import { mulberry32, onceXmur3 } from './utils.js'
|
|
6
11
|
|
|
7
12
|
const INDENT = ' '
|
|
8
13
|
const PADDING = ' '
|
|
9
14
|
const SEPARATOR = os.EOL + os.EOL
|
|
10
15
|
const NEXT_LINE = os.EOL === '\n' ? '\r\v' : os.EOL
|
|
11
16
|
|
|
12
|
-
const LATENCY_UNIT = ' ms'
|
|
13
|
-
|
|
14
17
|
const PARAMS_BLACKLIST = {
|
|
15
18
|
v: true,
|
|
16
19
|
msg: true,
|
|
@@ -34,6 +37,8 @@ const LABELS = {
|
|
|
34
37
|
60: (c, str) => label(c, ' FATAL ', 'red', 'bgRed', 'white', str)
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
const COLORS = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan']
|
|
41
|
+
|
|
37
42
|
function rightPag(str, length) {
|
|
38
43
|
let add = length - stripAnsi(str).length
|
|
39
44
|
for (let i = 0; i < add; i++) str += ' '
|
|
@@ -56,15 +61,54 @@ function formatName(key) {
|
|
|
56
61
|
.replace(/^\w/, char => char.toUpperCase())
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
function shuffledColors(str) {
|
|
65
|
+
let index = -1
|
|
66
|
+
let result = Array.from(COLORS)
|
|
67
|
+
let lastIndex = result.length - 1
|
|
68
|
+
let seed = onceXmur3(str)
|
|
69
|
+
let randomFn = mulberry32(seed)
|
|
70
|
+
|
|
71
|
+
while (++index < COLORS.length) {
|
|
72
|
+
let randIndex = index + Math.floor(randomFn() * (lastIndex - index + 1))
|
|
73
|
+
let value = result[randIndex]
|
|
74
|
+
|
|
75
|
+
result[randIndex] = result[index]
|
|
76
|
+
result[index] = value
|
|
77
|
+
}
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function splitAndColorize(c, partLength, str) {
|
|
82
|
+
let strBuilder = []
|
|
83
|
+
let colors = shuffledColors(str)
|
|
84
|
+
|
|
85
|
+
for (
|
|
86
|
+
let start = 0, end = partLength, n = 0, color = colors[n];
|
|
87
|
+
start < str.length;
|
|
88
|
+
start += partLength,
|
|
89
|
+
end += partLength,
|
|
90
|
+
n = n + 1,
|
|
91
|
+
color = colors[n % colors.length]
|
|
92
|
+
) {
|
|
93
|
+
let strToColorize = str.slice(start, end)
|
|
94
|
+
if (strToColorize.length === 1) {
|
|
95
|
+
color = colors[Math.abs(n - 1) % colors.length]
|
|
96
|
+
}
|
|
97
|
+
strBuilder.push(c[color](strToColorize))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return strBuilder.join('')
|
|
101
|
+
}
|
|
102
|
+
|
|
59
103
|
function formatNodeId(c, nodeId) {
|
|
60
104
|
let pos = nodeId.lastIndexOf(':')
|
|
61
|
-
let id, random
|
|
62
105
|
if (pos === -1) {
|
|
63
106
|
return nodeId
|
|
64
107
|
} else {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
108
|
+
let s = nodeId.split(':')
|
|
109
|
+
let id = c.bold(s[0])
|
|
110
|
+
let random = splitAndColorize(c, 3, s[1])
|
|
111
|
+
return `${id}:${random}`
|
|
68
112
|
}
|
|
69
113
|
}
|
|
70
114
|
|
|
@@ -92,7 +136,13 @@ function formatArray(c, array) {
|
|
|
92
136
|
|
|
93
137
|
function formatActionId(c, id) {
|
|
94
138
|
let p = id.split(' ')
|
|
95
|
-
|
|
139
|
+
if (p.length === 1) {
|
|
140
|
+
return p
|
|
141
|
+
}
|
|
142
|
+
return `${c.bold(splitAndColorize(c, 4, p[0]))} ${formatNodeId(
|
|
143
|
+
c,
|
|
144
|
+
p[1]
|
|
145
|
+
)} ${c.bold(p[2])}`
|
|
96
146
|
}
|
|
97
147
|
|
|
98
148
|
function formatParams(c, params, parent) {
|
|
@@ -108,8 +158,13 @@ function formatParams(c, params, parent) {
|
|
|
108
158
|
|
|
109
159
|
let start = PADDING + rightPag(`${name}: `, maxName + 2)
|
|
110
160
|
|
|
111
|
-
if (name === 'Node ID') {
|
|
161
|
+
if (name === 'Node ID' || (parent === 'Meta' && name === 'server')) {
|
|
112
162
|
return start + formatNodeId(c, value)
|
|
163
|
+
} else if (
|
|
164
|
+
parent === 'Meta' &&
|
|
165
|
+
(name === 'clients' || name === 'excludeClients')
|
|
166
|
+
) {
|
|
167
|
+
return `${start}[${value.map(v => `"${formatNodeId(c, v)}"`).join()}]`
|
|
113
168
|
} else if (name === 'Action ID' || (parent === 'Meta' && name === 'id')) {
|
|
114
169
|
return start + formatActionId(c, value)
|
|
115
170
|
} else if (Array.isArray(value)) {
|
|
@@ -124,8 +179,6 @@ function formatParams(c, params, parent) {
|
|
|
124
179
|
.split(NEXT_LINE)
|
|
125
180
|
.join(NEXT_LINE + INDENT)
|
|
126
181
|
)
|
|
127
|
-
} else if (name === 'Latency' && !parent) {
|
|
128
|
-
return start + c.bold(value) + LATENCY_UNIT
|
|
129
182
|
} else if (typeof value === 'string' && parent) {
|
|
130
183
|
return start + '"' + c.bold(value) + '"'
|
|
131
184
|
} else {
|
|
@@ -169,14 +222,11 @@ function prettyStackTrace(c, stack, basepath) {
|
|
|
169
222
|
.join(NEXT_LINE)
|
|
170
223
|
}
|
|
171
224
|
|
|
172
|
-
|
|
225
|
+
function humanFormatter(options) {
|
|
173
226
|
let c = pico.createColors(options.color)
|
|
174
|
-
|
|
175
|
-
let basepath = options.basepath || process.cwd()
|
|
176
|
-
if (basepath.slice(-1) !== path.sep) basepath += path.sep
|
|
177
|
-
this.basepath = basepath
|
|
227
|
+
let basepath = options.basepath
|
|
178
228
|
|
|
179
|
-
return function (record) {
|
|
229
|
+
return function format(record) {
|
|
180
230
|
let message = [LABELS[record.level](c, record.msg)]
|
|
181
231
|
let params = Object.keys(record)
|
|
182
232
|
.filter(key => !PARAMS_BLACKLIST[key])
|
|
@@ -213,3 +263,32 @@ export function humanFormatter(options) {
|
|
|
213
263
|
return message.filter(i => i !== '').join(NEXT_LINE) + SEPARATOR
|
|
214
264
|
}
|
|
215
265
|
}
|
|
266
|
+
|
|
267
|
+
export default async function (options) {
|
|
268
|
+
let format = humanFormatter(options)
|
|
269
|
+
let destination = pino.destination({
|
|
270
|
+
dest: options.destination || 1,
|
|
271
|
+
sync: options.sync || false
|
|
272
|
+
})
|
|
273
|
+
await once(destination, 'ready')
|
|
274
|
+
let transform = new Transform({
|
|
275
|
+
autoDestroy: true,
|
|
276
|
+
objectMode: true,
|
|
277
|
+
transform(chunk, encoding, callback) {
|
|
278
|
+
callback(null, format(chunk))
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
return abstractTransport(
|
|
283
|
+
source => {
|
|
284
|
+
source.pipe(transform)
|
|
285
|
+
transform.pipe(destination)
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
close(err, cb) {
|
|
289
|
+
transform.end()
|
|
290
|
+
transform.on('close', cb.bind(null, err))
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function onceXmur3(str) {
|
|
2
|
+
let h = 1779033703 ^ str.length
|
|
3
|
+
for (let i = 0; i < str.length; i++) {
|
|
4
|
+
h = Math.imul(h ^ str.charCodeAt(i), 3432918353)
|
|
5
|
+
h = (h << 13) | (h >>> 19)
|
|
6
|
+
}
|
|
7
|
+
h = Math.imul(h ^ (h >>> 16), 2246822507)
|
|
8
|
+
h = Math.imul(h ^ (h >>> 13), 3266489909)
|
|
9
|
+
return (h ^ (h >>> 16)) >>> 0
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function mulberry32(a) {
|
|
13
|
+
return function () {
|
|
14
|
+
a |= 0
|
|
15
|
+
a = (a + 0x6d2b79f5) | 0
|
|
16
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
|
17
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
18
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
19
|
+
}
|
|
20
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -17,6 +17,15 @@ export {
|
|
|
17
17
|
put,
|
|
18
18
|
del
|
|
19
19
|
} from './request/index.js'
|
|
20
|
+
export {
|
|
21
|
+
NoConflictResolution,
|
|
22
|
+
addSyncMapFilter,
|
|
23
|
+
WithoutTime,
|
|
24
|
+
SyncMapData,
|
|
25
|
+
addSyncMap,
|
|
26
|
+
ChangedAt,
|
|
27
|
+
WithTime
|
|
28
|
+
} from './add-sync-map/index.js'
|
|
20
29
|
export { TestServer, TestServerOptions } from './test-server/index.js'
|
|
21
30
|
export { TestClient, LoguxActionError } from './test-client/index.js'
|
|
22
31
|
export { Context, ChannelContext } from './context/index.js'
|
package/index.js
CHANGED
|
@@ -7,6 +7,12 @@ export {
|
|
|
7
7
|
put,
|
|
8
8
|
del
|
|
9
9
|
} from './request/index.js'
|
|
10
|
+
export {
|
|
11
|
+
NoConflictResolution,
|
|
12
|
+
addSyncMapFilter,
|
|
13
|
+
addSyncMap,
|
|
14
|
+
ChangedAt
|
|
15
|
+
} from './add-sync-map/index.js'
|
|
10
16
|
export { BaseServer, wasNot403 } from './base-server/index.js'
|
|
11
17
|
export { ALLOWED_META } from './allowed-meta/index.js'
|
|
12
18
|
export { TestServer } from './test-server/index.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logux/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Build own Logux server or make proxy between WebSocket and HTTP backend on any language",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"logux",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"homepage": "https://logux.io/",
|
|
19
19
|
"repository": "logux/server",
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": "^
|
|
21
|
+
"node": "^14.0.0 || ^16.0.0 || >=18.0.0"
|
|
22
22
|
},
|
|
23
23
|
"type": "module",
|
|
24
24
|
"types": "./index.d.ts",
|
|
@@ -27,23 +27,24 @@
|
|
|
27
27
|
"./package.json": "./package.json"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@logux/actions": "^0.
|
|
31
|
-
"@logux/core": "^0.
|
|
30
|
+
"@logux/actions": "^0.3.0",
|
|
31
|
+
"@logux/core": "^0.8.0",
|
|
32
32
|
"JSONStream": "^1.3.5",
|
|
33
|
-
"cookie": "^0.
|
|
34
|
-
"dotenv": "^
|
|
35
|
-
"fast-glob": "^3.2.
|
|
36
|
-
"ip": "^1.1.
|
|
33
|
+
"cookie": "^0.5.0",
|
|
34
|
+
"dotenv": "^16.0.2",
|
|
35
|
+
"fast-glob": "^3.2.11",
|
|
36
|
+
"ip": "^1.1.8",
|
|
37
37
|
"nanodelay": "^2.0.2",
|
|
38
|
-
"nanoevents": "^
|
|
39
|
-
"nanoid": "^
|
|
40
|
-
"node-fetch": "^2.6.
|
|
38
|
+
"nanoevents": "^7.0.1",
|
|
39
|
+
"nanoid": "^4.0.0",
|
|
40
|
+
"node-fetch": "^2.6.7",
|
|
41
41
|
"picocolors": "^1.0.0",
|
|
42
|
-
"pino": "
|
|
43
|
-
"
|
|
42
|
+
"pino": "8.4.2",
|
|
43
|
+
"pino-abstract-transport": "^1.0.0",
|
|
44
|
+
"semver": "^7.3.7",
|
|
44
45
|
"strip-ansi": "^7.0.1",
|
|
45
46
|
"url-pattern": "^1.0.3",
|
|
46
|
-
"ws": "^8.
|
|
47
|
+
"ws": "^8.8.1",
|
|
47
48
|
"yyyy-mm-dd": "^1.0.2"
|
|
48
49
|
}
|
|
49
50
|
}
|
package/test-client/index.d.ts
CHANGED
|
@@ -151,7 +151,7 @@ export class TestClient {
|
|
|
151
151
|
* @param test Function, where do you expect action will be received
|
|
152
152
|
* @returns Promise with all received actions
|
|
153
153
|
*/
|
|
154
|
-
collect(test: () => Promise<
|
|
154
|
+
collect(test: () => Promise<any>): Promise<Action[]>
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
157
|
* Send action to the sever and collect all response actions.
|
|
@@ -180,11 +180,13 @@ export class TestClient {
|
|
|
180
180
|
*
|
|
181
181
|
* @param channel Channel name or `logux/subscribe` action.
|
|
182
182
|
* @param filter Optional filter for subscription.
|
|
183
|
+
* @param since Optional time from last data.
|
|
183
184
|
* @returns Promise with all actions from the server.
|
|
184
185
|
*/
|
|
185
186
|
subscribe(
|
|
186
187
|
channel: string | LoguxSubscribeAction,
|
|
187
|
-
filter?: object
|
|
188
|
+
filter?: object,
|
|
189
|
+
since?: { id: string; time: number }
|
|
188
190
|
): Promise<Action[]>
|
|
189
191
|
|
|
190
192
|
/**
|
|
@@ -216,5 +218,5 @@ export class TestClient {
|
|
|
216
218
|
* @param test Function, where do you expect action will be received
|
|
217
219
|
* @returns Promise with all received actions
|
|
218
220
|
*/
|
|
219
|
-
received(test: () => Promise<
|
|
221
|
+
received(test: () => Promise<any> | any): Promise<Action[]>
|
|
220
222
|
}
|
package/test-client/index.js
CHANGED
|
@@ -133,7 +133,7 @@ export class TestClient {
|
|
|
133
133
|
})
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
async subscribe(channel, filter) {
|
|
136
|
+
async subscribe(channel, filter, since) {
|
|
137
137
|
let action = channel
|
|
138
138
|
if (typeof channel === 'string') {
|
|
139
139
|
action = { type: 'logux/subscribe', channel }
|
|
@@ -141,6 +141,9 @@ export class TestClient {
|
|
|
141
141
|
if (filter) {
|
|
142
142
|
action.filter = filter
|
|
143
143
|
}
|
|
144
|
+
if (since) {
|
|
145
|
+
action.since = since
|
|
146
|
+
}
|
|
144
147
|
let actions = await this.process(action)
|
|
145
148
|
return actions.filter(i => i.type !== 'logux/processed')
|
|
146
149
|
}
|
package/test-server/index.d.ts
CHANGED
|
@@ -126,4 +126,16 @@ export class TestServer<
|
|
|
126
126
|
* @param test Callback with subscripting or action sending.
|
|
127
127
|
*/
|
|
128
128
|
expectUndo(reason: string, test: () => any | Promise<any>): Promise<void>
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Call callback and throw an error if there was no error during
|
|
132
|
+
* server processing.
|
|
133
|
+
*
|
|
134
|
+
* @param text RegExp or string of error message.
|
|
135
|
+
* @param test Callback with subscripting or action sending.
|
|
136
|
+
*/
|
|
137
|
+
expectError(
|
|
138
|
+
text: string | RegExp,
|
|
139
|
+
test: () => any | Promise<any>
|
|
140
|
+
): Promise<void>
|
|
129
141
|
}
|
package/test-server/index.js
CHANGED
|
@@ -30,6 +30,10 @@ export class TestServer extends BaseServer {
|
|
|
30
30
|
}
|
|
31
31
|
if (opts.auth !== false) this.auth(() => true)
|
|
32
32
|
this.testUsers = {}
|
|
33
|
+
|
|
34
|
+
this.on('fatal', () => {
|
|
35
|
+
this.destroy()
|
|
36
|
+
})
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
async connect(userId, opts = {}) {
|
|
@@ -70,4 +74,18 @@ export class TestServer extends BaseServer {
|
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
}
|
|
77
|
+
|
|
78
|
+
async expectError(text, test) {
|
|
79
|
+
try {
|
|
80
|
+
await test()
|
|
81
|
+
throw new Error('Actions passed without error')
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (
|
|
84
|
+
(typeof text === 'string' && e.message !== text) ||
|
|
85
|
+
(text instanceof RegExp && !text.test(e.message))
|
|
86
|
+
) {
|
|
87
|
+
throw e
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
73
91
|
}
|