Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dex4er / js-file-timestamp-stream / 675

12 Jun 2021 - 13:05 coverage: 95.018% (-0.2%) from 95.238%
675

Pull #33

travis-ci

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
Merge d75b28a68 into 09f087434
Pull Request #33: Update dependency tslib to v2

37 of 52 branches covered (71.15%)

267 of 281 relevant lines covered (95.02%)

2.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.61
/src/file-timestamp-stream.ts
1
/// <reference types="node" />
2

3
import fs, {WriteStream} from "fs"
1×
4
import {Writable, WritableOptions} from "stream"
1×
5
import finished from "stream.finished"
1×
6
import strftime from "ultra-strftime"
1×
7

8
export interface FileTimestampStreamOptions extends WritableOptions {
9
  /** a string with [flags](https://nodejs.org/api/fs.html#fs_fs_open_path_flags_mode_callback) for opened stream (default: `'a'`) */
10
  flags?: string | null
11
  /** a custom [fs](https://nodejs.org/api/fs.html) module (optional) */
12
  fs?: typeof fs
13
  /** a template for new filenames (default: `'out.log'`) */
14
  path?: string
15
}
16

17
export class FileTimestampStream extends Writable {
1×
18
  static readonly CLOSE_UNUSED_FILE_AFTER = 1000
1×
19

20
  readonly flags = this.options.flags || "a"
6×
21
  readonly fs = this.options.fs || fs
6×
22
  readonly path = this.options.path || "out.log"
6×
23

24
  destroyed = false
6×
25

26
  /** contains last opened filename */
27
  protected currentFilename?: string
28
  /** contains current [fs.WriteStream](https://nodejs.org/api/fs.html#fs_class_fs_writestream) object */
29
  protected stream?: WriteStream
30

31
  private readonly streams: Map<string, WriteStream> = new Map()
6×
32
  private readonly streamCancelFinishers: Map<string, () => void> = new Map()
6×
33
  private readonly streamErrorHandlers: Map<string, (err: Error) => void> = new Map()
6×
34
  private readonly closers: Map<string, NodeJS.Timer> = new Map()
6×
35

36
  private closer?: NodeJS.Timer
37

38
  constructor(private options: FileTimestampStreamOptions = {}) {
6×
39
    super(options)
6×
40
  }
41

42
  _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
43
    if (this.destroyed) {
Branches [[4, 0]] missed. 8×
44
      return callback(new Error("write after destroy"))
!
45
    }
46

47
    try {
8×
48
      this.rotate()
8×
49
      this.stream!.write(chunk, encoding, callback)
7×
50
    } catch (e) {
51
      callback(e)
1×
52
    }
53
  }
54

55
  _writev(chunks: Array<{chunk: any; encoding: string}>, callback: (error?: Error | null) => void): void {
56
    if (this.destroyed) {
Branches [[5, 0]] missed. 1×
57
      return callback(new Error("write after destroy"))
!
58
    }
59

60
    let corked = false
1×
61
    try {
1×
62
      this.rotate()
1×
63
      corked = true
1×
64
      this.stream!.cork()
1×
65
      for (const chunk of chunks) {
1×
66
        this.stream!.write(chunk.chunk, chunk.encoding)
2×
67
      }
68
      process.nextTick(() => this.stream!.uncork())
1×
69
      callback()
1×
70
    } catch (e) {
71
      if (corked) {
Branches [[6, 0], [6, 1]] missed. !
72
        process.nextTick(() => this.stream!.uncork())
!
73
      }
74
      callback(e)
!
75
    }
76
  }
77

78
  _final(callback: (error?: Error | null) => void): void {
79
    if (this.stream) {
Branches [[7, 1]] missed. 2×
80
      this.stream.end(callback)
2×
81
    } else {
82
      callback()
!
83
    }
84
  }
85

