speechflow 2.0.4 → 2.1.1

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +47 -15
  3. package/etc/speechflow.yaml +20 -48
  4. package/etc/stx.conf +2 -2
  5. package/package.json +6 -6
  6. package/speechflow-cli/dst/speechflow-node-a2a-gtcrn-wt.d.ts +1 -0
  7. package/speechflow-cli/dst/speechflow-node-a2a-gtcrn-wt.js +60 -0
  8. package/speechflow-cli/dst/speechflow-node-a2a-gtcrn-wt.js.map +1 -0
  9. package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.d.ts +15 -0
  10. package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js +234 -0
  11. package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js.map +1 -0
  12. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +2 -2
  13. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  14. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +42 -21
  15. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  16. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +1 -1
  17. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
  18. package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +26 -6
  19. package/speechflow-cli/dst/speechflow-node-t2t-profanity.js.map +1 -1
  20. package/speechflow-cli/dst/speechflow-node-t2t-sentence.d.ts +3 -2
  21. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +135 -51
  22. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
  23. package/speechflow-cli/dst/speechflow-node-t2t-simulator.d.ts +11 -0
  24. package/speechflow-cli/dst/speechflow-node-t2t-simulator.js +128 -0
  25. package/speechflow-cli/dst/speechflow-node-t2t-simulator.js.map +1 -0
  26. package/speechflow-cli/dst/speechflow-util-queue.d.ts +9 -3
  27. package/speechflow-cli/dst/speechflow-util-queue.js +39 -17
  28. package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
  29. package/speechflow-cli/etc/oxlint.jsonc +1 -0
  30. package/speechflow-cli/package.d/sherpa-onnx+1.12.25.patch +12 -0
  31. package/speechflow-cli/package.json +32 -26
  32. package/speechflow-cli/src/lib.d.ts +30 -4
  33. package/speechflow-cli/src/speechflow-node-a2a-gtcrn-wt.ts +68 -0
  34. package/speechflow-cli/src/speechflow-node-a2a-gtcrn.ts +219 -0
  35. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +2 -2
  36. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +48 -27
  37. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +1 -1
  38. package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +30 -11
  39. package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +152 -60
  40. package/speechflow-cli/src/speechflow-util-queue.ts +44 -19
  41. package/speechflow-ui-db/dst/app-font-fa-brands-400.woff2 +0 -0
  42. package/speechflow-ui-db/dst/app-font-fa-regular-400.woff2 +0 -0
  43. package/speechflow-ui-db/dst/app-font-fa-solid-900.woff2 +0 -0
  44. package/speechflow-ui-db/dst/app-font-fa-v4compatibility.woff2 +0 -0
  45. package/speechflow-ui-db/dst/index.css +1 -1
  46. package/speechflow-ui-db/dst/index.js +47 -46
  47. package/speechflow-ui-db/package.json +21 -18
  48. package/speechflow-ui-db/src/app.vue +64 -19
  49. package/speechflow-ui-st/dst/app-font-fa-brands-400.woff2 +0 -0
  50. package/speechflow-ui-st/dst/app-font-fa-regular-400.woff2 +0 -0
  51. package/speechflow-ui-st/dst/app-font-fa-solid-900.woff2 +0 -0
  52. package/speechflow-ui-st/dst/app-font-fa-v4compatibility.woff2 +0 -0
  53. package/speechflow-ui-st/dst/index.css +1 -1
  54. package/speechflow-ui-st/dst/index.js +65 -64
  55. package/speechflow-ui-st/package.json +22 -19
  56. package/speechflow-ui-st/src/app.vue +9 -8
@@ -14,13 +14,13 @@ import { Duration } from "luxon"
14
14
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
15
15
  import * as util from "./speechflow-util"
16
16
 
17
- /* text stream queue element */
17
+ /* text stream queue element */
18
18
  type TextQueueElement = {
19
- type: "text-frame",
20
- chunk: SpeechFlowChunk,
21
- complete?: boolean
19
+ type: "text-frame",
20
+ chunk: SpeechFlowChunk,
21
+ complete: boolean
22
22
  } | {
23
- type: "text-eof"
23
+ type: "text-eof"
24
24
  }
