@whitewall/blip-sdk 0.0.138 → 0.0.139
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/dist/cjs/namespaces/account.js +43 -0
- package/dist/cjs/namespaces/account.js.map +1 -1
- package/dist/cjs/namespaces/namespace.js +78 -38
- package/dist/cjs/namespaces/namespace.js.map +1 -1
- package/dist/cjs/namespaces/portal.js +4 -1
- package/dist/cjs/namespaces/portal.js.map +1 -1
- package/dist/cjs/sender/enveloperesolver.js +3 -3
- package/dist/cjs/sender/enveloperesolver.js.map +1 -1
- package/dist/cjs/sender/throttler.js +51 -21
- package/dist/cjs/sender/throttler.js.map +1 -1
- package/dist/esm/namespaces/account.js +43 -0
- package/dist/esm/namespaces/account.js.map +1 -1
- package/dist/esm/namespaces/namespace.js +78 -38
- package/dist/esm/namespaces/namespace.js.map +1 -1
- package/dist/esm/namespaces/portal.js +4 -1
- package/dist/esm/namespaces/portal.js.map +1 -1
- package/dist/esm/sender/enveloperesolver.js +3 -3
- package/dist/esm/sender/enveloperesolver.js.map +1 -1
- package/dist/esm/sender/throttler.js +51 -21
- package/dist/esm/sender/throttler.js.map +1 -1
- package/dist/types/namespaces/account.d.ts +2 -1
- package/dist/types/namespaces/account.d.ts.map +1 -1
- package/dist/types/namespaces/namespace.d.ts +25 -5
- package/dist/types/namespaces/namespace.d.ts.map +1 -1
- package/dist/types/namespaces/portal.d.ts.map +1 -1
- package/dist/types/sender/throttler.d.ts +7 -2
- package/dist/types/sender/throttler.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/namespaces/account.ts +55 -1
- package/src/namespaces/namespace.ts +137 -62
- package/src/namespaces/portal.ts +4 -1
- package/src/sender/enveloperesolver.ts +3 -3
- package/src/sender/throttler.ts +59 -20
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
ThreadItem,
|
|
18
18
|
} from '../types/index.ts'
|
|
19
19
|
import type { Notification } from '../types/notification.ts'
|
|
20
|
-
import type
|
|
20
|
+
import { type ODataFilter, filter } from '../utils/odata.ts'
|
|
21
21
|
import { uri } from '../utils/uri.ts'
|
|
22
22
|
import { type ConsumeOptions, Namespace, type SendCommandOptions } from './namespace.ts'
|
|
23
23
|
|
|
@@ -217,6 +217,59 @@ export class AccountNamespace extends Namespace {
|
|
|
217
217
|
)
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
public async *streamContactsInTimeframe(
|
|
221
|
+
startDate: Date | string,
|
|
222
|
+
endDate: Date | string,
|
|
223
|
+
opts: Omit<ConsumeOptions, 'skip' | 'max'> = {},
|
|
224
|
+
): AsyncIterable<Contact> {
|
|
225
|
+
const take = opts.take ?? 1000
|
|
226
|
+
const start = new Date(startDate)
|
|
227
|
+
const end = new Date(endDate)
|
|
228
|
+
if (start > end) return
|
|
229
|
+
|
|
230
|
+
const seenIdentities = new Set<string>()
|
|
231
|
+
let cursorTime = end.getTime()
|
|
232
|
+
const startTime = start.getTime()
|
|
233
|
+
|
|
234
|
+
while (cursorTime >= startTime) {
|
|
235
|
+
const items = await this.sendCommand<'get', Array<Contact>>(
|
|
236
|
+
{
|
|
237
|
+
method: 'get',
|
|
238
|
+
uri: uri`/contacts?${{
|
|
239
|
+
$filter: filter<Contact>()
|
|
240
|
+
.ge('lastMessageDate', start)
|
|
241
|
+
.le('lastMessageDate', new Date(cursorTime)),
|
|
242
|
+
}}`,
|
|
243
|
+
},
|
|
244
|
+
{ collection: true, ...opts, take, fetchall: false, optimistic: false },
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
let oldestTime = Number.POSITIVE_INFINITY
|
|
248
|
+
|
|
249
|
+
for (const contact of items) {
|
|
250
|
+
if (contact.lastMessageDate) {
|
|
251
|
+
if (!seenIdentities.has(contact.identity)) {
|
|
252
|
+
seenIdentities.add(contact.identity)
|
|
253
|
+
yield contact
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const lastMessageTime = new Date(contact.lastMessageDate).getTime()
|
|
257
|
+
if (lastMessageTime < oldestTime) {
|
|
258
|
+
oldestTime = lastMessageTime
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (items.length < take || !opts.fetchall) break
|
|
264
|
+
|
|
265
|
+
if (oldestTime < Number.POSITIVE_INFINITY) {
|
|
266
|
+
cursorTime = oldestTime - 1
|
|
267
|
+
} else {
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
220
273
|
public async getContactComments(contact: Identity, opts?: ConsumeOptions): Promise<Array<Comment>> {
|
|
221
274
|
try {
|
|
222
275
|
return await this.sendCommand(
|
|
@@ -401,6 +454,7 @@ export class AccountNamespace extends Namespace {
|
|
|
401
454
|
)
|
|
402
455
|
}
|
|
403
456
|
|
|
457
|
+
// THis doesn't ensure that the contact exists, it may have received a active message but never interacted
|
|
404
458
|
public async getThreads(startDate: Date | string, endDate: Date | string, opts: Omit<ConsumeOptions, 'skip'> = {}) {
|
|
405
459
|
const start = new Date(startDate)
|
|
406
460
|
const end = new Date(endDate)
|
|
@@ -4,6 +4,12 @@ import { type Command, type CommandMethods, type Domain, type Identity, Node, ty
|
|
|
4
4
|
import { randomId } from '../utils/random.ts'
|
|
5
5
|
import { type URI, uriToString } from '../utils/uri.ts'
|
|
6
6
|
|
|
7
|
+
type Unpacked<T> = T extends Array<infer U> ? U : T
|
|
8
|
+
type PromiseOrIterator<T, S extends boolean> = S extends true ? AsyncIterable<Unpacked<T>> : Promise<T>
|
|
9
|
+
type NamespaceCommand<TMethod extends CommandMethods = CommandMethods> = Omit<Command<TMethod>, 'uri' | 'id' | 'to'> & {
|
|
10
|
+
uri: URI
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
export type SendCommandOptions = {
|
|
8
14
|
to?: NodeLike
|
|
9
15
|
ownerIdentity?: Identity | Domain
|
|
@@ -15,8 +21,11 @@ export type ConsumeOptions = SendCommandOptions & {
|
|
|
15
21
|
skip?: number
|
|
16
22
|
fetchall?: boolean
|
|
17
23
|
max?: number
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
/**
|
|
25
|
+
* If true, it will try to parallel fetch data in chunks
|
|
26
|
+
* Be careful as this can be slower in situations with few data and will consume much more bandwidth
|
|
27
|
+
* Set together with max or fetchall
|
|
28
|
+
*/
|
|
20
29
|
optimistic?: boolean
|
|
21
30
|
}
|
|
22
31
|
|
|
@@ -31,93 +40,159 @@ export class Namespace {
|
|
|
31
40
|
return `postmaster@${this.ownerSubdomain ? `${this.ownerSubdomain}.` : ''}${this.defaultOptions?.domain ?? 'msging.net'}` as Identity
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
public
|
|
35
|
-
TMethod
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
>(
|
|
39
|
-
command:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
): Promise<TResponse> {
|
|
43
|
+
public sendCommand<TMethod extends CommandMethods, TResponse = void>(
|
|
44
|
+
command: NamespaceCommand<TMethod>,
|
|
45
|
+
opts: ConsumeOptions & { collection: true; stream: true },
|
|
46
|
+
): PromiseOrIterator<TResponse, true>
|
|
47
|
+
public sendCommand<TMethod extends CommandMethods, TResponse = void>(
|
|
48
|
+
command: NamespaceCommand<TMethod>,
|
|
49
|
+
opts?: ConsumeOptions & { collection?: boolean; stream?: false },
|
|
50
|
+
): PromiseOrIterator<TResponse, false>
|
|
51
|
+
public sendCommand<TMethod extends CommandMethods, TResponse = void>(
|
|
52
|
+
command: NamespaceCommand<TMethod>,
|
|
53
|
+
opts?: ConsumeOptions & { collection?: boolean; stream?: boolean },
|
|
54
|
+
): Promise<TResponse> | AsyncIterable<Unpacked<TResponse>> {
|
|
46
55
|
const options = { ...this.defaultOptions, ...opts }
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
if (options.stream) {
|
|
58
|
+
return this.sendIterableCommand(command, options)
|
|
59
|
+
} else if (options.collection) {
|
|
60
|
+
return this.sendCollectionCommand(command, options)
|
|
61
|
+
} else {
|
|
62
|
+
return this.sendPlainCommand(command, options)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
50
65
|
|
|
51
|
-
|
|
66
|
+
private prepareCommandContext<TMethod extends CommandMethods>(
|
|
67
|
+
command: NamespaceCommand<TMethod>,
|
|
68
|
+
options: ConsumeOptions,
|
|
69
|
+
) {
|
|
70
|
+
const domain =
|
|
71
|
+
options?.domain ??
|
|
72
|
+
(this.blipClient.sender instanceof ConnectionSender ? this.blipClient.sender.domain : 'msging.net')
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
return {
|
|
75
|
+
path: options?.ownerIdentity ? `lime://${options.ownerIdentity}${command.uri.path}` : command.uri.path,
|
|
76
|
+
query: new Map(command.uri.query),
|
|
77
|
+
owner: new Node('postmaster', `${this.ownerSubdomain ? `${this.ownerSubdomain}.` : ''}${domain}`),
|
|
78
|
+
take: options?.take ?? Math.min(options?.max ?? 100, 100),
|
|
55
79
|
}
|
|
80
|
+
}
|
|
56
81
|
|
|
57
|
-
|
|
58
|
-
|
|
82
|
+
private buildCommand<TMethod extends CommandMethods>(
|
|
83
|
+
base: NamespaceCommand<TMethod>,
|
|
84
|
+
path: string,
|
|
85
|
+
query: Map<string, string>,
|
|
86
|
+
owner: Node,
|
|
87
|
+
options: ConsumeOptions,
|
|
88
|
+
): Command<TMethod> {
|
|
89
|
+
return {
|
|
90
|
+
id: randomId(),
|
|
91
|
+
to: options.to ?? owner,
|
|
92
|
+
...base,
|
|
93
|
+
uri: uriToString({ path, query }),
|
|
94
|
+
} as Command<TMethod>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private extractCollection<T>(response: unknown): Array<T> {
|
|
98
|
+
if (response && typeof response === 'object') {
|
|
99
|
+
const r = response as Record<string, unknown>
|
|
100
|
+
if (Array.isArray(r.items)) return r.items as Array<T>
|
|
101
|
+
if (Array.isArray(r.data)) return r.data as Array<T>
|
|
59
102
|
}
|
|
103
|
+
return []
|
|
104
|
+
}
|
|
60
105
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
106
|
+
private async *sendIterableCommand<TMethod extends CommandMethods, TResponse = void>(
|
|
107
|
+
command: Omit<Command<TMethod>, 'uri' | 'id' | 'to'> & {
|
|
108
|
+
uri: URI
|
|
109
|
+
},
|
|
110
|
+
options: ConsumeOptions,
|
|
111
|
+
): AsyncIterable<Unpacked<TResponse>> {
|
|
112
|
+
const { path, query, owner, take } = this.prepareCommandContext(command, options)
|
|
113
|
+
let skip = options.skip ?? 0
|
|
114
|
+
let yielded = 0
|
|
115
|
+
|
|
116
|
+
while (true) {
|
|
117
|
+
const q = new Map(query).set('$take', take.toString()).set('$skip', skip.toString())
|
|
118
|
+
|
|
119
|
+
const commandToSend = this.buildCommand(command, path, q, owner, options)
|
|
73
120
|
const response = await this.blipClient.sender.sendCommand(commandToSend)
|
|
74
|
-
|
|
121
|
+
const items = this.extractCollection<Unpacked<TResponse>>(response)
|
|
122
|
+
|
|
123
|
+
if (!items.length) {
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const item of items) {
|
|
128
|
+
yield item
|
|
129
|
+
|
|
130
|
+
if (options.max && yielded++ >= options.max) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!options.fetchall || items.length < take) {
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
skip += take
|
|
75
140
|
}
|
|
141
|
+
}
|
|
76
142
|
|
|
143
|
+
private async sendPlainCommand<TMethod extends CommandMethods, TResponse = void>(
|
|
144
|
+
command: Omit<Command<TMethod>, 'uri' | 'id' | 'to'> & {
|
|
145
|
+
uri: URI
|
|
146
|
+
},
|
|
147
|
+
options: ConsumeOptions,
|
|
148
|
+
): Promise<TResponse> {
|
|
149
|
+
const { path, query, owner } = this.prepareCommandContext(command, options)
|
|
150
|
+
|
|
151
|
+
const commandToSend = this.buildCommand(command, path, query, owner, options)
|
|
152
|
+
const response = await this.blipClient.sender.sendCommand(commandToSend)
|
|
153
|
+
return response as TResponse
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async sendCollectionCommand<TMethod extends CommandMethods, TResponse = void>(
|
|
157
|
+
command: Omit<Command<TMethod>, 'uri' | 'id' | 'to'> & {
|
|
158
|
+
uri: URI
|
|
159
|
+
},
|
|
160
|
+
options: ConsumeOptions,
|
|
161
|
+
): Promise<TResponse> {
|
|
162
|
+
const { path, query, owner, take } = this.prepareCommandContext(command, options)
|
|
77
163
|
if (options.optimistic && !options.max && !options.fetchall) {
|
|
78
164
|
throw new Error('Optimistic consume requires max or fetchall to be set')
|
|
79
165
|
}
|
|
80
166
|
|
|
81
167
|
let skip = options.skip ?? 0
|
|
82
|
-
|
|
168
|
+
const results: Array<Unpacked<TResponse>> = []
|
|
83
169
|
|
|
170
|
+
query.set('$take', take.toString())
|
|
84
171
|
while (true) {
|
|
85
172
|
const remaining = options.max !== undefined ? options.max - results.length : Number.POSITIVE_INFINITY
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const response = (await this.blipClient.sender.sendCommand(commandToSend)) as
|
|
100
|
-
| { data: Array<TResponse> }
|
|
101
|
-
| { items: Array<TResponse> }
|
|
102
|
-
const items: Array<TResponse> =
|
|
103
|
-
'items' in response ? response.items : 'data' in response ? response.data : []
|
|
104
|
-
|
|
105
|
-
return items ?? []
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
const items = await Promise.all(tasks)
|
|
109
|
-
results = results.concat(items.flat())
|
|
173
|
+
const pageCapacity = options.optimistic ? Math.min(Math.ceil(remaining / take), 20) : 1
|
|
174
|
+
|
|
175
|
+
const items = await Promise.all(
|
|
176
|
+
Array.from({ length: pageCapacity }, async (_, i: number) => {
|
|
177
|
+
const q = new Map(query).set('$skip', (skip + i * take).toString())
|
|
178
|
+
const cmd = this.buildCommand(command, path, q, owner, options)
|
|
179
|
+
const response = await this.blipClient.sender.sendCommand(cmd)
|
|
180
|
+
return this.extractCollection<Unpacked<TResponse>>(response)
|
|
181
|
+
}),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const flattened = items.flat()
|
|
185
|
+
results.push(...flattened)
|
|
110
186
|
|
|
111
187
|
if (
|
|
112
188
|
!options.fetchall ||
|
|
113
|
-
|
|
114
|
-
results.length === 0 ||
|
|
189
|
+
flattened.length < take ||
|
|
115
190
|
(options.max !== undefined && results.length >= options.max)
|
|
116
191
|
) {
|
|
117
192
|
break
|
|
118
193
|
}
|
|
119
194
|
|
|
120
|
-
skip += take *
|
|
195
|
+
skip += take * pageCapacity
|
|
121
196
|
}
|
|
122
197
|
|
|
123
198
|
return results as TResponse
|
package/src/namespaces/portal.ts
CHANGED
|
@@ -114,9 +114,9 @@ export class EnvelopeResolver {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
public createEnvelopeResponsePromise(id: string): Promise<Envelope> {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
const { resolve, promise } = Promise.withResolvers<Envelope>()
|
|
118
|
+
this.waitingEnvelopeResponseResolvers[id] = resolve
|
|
119
|
+
return promise
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
public addListener<K extends keyof EventMap>(ev: K, listener: Listener<K>) {
|
package/src/sender/throttler.ts
CHANGED
|
@@ -1,36 +1,75 @@
|
|
|
1
1
|
export class EnvelopeThrottler {
|
|
2
|
-
private
|
|
2
|
+
private buckets = {
|
|
3
3
|
message: {
|
|
4
4
|
max: 50,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
tokens: 0,
|
|
6
|
+
lastRefill: 0,
|
|
7
|
+
waiting: [] as Array<() => void>,
|
|
8
|
+
timer: null as NodeJS.Timeout | null,
|
|
7
9
|
},
|
|
8
10
|
command: {
|
|
9
11
|
max: 200,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
tokens: 0,
|
|
13
|
+
lastRefill: 0,
|
|
14
|
+
waiting: [] as Array<() => void>,
|
|
15
|
+
timer: null as NodeJS.Timeout | null,
|
|
12
16
|
},
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
private readonly refillInterval = 1000 // ms
|
|
20
|
+
private readonly throughputFactor = 0.9
|
|
21
|
+
// memo for fast-path (zero allocation)
|
|
22
|
+
private readonly voidPromise = Promise.resolve()
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
public throttle(type: keyof typeof this.buckets): Promise<void> {
|
|
25
|
+
const bucket = this.buckets[type]
|
|
26
|
+
this.refillTokens(type)
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
if (bucket.tokens > 0) {
|
|
29
|
+
bucket.tokens--
|
|
30
|
+
return this.voidPromise
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { resolve, promise } = Promise.withResolvers<void>()
|
|
34
|
+
bucket.waiting.push(resolve)
|
|
35
|
+
|
|
36
|
+
this.scheduleRefill(type)
|
|
37
|
+
|
|
38
|
+
return promise
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private refillTokens(type: keyof typeof this.buckets): void {
|
|
42
|
+
const bucket = this.buckets[type]
|
|
43
|
+
const now = Date.now()
|
|
44
|
+
const elapsed = now - bucket.lastRefill
|
|
45
|
+
|
|
46
|
+
if (elapsed >= this.refillInterval) {
|
|
47
|
+
bucket.tokens = Math.floor(bucket.max * this.throughputFactor)
|
|
48
|
+
bucket.lastRefill = now
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private scheduleRefill(type: keyof typeof this.buckets): void {
|
|
53
|
+
const bucket = this.buckets[type]
|
|
54
|
+
if (bucket.timer) return
|
|
55
|
+
|
|
56
|
+
const delay = Math.max(0, this.refillInterval - (Date.now() - bucket.lastRefill))
|
|
57
|
+
|
|
58
|
+
bucket.timer = setTimeout(() => {
|
|
59
|
+
bucket.timer = null
|
|
60
|
+
this.refillTokens(type)
|
|
61
|
+
|
|
62
|
+
while (bucket.tokens > 0 && bucket.waiting.length > 0) {
|
|
63
|
+
const resolve = bucket.waiting.shift()
|
|
64
|
+
if (resolve) {
|
|
65
|
+
bucket.tokens--
|
|
66
|
+
resolve()
|
|
67
|
+
}
|
|
25
68
|
}
|
|
26
69
|
|
|
27
|
-
if (
|
|
28
|
-
this.
|
|
29
|
-
break
|
|
30
|
-
} else {
|
|
31
|
-
const wait = timeToReset - elapsed
|
|
32
|
-
await new Promise((resolve) => setTimeout(resolve, wait))
|
|
70
|
+
if (bucket.waiting.length > 0) {
|
|
71
|
+
this.scheduleRefill(type)
|
|
33
72
|
}
|
|
34
|
-
}
|
|
73
|
+
}, delay)
|
|
35
74
|
}
|
|
36
75
|
}
|