86
  _destroy(error: Error | null, callback: (error: Error | null) => void): void {
87
    if (this.streamErrorHandlers.size > 0) {
5×
88
      for (const [filename, handler] of this.streamErrorHandlers) {
4×
89
        const stream = this.streams.get(filename)
6×
90
        if (stream) {
6×
91
          stream.removeListener("error", handler)
5×
92
        }
93
      }
94
      this.streamErrorHandlers.clear()
4×
95
    }
96
    if (this.streamCancelFinishers.size > 0) {
5×
97
      for (const [filename, cancel] of this.streamCancelFinishers) {
4×
98
        cancel()
5×
99
        this.streamCancelFinishers.delete(filename)
5×
100
      }
101
      this.streamCancelFinishers.clear()
4×
102
    }
103
    if (this.streams.size > 0) {
5×
104
      for (const stream of this.streams.values()) {
4×
105
        if (typeof stream.destroy === "function") {
Branches [[12, 1]] missed. 5×
106
          stream.destroy()
5×
107
        }
108
      }
109
      this.streams.clear()
4×
110
    }
111
    if (this.closers.size > 0) {
5×
112
      for (const closer of this.closers.values()) {
4×
113
        clearInterval(closer)
5×
114
      }
115
      this.streams.clear()
4×
116
    }
117

118
    this.destroyed = true
5×
119
    this.stream = undefined
5×
120
    this.closer = undefined
5×
121

122
    callback(error)
5×
123
  }
124

125
  /**
126
   * This method can be overriden in subclass
127
   *
128
   * The method generates a filename for new files. By default it returns new
129
   * filename based on path and current time.
130
   */
131
  protected newFilename(): string {
132
    return strftime(this.path, new Date())
7×
133
  }
134

135
  private rotate(): void {
136
    const newFilename = this.newFilename()
9×
137
    const {currentFilename, stream, closer} = this
9×
138

139
    if (newFilename !== currentFilename) {
9×
140
      if (currentFilename && stream && closer) {
Branches [[15, 0]] missed. 7×
141
        clearInterval(closer)
!
142
        stream.end()
!
143

144
        const streamErrorHandler = this.streamErrorHandlers.get(currentFilename)
!
145

146
        if (streamErrorHandler) {
Branches [[17, 0], [17, 1]] missed. !
147
          stream.removeListener("error", streamErrorHandler)
!
148
          this.streamErrorHandlers.delete(currentFilename)
!
149
        }
150
      }
151

152
      const newStream = this.fs.createWriteStream(newFilename, {
7×
153
        flags: this.flags,
154
      })
155
      this.stream = newStream
6×
156
      this.streams.set(newFilename, newStream)
6×
157

158
      const newStreamErrorHandler = (err: Error) => {
6×
UNCOV
159
        this.emit("error", err)
!
160
      }
161
      newStream.on("error", newStreamErrorHandler)
6×
162
      this.streamErrorHandlers.set(newFilename, newStreamErrorHandler)
6×
163

164
      const newCloser = setInterval(() => {
6×
165
        if (newFilename !== this.newFilename()) {
Branches [[18, 1]] missed. 1×
166
          clearInterval(newCloser)
1×
167
          this.closers.delete(newFilename)
1×
168
          newStream.end()
1×
169
        }
170
      }, FileTimestampStream.CLOSE_UNUSED_FILE_AFTER).unref()
171
      this.closer = closer
6×
172
      this.closers.set(newFilename, newCloser)
6×
173

174
      const newStreamCancelFinisher = finished(newStream, () => {
6×
175
        clearInterval(newCloser)
1×
176
        this.closers.delete(newFilename)
1×
177

178
        if (typeof newStream.destroy === "function") {
Branches [[19, 1]] missed. 1×
179
          newStream.destroy()
1×
180
        }
181
        this.streamCancelFinishers.delete(newFilename)
1×
182
        this.streams.delete(newFilename)
1×
183
      })
184
      this.streamCancelFinishers.set(newFilename, newStreamCancelFinisher)
6×
185

186
      this.currentFilename = newFilename
6×
187
    }
188
  }
189
}
190

191
export default FileTimestampStream
1×
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2023 Coveralls, Inc