25
25
 
26
26
  /* SpeechFlow node for sentence splitting */
@@ -30,10 +30,10 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
30
30
 
31
31
  /* internal state */
32
32
  private queue = new util.Queue<TextQueueElement>()
33
- private queueRecv = this.queue.pointerUse("recv")
34
- private queueSplit = this.queue.pointerUse("split")
35
33
  private queueSend = this.queue.pointerUse("send")
36
- private closing = false
34
+ private queueSplit = this.queue.pointerUse("split")
35
+ private queueRecv = this.queue.pointerUse("recv")
36
+ private closing = false
37
37
  private workingOffTimer: ReturnType<typeof setTimeout> | null = null
38
38
 
39
39
  /* construct node */
@@ -41,13 +41,24 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
41
41
  super(id, cfg, opts, args)
42
42
 
43
43
  /* declare node configuration parameters */
44
- this.configure({})
44
+ this.configure({
45
+ timeout: { type: "number", pos: 0, val: 3 * 1000 },
46
+ interim: { type: "boolean", pos: 1, val: false }
47
+ })
45
48
 
46
49
  /* declare node input/output format */
47
50
  this.input = "text"
48
51
  this.output = "text"
49
52
  }
50
53
 
54
+ /* concatenate two payloads with proper whitespacing */
55
+ private concatPayload (s1: string, s2: string) {
56
+ if (!(s1.match(/\s+$/) || s2.match(/^\s+/)))
57
+ return `${s1} ${s2}`
58
+ else
59
+ return `${s1}${s2}`
60
+ }
61
+
51
62
  /* open node */
52
63
  async open () {
53
64
  /* clear destruction flag */
@@ -78,70 +89,102 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
78
89
  this.queueSplit.walk(+1)
79
90
  break
80
91
  }
81
- const chunk = element.chunk
82
- const payload = chunk.payload as string
83
- const m = payload.match(/^((?:.|\r?\n)+?[.;?!])\s*((?:.|\r?\n)*)$/)
84
- if (m !== null) {
85
- /* contains a sentence */
86
- const [ , sentence, rest ] = m
87
- if (rest !== "") {
88
- /* contains more than a sentence */
89
- const chunk2 = chunk.clone()
90
- const duration = Duration.fromMillis(
91
- chunk.timestampEnd.minus(chunk.timestampStart).toMillis() *
92
- (sentence.length / payload.length))
93
- chunk2.timestampStart = chunk.timestampStart.plus(duration)
94
- chunk.timestampEnd = chunk2.timestampStart
95
- chunk.payload = sentence
96
- chunk2.payload = rest
97
- element.complete = true
98
- this.queueSplit.touch()
99
- this.queueSplit.walk(+1)
100
- this.queueSplit.insert({ type: "text-frame", chunk: chunk2 })
101
- }
102
- else {
103
- /* contains just the sentence */
104
- element.complete = true
105
- this.queueSplit.touch()
106
- this.queueSplit.walk(+1)
107
- }
92
+
93
+ /* skip elements already completed */
94
+ if (element.type === "text-frame" && element.chunk.kind === "final" && element.complete === true) {
95
+ this.queueSplit.walk(+1)
96
+ continue
108
97
  }
109
- else {
110
- /* contains less than a sentence */
111
- const position = this.queueSplit.position()
112
- if (position < this.queueSplit.maxPosition() - 1) {
113
- /* merge into following chunk */
114
- const element2 = this.queueSplit.peek(position + 1)
115
- if (element2 === undefined)
116
- break
117
- if (element2.type === "text-eof") {
98
+
99
+ /* perform sentence splitting on input chunk */
100
+ if (element.chunk.kind === "final") {
101
+ const chunk = element.chunk
102
+ const payload = chunk.payload as string
103
+ const m = payload.match(/^((?:.|\r?\n)+?[.;?!])(?:\s+((?:.|\r?\n)+)|\s*)$/)
104
+ if (m !== null) {
105
+ /* contains a sentence */
106
+ const [ , sentence, rest ] = m
107
+ if (rest !== undefined && rest !== "") {
108
+ /* contains more than a sentence */
109
+ const chunk2 = chunk.clone()
110
+ const duration = Duration.fromMillis(
111
+ chunk.timestampEnd.minus(chunk.timestampStart).toMillis() *
112
+ (sentence.length / payload.length))
113
+ chunk2.timestampStart = chunk.timestampStart.plus(duration)
114
+ chunk.timestampEnd = chunk2.timestampStart
115
+ chunk.payload = sentence
116
+ chunk2.payload = rest
118
117
  element.complete = true
118
+ this.queue.silent(true)
119
119
  this.queueSplit.touch()
120
+ this.queue.silent(false)
121
+ this.queueSplit.walk(+1)
122
+ this.queueSplit.insert({ type: "text-frame", chunk: chunk2, complete: false })
123
+ }
124
+ else {
125
+ /* contains just the sentence */
126
+ element.complete = true
127
+ this.queue.silent(true)
128
+ this.queueSplit.silent(true)
129
+ const position = this.queueSplit.position()
120
130
  this.queueSplit.walk(+1)
131
+ this.queue.silent(false)
132
+ this.queueSplit.silent(false)
133
+ this.queueSplit.touch(position)
134
+ }
135
+ }
136
+ else {
137
+ /* contains less than a sentence */
138
+ const position = this.queueSplit.position()
139
+ if (position < this.queueSplit.maxPosition() - 1) {
140
+ /* merge into following chunk */
141
+ const element2 = this.queueSplit.peek(position + 1)
142
+ if (element2 === undefined)
143
+ break
144
+ if (element2.type === "text-eof") {
145
+ /* no more chunks: output as final
146
+ (perhaps incomplete sentence at end of stream) */
147
+ element.complete = true
148
+ this.queueSplit.walk(+1)
149
+ this.queueSplit.touch(this.queueSplit.position() - 1)
150
+ break
151
+ }
152
+ if (element2.chunk.kind === "final") {
153
+ /* merge into following chunk */
154
+ element2.chunk.timestampStart = element.chunk.timestampStart
155
+ element2.chunk.payload = this.concatPayload(element.chunk.payload as string,
156
+ element2.chunk.payload as string)
157
+
158
+ /* remove current element and touch now current element */
159
+ this.queue.silent(true)
160
+ this.queueSplit.delete()
161
+ this.queue.silent(false)
162
+ this.queueSplit.touch()
163
+ }
164
+ else
165
+ break
166
+ }
167
+ else {
168
+ /* no following chunk yet */
121
169
  break
122
170
  }
123
- element2.chunk.timestampStart = element.chunk.timestampStart
124
- element2.chunk.payload =
125
- (element.chunk.payload as string) + " " +
126
- (element2.chunk.payload as string)
127
- this.queueSplit.delete()
128
- this.queueSplit.touch()
129
171
  }
130
- else
131
- break
132
172
  }
173
+ else
174
+ break
133
175
  }
134
176
 
135
177
  /* re-initiate working off round (if still not destroyed) */
136
- workingOff = false
137
178
  if (!this.closing) {
138
179
  this.workingOffTimer = setTimeout(workOffQueue, 100)
139
180
  this.queue.once("write", workOffQueue)
140
181
  }
182
+ workingOff = false
141
183
  }
142
184
  this.queue.once("write", workOffQueue)
143
185
 
144
186
  /* provide Duplex stream and internally attach to classifier */
187
+ let previewed = false
145
188
  const self = this
146
189
  this.stream = new Stream.Duplex({
147
190
  writableObjectMode: true,
@@ -150,7 +193,7 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
150
193
  highWaterMark: 1,
151
194
 
152
195
  /* receive text chunk (writable side of stream) */
153
- write (chunk: SpeechFlowChunk, encoding, callback) {
196
+ write (chunk: SpeechFlowChunk, encoding: BufferEncoding, callback: (error?: Error | null) => void) {
154
197
  if (self.closing)
155
198
  callback(new Error("stream already destroyed"))
156
199
  else if (Buffer.isBuffer(chunk.payload))
@@ -158,14 +201,30 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
158
201
  else if (chunk.payload.length === 0)
159
202
  callback()
160
203
  else {
161
- self.log("info", `received text: ${JSON.stringify(chunk.payload)}`)
162
- self.queueRecv.append({ type: "text-frame", chunk })
204
+ /* final chunks: queue for sentence splitting */
205
+ self.log("info", `received text (${chunk.kind}): ${JSON.stringify(chunk.payload)}`)
206
+ const recvPos = self.queueRecv.position()
207
+ if (recvPos > 0) {
208
+ const element = self.queueRecv.peek(recvPos - 1)
209
+ if (element) {
210
+ if (element.type === "text-eof") {
211
+ callback(new Error("received text input after end-of-stream"))
212
+ return
213
+ }
214
+ if (element.chunk.kind === "intermediate") {
215
+ self.queueRecv.walk(-1)
216
+ self.queueRecv.delete()
217
+ }
218
+ }
219
+ }
220
+ previewed = false
221
+ self.queueRecv.append({ type: "text-frame", chunk, complete: false })
163
222
  callback()
164
223
  }
165
224
  },
166
225
 
167
226
  /* receive no more text chunks (writable side of stream) */
168
- final (callback) {
227
+ final (callback: (error?: Error | null) => void) {
169
228
  if (self.closing) {
170
229
  callback()
171
230
  return
@@ -192,6 +251,8 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
192
251
  else if (element !== undefined
193
252
  && element.type === "text-frame"
194
253
  && element.complete === true) {
254
+ /* send all consecutive complete chunks */
255
+ let eofSeen = false
195
256
  while (true) {
196
257
  const nextElement = self.queueSend.peek()
197
258
  if (nextElement === undefined)
@@ -199,16 +260,47 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
199
260
  else if (nextElement.type === "text-eof") {
200
261
  this.push(null)
201
262
  self.queueSend.walk(+1)
263
+ eofSeen = true
202
264
  break
203
265
  }
204
266
  else if (nextElement.type === "text-frame"
205
267
  && nextElement.complete !== true)
206
268
  break
207
- self.log("info", `send text: ${JSON.stringify(nextElement.chunk.payload)}`)
269
+ self.log("info", `send text 1 (${nextElement.chunk.kind}): ${JSON.stringify(nextElement.chunk.payload)} pos=${self.queueSend.position()}`)
208
270
  this.push(nextElement.chunk)
209
271
  self.queueSend.walk(+1)
210
272
  self.queue.trim()
211
273
  }
274
+
275
+ /* wait for more data (unless end-of-stream was reached) */
276
+ if (!eofSeen && !self.closing)
277
+ self.queue.once("write", flushPendingChunks)
278
+ }
279
+ else if (element !== undefined
280
+ && element.type === "text-frame"
281
+ && element.complete === false
282
+ && !previewed
283
+ && self.params.interim === true) {
284
+ /* merge together all still queued elements and
285
+ send this out as an intermediate chunk as preview */
286
+ const previewChunk = element.chunk.clone()
287
+ previewChunk.kind = "intermediate"
288
+ for (let pos = self.queueSend.position() + 1; pos < self.queueSend.maxPosition(); pos++) {
289
+ const element2 = self.queueSend.peek(pos)
290
+ if (!element2)
291
+ continue
292
+ if (element2.type === "text-eof")
293
+ break
294
+ previewChunk.payload = self.concatPayload(
295
+ previewChunk.payload as string, element2.chunk.payload as string)
296
+ }
297
+ this.push(previewChunk)
298
+ self.log("info", `send text 2 (intermediate): ${JSON.stringify(previewChunk.payload)}`)
299
+ previewed = true
300
+
301
+ /* wait for more data */
302
+ if (!self.closing)
303
+ self.queue.once("write", flushPendingChunks)
212
304
  }
213
305
  else if (!self.closing)
214
306
  self.queue.once("write", flushPendingChunks)
@@ -223,7 +315,7 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
223
315
  /* indicate closing */
224
316
  this.closing = true
225
317
 
226
- /* clean up timer */
318
+ /* clean up timers */
227
319
  if (this.workingOffTimer !== null) {
228
320
  clearTimeout(this.workingOffTimer)
229
321
  this.workingOffTimer = null
@@ -97,6 +97,7 @@ export type QueueElement = { type: string }
97
97
  export class QueuePointer<T extends QueueElement> extends EventEmitter {
98
98
  /* internal state */
99
99
  private index = 0
100
+ private silence = false
100
101
 
101
102
  /* construction */
102
103
  constructor (
@@ -107,6 +108,17 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
107
108
  this.setMaxListeners(100)
108
109
  }
109
110
 
111
+ /* control silence operation */
112
+ silent (silence: boolean) {
113
+ this.silence = silence
114
+ }
115
+
116
+ /* notify about operation */
117
+ notify (event: string, info: any) {
118
+ if (!this.silence)
119
+ this.emit(event, info)
120
+ }
121
+
110
122
  /* positioning operations */
111
123
  maxPosition () {
112
124
  return this.queue.elements.length
@@ -114,7 +126,7 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
114
126
  position (index?: number): number {
115
127
  if (index !== undefined) {
116
128
  this.index = Math.max(0, Math.min(index, this.queue.elements.length))
117
- this.emit("position", this.index)
129
+ this.notify("position", this.index)
118
130
  }
119
131
  return this.index
120
132
  }
@@ -125,19 +137,19 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
125
137
  else if (num < 0)
126
138
  this.index = Math.max(this.index + num, 0)
127
139
  if (this.index !== indexOld)
128
- this.emit("position", { start: this.index })
140
+ this.notify("position", { start: this.index })
129
141
  }
130
142
  walkForwardUntil (type: T["type"]) {
131
143
  while (this.index < this.queue.elements.length
132
144
  && this.queue.elements[this.index].type !== type)
133
145
  this.index++
134
- this.emit("position", { start: this.index })
146
+ this.notify("position", { start: this.index })
135
147
  }
136
148
  walkBackwardUntil (type: T["type"]) {
137
149
  while (this.index > 0
138
150
  && this.queue.elements[this.index].type !== type)
139
151
  this.index--
140
- this.emit("position", { start: this.index })
152
+ this.notify("position", { start: this.index })
141
153
  }
142
154
 
143
155
  /* search operations */
@@ -146,7 +158,7 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
146
158
  while (position < this.queue.elements.length
147
159
  && this.queue.elements[position].type !== type)
148
160
  position++
149
- this.emit("search", { start: this.index, end: position })
161
+ this.notify("search", { start: this.index, end: position })
150
162
  return position
151
163
  }
152
164
  searchBackward (type: T["type"]) {
@@ -154,24 +166,24 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
154
166
  while (position > 0
155
167
  && this.queue.elements[position].type !== type)
156
168
  position--
157
- this.emit("search", { start: position, end: this.index })
169
+ this.notify("search", { start: position, end: this.index })
158
170
  return position
159
171
  }
160
172
 
161
173
  /* reading operations */
162
- peek (position?: number) {
174
+ peek (position?: number): T | undefined {
163
175
  if (position === undefined)
164
176
  position = this.index
165
177
  position = Math.max(0, Math.min(position, this.queue.elements.length))
166
178
  const element = this.queue.elements[position]
167
- this.queue.emit("read", { start: position, end: position })
179
+ this.queue.notify("read", { start: position, end: position })
168
180
  return element
169
181
  }
170
- read () {
182
+ read (): T | undefined {
171
183
  const element = this.queue.elements[this.index]
172
184
  if (this.index < this.queue.elements.length)
173
185
  this.index++
174
- this.queue.emit("read", { start: this.index - 1, end: this.index - 1 })
186
+ this.queue.notify("read", { start: this.index - 1, end: this.index - 1 })
175
187
  return element
176
188
  }
177
189
  slice (size?: number) {
@@ -186,30 +198,32 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
186
198
  slice = this.queue.elements.slice(this.index)
187
199
  this.index = this.queue.elements.length
188
200
  }
189
- this.queue.emit("read", { start, end: this.index })
201
+ this.queue.notify("read", { start, end: this.index })
190
202
  return slice
191
203
  }
192
204
 
193
205
  /* writing operations */
194
- touch () {
195
- if (this.index >= this.queue.elements.length)
196
- throw new Error("cannot touch after last element")
197
- this.queue.emit("write", { start: this.index, end: this.index + 1 })
206
+ touch (position?: number) {
207
+ if (position === undefined)
208
+ position = this.index
209
+ if (position >= this.queue.elements.length)
210
+ throw new Error(`cannot touch after last element ${position} ${this.queue.elements.length}`)
211
+ this.queue.notify("write", { start: position, end: position, op: "touch" })
198
212
  }
199
213
  append (element: T) {
200
214
  this.queue.elements.push(element)
201
215
  this.index = this.queue.elements.length
202
- this.queue.emit("write", { start: this.index - 1, end: this.index - 1 })
216
+ this.queue.notify("write", { start: this.index - 1, end: this.index - 1, op: "append" })
203
217
  }
204
218
  insert (element: T) {
205
219
  this.queue.elements.splice(this.index, 0, element)
206
- this.queue.emit("write", { start: this.index - 1, end: this.index })
220
+ this.queue.notify("write", { start: this.index, end: this.index, op: "insert" })
207
221
  }
208
222
  delete () {
209
223
  if (this.index >= this.queue.elements.length)
210
224
  throw new Error("cannot delete after last element")
211
225
  this.queue.elements.splice(this.index, 1)
212
- this.queue.emit("write", { start: this.index, end: this.index })
226
+ this.queue.notify("write", { start: this.index, end: this.index, op: "delete" })
213
227
  }
214
228
  }
215
229
 
@@ -217,10 +231,18 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
217
231
  export class Queue<T extends QueueElement> extends EventEmitter {
218
232
  public elements: T[] = []
219
233
  private pointers = new Map<string, QueuePointer<T>>()
234
+ private silence = false
220
235
  constructor () {
221
236
  super()
222
237
  this.setMaxListeners(100)
223
238
  }
239
+ silent (silence: boolean) {
240
+ this.silence = silence
241
+ }
242
+ notify (event: string, info: any) {
243
+ if (!this.silence)
244
+ this.emit(event, info)
245
+ }
224
246
  pointerUse (name: string): QueuePointer<T> {
225
247
  if (!this.pointers.has(name))
226
248
  this.pointers.set(name, new QueuePointer<T>(name, this))
@@ -234,9 +256,10 @@ export class Queue<T extends QueueElement> extends EventEmitter {
234
256
  trim (): void {
235
257
  /* determine minimum pointer position */
236
258
  let min = this.elements.length
237
- for (const pointer of this.pointers.values())
259
+ for (const pointer of this.pointers.values()) {
238
260
  if (min > pointer.position())
239
261
  min = pointer.position()
262
+ }
240
263
 
241
264
  /* trim the maximum amount of first elements */
242
265
  if (min > 0) {
@@ -245,6 +268,8 @@ export class Queue<T extends QueueElement> extends EventEmitter {
245
268
  /* shift all pointers */
246
269
  for (const pointer of this.pointers.values())
247
270
  pointer.position(pointer.position() - min)
271
+
272
+ this.notify("write", { start: 0, end: min, op: "trim" })
248
273
  }
249
274
  }
250
275
  }