dd-trace 2.31.0 → 2.33.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.
Files changed (38) hide show
  1. package/package.json +6 -6
  2. package/packages/datadog-instrumentations/src/body-parser.js +15 -9
  3. package/packages/datadog-instrumentations/src/express.js +32 -0
  4. package/packages/datadog-instrumentations/src/http/server.js +2 -1
  5. package/packages/datadog-instrumentations/src/mocha.js +8 -4
  6. package/packages/datadog-instrumentations/src/pg.js +3 -4
  7. package/packages/datadog-instrumentations/src/playwright.js +14 -1
  8. package/packages/datadog-plugin-http/src/server.js +2 -2
  9. package/packages/datadog-plugin-http2/src/server.js +0 -5
  10. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -0
  11. package/packages/datadog-plugin-pg/src/index.js +5 -5
  12. package/packages/dd-trace/src/appsec/addresses.js +0 -3
  13. package/packages/dd-trace/src/appsec/blocked_templates.js +2 -9
  14. package/packages/dd-trace/src/appsec/blocking.js +1 -1
  15. package/packages/dd-trace/src/appsec/{gateway/channels.js → channels.js} +4 -4
  16. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -1
  17. package/packages/dd-trace/src/appsec/index.js +87 -79
  18. package/packages/dd-trace/src/appsec/recommended.json +448 -121
  19. package/packages/dd-trace/src/appsec/remote_config/apply_states.js +7 -0
  20. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -0
  21. package/packages/dd-trace/src/appsec/remote_config/index.js +29 -10
  22. package/packages/dd-trace/src/appsec/remote_config/manager.js +33 -12
  23. package/packages/dd-trace/src/appsec/reporter.js +27 -58
  24. package/packages/dd-trace/src/appsec/rule_manager.js +160 -32
  25. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +4 -12
  26. package/packages/dd-trace/src/appsec/waf/index.js +75 -0
  27. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +57 -0
  28. package/packages/dd-trace/src/appsec/waf/waf_manager.js +66 -0
  29. package/packages/dd-trace/src/encode/0.4.js +12 -4
  30. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +14 -4
  31. package/packages/dd-trace/src/plugins/util/test.js +34 -17
  32. package/packages/dd-trace/src/telemetry/send-data.js +13 -3
  33. package/packages/dd-trace/src/appsec/callbacks/ddwaf.js +0 -137
  34. package/packages/dd-trace/src/appsec/callbacks/index.js +0 -7
  35. package/packages/dd-trace/src/appsec/gateway/als.js +0 -6
  36. package/packages/dd-trace/src/appsec/gateway/engine/engine.js +0 -140
  37. package/packages/dd-trace/src/appsec/gateway/engine/index.js +0 -51
  38. package/packages/dd-trace/src/appsec/gateway/engine/runner.js +0 -42
@@ -0,0 +1,57 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../log')
4
+ const Reporter = require('../reporter')
5
+
6
+ class WAFContextWrapper {
7
+ constructor (ddwafContext, requiredAddresses, wafTimeout, rulesInfo) {
8
+ this.ddwafContext = ddwafContext
9
+ this.requiredAddresses = requiredAddresses
10
+ this.wafTimeout = wafTimeout
11
+ this.rulesInfo = rulesInfo
12
+ }
13
+
14
+ run (params) {
15
+ const inputs = {}
16
+ let someInputAdded = false
17
+
18
+ // TODO: possible optimizaion: only send params that haven't already been sent with same value to this wafContext
19
+ for (const key of Object.keys(params)) {
20
+ if (this.requiredAddresses.has(key)) {
21
+ inputs[key] = params[key]
22
+ someInputAdded = true
23
+ }
24
+ }
25
+
26
+ if (!someInputAdded) return
27
+
28
+ try {
29
+ const start = process.hrtime.bigint()
30
+
31
+ const result = this.ddwafContext.run(inputs, this.wafTimeout)
32
+
33
+ const end = process.hrtime.bigint()
34
+
35
+ Reporter.reportMetrics({
36
+ duration: result.totalRuntime / 1e3,
37
+ durationExt: parseInt(end - start) / 1e3,
38
+ rulesVersion: this.rulesInfo.version
39
+ })
40
+
41
+ if (result.data && result.data !== '[]') {
42
+ Reporter.reportAttack(result.data)
43
+ }
44
+
45
+ return result.actions
46
+ } catch (err) {
47
+ log.error('Error while running the AppSec WAF')
48
+ log.error(err)
49
+ }
50
+ }
51
+
52
+ dispose () {
53
+ this.ddwafContext.dispose()
54
+ }
55
+ }
56
+
57
+ module.exports = WAFContextWrapper
@@ -0,0 +1,66 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../log')
4
+ const Reporter = require('../reporter')
5
+ const WAFContextWrapper = require('./waf_context_wrapper')
6
+
7
+ const contexts = new WeakMap()
8
+
9
+ class WAFManager {
10
+ constructor (rules, config) {
11
+ this.config = config
12
+ this.wafTimeout = config.wafTimeout
13
+ this.ddwaf = this._loadDDWAF(rules)
14
+ this._reportMetrics()
15
+ }
16
+
17
+ _loadDDWAF (rules) {
18
+ try {
19
+ // require in `try/catch` because this can throw at require time
20
+ const { DDWAF } = require('@datadog/native-appsec')
21
+
22
+ const { obfuscatorKeyRegex, obfuscatorValueRegex } = this.config
23
+ return new DDWAF(rules, { obfuscatorKeyRegex, obfuscatorValueRegex })
24
+ } catch (err) {
25
+ log.error('AppSec could not load native package. In-app WAF features will not be available.')
26
+
27
+ throw err
28
+ }
29
+ }
30
+
31
+ _reportMetrics () {
32
+ Reporter.metricsQueue.set('_dd.appsec.waf.version', this.ddwaf.constructor.version())
33
+
34
+ const { loaded, failed, errors } = this.ddwaf.rulesInfo
35
+
36
+ Reporter.metricsQueue.set('_dd.appsec.event_rules.loaded', loaded)
37
+ Reporter.metricsQueue.set('_dd.appsec.event_rules.error_count', failed)
38
+ if (failed) Reporter.metricsQueue.set('_dd.appsec.event_rules.errors', JSON.stringify(errors))
39
+
40
+ Reporter.metricsQueue.set('manual.keep', 'true')
41
+ }
42
+
43
+ getWAFContext (req) {
44
+ let wafContext = contexts.get(req)
45
+
46
+ if (!wafContext) {
47
+ wafContext = new WAFContextWrapper(
48
+ this.ddwaf.createContext(),
49
+ this.ddwaf.requiredAddresses,
50
+ this.wafTimeout,
51
+ this.ddwaf.rulesInfo
52
+ )
53
+ contexts.set(req, wafContext)
54
+ }
55
+
56
+ return wafContext
57
+ }
58
+
59
+ destroy () {
60
+ if (this.ddwaf) {
61
+ this.ddwaf.dispose()
62
+ }
63
+ }
64
+ }
65
+
66
+ module.exports = WAFManager
@@ -3,6 +3,8 @@
3
3
  const { truncateSpan, normalizeSpan } = require('./tags-processors')
4
4
  const Chunk = require('./chunk')
5
5
  const log = require('../log')
6
+ const { isTrue } = require('../util')
7
+ const coalesce = require('koalas')
6
8
 
7
9
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
8
10
 
@@ -24,6 +26,10 @@ class AgentEncoder {
24
26
  this._stringBytes = new Chunk()
25
27
  this._writer = writer
26
28
  this._reset()
29
+ this._debugEncoding = isTrue(coalesce(
30
+ process.env.DD_TRACE_ENCODING_DEBUG,
31
+ false
32
+ ))
27
33
  }
28
34
 
29
35
  count () {
@@ -40,11 +46,13 @@ class AgentEncoder {
40
46
 
41
47
  const end = bytes.length
42
48
 
43
- log.debug(() => {
44
- const hex = bytes.buffer.subarray(start, end).toString('hex').match(/../g).join(' ')
49
+ if (this._debugEncoding) {
50
+ log.debug(() => {
51
+ const hex = bytes.buffer.subarray(start, end).toString('hex').match(/../g).join(' ')
45
52
 
46
- return `Adding encoded trace to buffer: ${hex}`
47
- })
53
+ return `Adding encoded trace to buffer: ${hex}`
54
+ })
55
+ }
48
56
 
49
57
  // we can go over the soft limit since the agent has a 50MB hard limit
50
58
  if (this._traceBytes.length > this._limit || this._stringBytes.length > this._limit) {
@@ -129,12 +129,17 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
129
129
  _encodeEventContent (bytes, content) {
130
130
  const keysLength = Object.keys(content).length
131
131
 
132
+ let totalKeysLength = keysLength
132
133
  if (content.meta.test_session_id) {
133
- this._encodeMapPrefix(bytes, keysLength + 3)
134
- } else {
135
- this._encodeMapPrefix(bytes, keysLength)
134
+ totalKeysLength = totalKeysLength + 1
136
135
  }
137
-
136
+ if (content.meta.test_module_id) {
137
+ totalKeysLength = totalKeysLength + 1
138
+ }
139
+ if (content.meta.test_suite_id) {
140
+ totalKeysLength = totalKeysLength + 1
141
+ }
142
+ this._encodeMapPrefix(bytes, totalKeysLength)
138
143
  if (content.type) {
139
144
  this._encodeString(bytes, 'type')
140
145
  this._encodeString(bytes, content.type)
@@ -170,15 +175,20 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
170
175
  this._encodeString(bytes, 'test_session_id')
171
176
  this._encodeId(bytes, id(content.meta.test_session_id, 10))
172
177
  delete content.meta.test_session_id
178
+ }
173
179
 
180
+ if (content.meta.test_module_id) {
174
181
  this._encodeString(bytes, 'test_module_id')
175
182
  this._encodeId(bytes, id(content.meta.test_module_id, 10))
176
183
  delete content.meta.test_module_id
184
+ }
177
185
 
186
+ if (content.meta.test_suite_id) {
178
187
  this._encodeString(bytes, 'test_suite_id')
179
188
  this._encodeId(bytes, id(content.meta.test_suite_id, 10))
180
189
  delete content.meta.test_suite_id
181
190
  }
191
+
182
192
  this._encodeString(bytes, 'meta')
183
193
  this._encodeMap(bytes, content.meta)
184
194
  this._encodeString(bytes, 'metrics')
@@ -50,12 +50,10 @@ const CI_APP_ORIGIN = 'ciapp-test'
50
50
  const JEST_TEST_RUNNER = 'test.jest.test_runner'
51
51
 
52
52
  const TEST_ITR_TESTS_SKIPPED = '_dd.ci.itr.tests_skipped'
53
- const TEST_SESSION_ITR_SKIPPING_ENABLED = 'test_session.itr.tests_skipping.enabled'
54
- const TEST_SESSION_CODE_COVERAGE_ENABLED = 'test_session.code_coverage.enabled'
55
- const TEST_MODULE_ITR_SKIPPING_ENABLED = 'test_module.itr.tests_skipping.enabled'
56
- const TEST_MODULE_CODE_COVERAGE_ENABLED = 'test_module.code_coverage.enabled'
53
+ const TEST_ITR_SKIPPING_ENABLED = 'test.itr.tests_skipping.enabled'
54
+ const TEST_CODE_COVERAGE_ENABLED = 'test.code_coverage.enabled'
57
55
 
58
- const TEST_CODE_COVERAGE_LINES_TOTAL = 'test.codecov_lines_total'
56
+ const TEST_CODE_COVERAGE_LINES_PCT = 'test.code_coverage.lines_pct'
59
57
 
60
58
  // jest worker variables
61
59
  const JEST_WORKER_TRACE_PAYLOAD_CODE = 60
@@ -97,17 +95,16 @@ module.exports = {
97
95
  TEST_SUITE_ID,
98
96
  TEST_ITR_TESTS_SKIPPED,
99
97
  TEST_MODULE,
100
- TEST_SESSION_ITR_SKIPPING_ENABLED,
101
- TEST_SESSION_CODE_COVERAGE_ENABLED,
102
- TEST_MODULE_ITR_SKIPPING_ENABLED,
103
- TEST_MODULE_CODE_COVERAGE_ENABLED,
104
- TEST_CODE_COVERAGE_LINES_TOTAL,
98
+ TEST_ITR_SKIPPING_ENABLED,
99
+ TEST_CODE_COVERAGE_ENABLED,
100
+ TEST_CODE_COVERAGE_LINES_PCT,
105
101
  addIntelligentTestRunnerSpanTags,
106
102
  getCoveredFilenamesFromCoverage,
107
103
  resetCoverage,
108
104
  mergeCoverage,
109
105
  fromCoverageMapToCoverage,
110
- getTestLineStart
106
+ getTestLineStart,
107
+ getCallSites
111
108
  }
112
109
 
113
110
  // Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19
@@ -316,17 +313,17 @@ function addIntelligentTestRunnerSpanTags (
316
313
  { isSuitesSkipped, isSuitesSkippingEnabled, isCodeCoverageEnabled, testCodeCoverageLinesTotal }
317
314
  ) {
318
315
  testSessionSpan.setTag(TEST_ITR_TESTS_SKIPPED, isSuitesSkipped ? 'true' : 'false')
319
- testSessionSpan.setTag(TEST_SESSION_ITR_SKIPPING_ENABLED, isSuitesSkippingEnabled ? 'true' : 'false')
320
- testSessionSpan.setTag(TEST_SESSION_CODE_COVERAGE_ENABLED, isCodeCoverageEnabled ? 'true' : 'false')
316
+ testSessionSpan.setTag(TEST_ITR_SKIPPING_ENABLED, isSuitesSkippingEnabled ? 'true' : 'false')
317
+ testSessionSpan.setTag(TEST_CODE_COVERAGE_ENABLED, isCodeCoverageEnabled ? 'true' : 'false')
321
318
 
322
319
  testModuleSpan.setTag(TEST_ITR_TESTS_SKIPPED, isSuitesSkipped ? 'true' : 'false')
323
- testModuleSpan.setTag(TEST_MODULE_ITR_SKIPPING_ENABLED, isSuitesSkippingEnabled ? 'true' : 'false')
324
- testModuleSpan.setTag(TEST_MODULE_CODE_COVERAGE_ENABLED, isCodeCoverageEnabled ? 'true' : 'false')
320
+ testModuleSpan.setTag(TEST_ITR_SKIPPING_ENABLED, isSuitesSkippingEnabled ? 'true' : 'false')
321
+ testModuleSpan.setTag(TEST_CODE_COVERAGE_ENABLED, isCodeCoverageEnabled ? 'true' : 'false')
325
322
 
326
323
  // If suites have been skipped we don't want to report the total coverage, as it will be wrong
327
324
  if (testCodeCoverageLinesTotal !== undefined && !isSuitesSkipped) {
328
- testSessionSpan.setTag(TEST_CODE_COVERAGE_LINES_TOTAL, testCodeCoverageLinesTotal)
329
- testModuleSpan.setTag(TEST_CODE_COVERAGE_LINES_TOTAL, testCodeCoverageLinesTotal)
325
+ testSessionSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
326
+ testModuleSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
330
327
  }
331
328
  }
332
329
 
@@ -399,3 +396,23 @@ function getTestLineStart (err, testSuitePath) {
399
396
  return null
400
397
  }
401
398
  }
399
+
400
+ // From https://github.com/felixge/node-stack-trace/blob/ba06dcdb50d465cd440d84a563836e293b360427/index.js#L1
401
+ function getCallSites () {
402
+ const oldLimit = Error.stackTraceLimit
403
+ Error.stackTraceLimit = Infinity
404
+
405
+ const dummy = {}
406
+
407
+ const v8Handler = Error.prepareStackTrace
408
+ Error.prepareStackTrace = function (_, v8StackTrace) {
409
+ return v8StackTrace
410
+ }
411
+ Error.captureStackTrace(dummy)
412
+
413
+ const v8StackTrace = dummy.stack
414
+ Error.prepareStackTrace = v8Handler
415
+ Error.stackTraceLimit = oldLimit
416
+
417
+ return v8StackTrace
418
+ }
@@ -1,5 +1,17 @@
1
1
  const request = require('../exporters/common/request')
2
2
  let seqId = 0
3
+
4
+ function getPayload (payload) {
5
+ // Some telemetry endpoints payloads accept collections of elements such as the 'logs' endpoint.
6
+ // 'logs' request type payload is meant to send library logs to Datadog’s backend.
7
+ if (Array.isArray(payload)) {
8
+ return payload
9
+ } else {
10
+ const { logger, tags, serviceMapping, ...trimmedPayload } = payload
11
+ return trimmedPayload
12
+ }
13
+ }
14
+
3
15
  function sendData (config, application, host, reqType, payload = {}) {
4
16
  const {
5
17
  hostname,
@@ -7,8 +19,6 @@ function sendData (config, application, host, reqType, payload = {}) {
7
19
  url
8
20
  } = config
9
21
 
10
- const { logger, tags, serviceMapping, ...trimmedPayload } = payload
11
-
12
22
  const options = {
13
23
  url,
14
24
  hostname,
@@ -27,7 +37,7 @@ function sendData (config, application, host, reqType, payload = {}) {
27
37
  tracer_time: Math.floor(Date.now() / 1000),
28
38
  runtime_id: config.tags['runtime-id'],
29
39
  seq_id: ++seqId,
30
- payload: trimmedPayload,
40
+ payload: getPayload(payload),
31
41
  application,
32
42
  host
33
43
  })
@@ -1,137 +0,0 @@
1
- 'use strict'
2
-
3
- const log = require('../../log')
4
- const addresses = require('../addresses')
5
- const Gateway = require('../gateway/engine')
6
- const Reporter = require('../reporter')
7
-
8
- const validAddressSet = new Set(Object.values(addresses))
9
-
10
- // TODO: put reusable code in a base class
11
- class WAFCallback {
12
- static loadDDWAF (rules, config) {
13
- try {
14
- // require in `try/catch` because this can throw at require time
15
- const { DDWAF } = require('@datadog/native-appsec')
16
-
17
- return new DDWAF(rules, config)
18
- } catch (err) {
19
- log.error('AppSec could not load native package. In-app WAF features will not be available.')
20
-
21
- throw err
22
- }
23
- }
24
-
25
- constructor (rules, config) {
26
- const { wafTimeout, obfuscatorKeyRegex, obfuscatorValueRegex } = config
27
-
28
- this.ddwaf = WAFCallback.loadDDWAF(rules, { obfuscatorKeyRegex, obfuscatorValueRegex })
29
-
30
- this.wafTimeout = wafTimeout
31
-
32
- Reporter.metricsQueue.set('_dd.appsec.waf.version', this.ddwaf.constructor.version())
33
-
34
- const { loaded, failed, errors } = this.ddwaf.rulesInfo
35
-
36
- Reporter.metricsQueue.set('_dd.appsec.event_rules.loaded', loaded)
37
- Reporter.metricsQueue.set('_dd.appsec.event_rules.error_count', failed)
38
- if (failed) Reporter.metricsQueue.set('_dd.appsec.event_rules.errors', JSON.stringify(errors))
39
-
40
- Reporter.metricsQueue.set('manual.keep', 'true')
41
-
42
- this.wafContextCache = new WeakMap()
43
-
44
- // closures are faster than binds
45
- const self = this
46
- const method = (params, store) => {
47
- return self.action(params, store)
48
- }
49
-
50
- // might be its own class with more info later
51
- const callback = { method }
52
-
53
- const subscribedAddresses = new Set()
54
-
55
- for (const rule of rules.rules) {
56
- for (const condition of rule.conditions) {
57
- for (const input of condition.parameters.inputs) {
58
- const address = input.address.split(':', 2)[0]
59
-
60
- if (!validAddressSet.has(address) || subscribedAddresses.has(address)) continue
61
-
62
- subscribedAddresses.add(address)
63
-
64
- Gateway.manager.addSubscription({ addresses: [address], callback })
65
- }
66
- }
67
- }
68
- }
69
-
70
- action (params, store) {
71
- let wafContext
72
-
73
- if (store) {
74
- const key = store.get('context')
75
-
76
- if (key) {
77
- if (this.wafContextCache.has(key)) {
78
- wafContext = this.wafContextCache.get(key)
79
- } else {
80
- wafContext = this.ddwaf.createContext()
81
- this.wafContextCache.set(key, wafContext)
82
- }
83
- }
84
- }
85
-
86
- if (!wafContext || wafContext.disposed) {
87
- wafContext = this.ddwaf.createContext()
88
- }
89
-
90
- // cast status code to string
91
- if (params[addresses.HTTP_INCOMING_RESPONSE_CODE]) {
92
- params[addresses.HTTP_INCOMING_RESPONSE_CODE] = params[addresses.HTTP_INCOMING_RESPONSE_CODE] + ''
93
- }
94
-
95
- try {
96
- // TODO: possible optimizaion: only send params that haven't already been sent to this wafContext
97
- const start = process.hrtime.bigint()
98
-
99
- const result = wafContext.run(params, this.wafTimeout)
100
-
101
- result.durationExt = parseInt(process.hrtime.bigint() - start)
102
-
103
- return this.applyResult(result, store)
104
- } catch (err) {
105
- log.error('Error while running the AppSec WAF')
106
- log.error(err)
107
- } finally {
108
- wafContext.dispose()
109
- }
110
- }
111
-
112
- applyResult (result, store) {
113
- Reporter.reportMetrics({
114
- duration: result.totalRuntime / 1e3,
115
- durationExt: result.durationExt / 1e3,
116
- rulesVersion: this.ddwaf.rulesInfo.version
117
- }, store)
118
-
119
- if (result.data && result.data !== '[]') {
120
- Reporter.reportAttack(result.data, store)
121
- }
122
-
123
- return result.actions
124
- }
125
-
126
- updateRuleData (ruleData) {
127
- this.ddwaf.updateRuleData(ruleData)
128
- }
129
-
130
- clear () {
131
- this.ddwaf.dispose()
132
-
133
- this.wafContextCache = new WeakMap()
134
- }
135
- }
136
-
137
- module.exports = WAFCallback
@@ -1,7 +0,0 @@
1
- 'use strict'
2
-
3
- // lazy loading
4
- // TODO: cache the returned value
5
- module.exports = {
6
- get DDWAF () { return require('./ddwaf') }
7
- }
@@ -1,6 +0,0 @@
1
- 'use strict'
2
-
3
- // TODO: use datadog-core storage instead
4
- const { AsyncLocalStorage } = require('async_hooks')
5
-
6
- module.exports = new AsyncLocalStorage()
@@ -1,140 +0,0 @@
1
- 'use strict'
2
-
3
- const Runner = require('./runner')
4
-
5
- const MAX_CONTEXT_SIZE = 1024
6
-
7
- class SubscriptionManager {
8
- constructor () {
9
- this.addressToSubscriptions = new Map()
10
- this.addresses = new Set()
11
- this.subscriptions = new Set()
12
- }
13
-
14
- clear () {
15
- this.addressToSubscriptions = new Map()
16
- this.addresses = new Set()
17
- this.subscriptions = new Set()
18
- }
19
-
20
- addSubscription (subscription) {
21
- if (!subscription.addresses.length || this.subscriptions.has(subscription)) return
22
-
23
- for (let i = 0; i < subscription.addresses.length; ++i) {
24
- const address = subscription.addresses[i]
25
-
26
- this.addresses.add(address)
27
-
28
- const list = this.addressToSubscriptions.get(address)
29
-
30
- if (list === undefined) {
31
- this.addressToSubscriptions.set(address, [subscription])
32
- } else {
33
- list.push(subscription)
34
- }
35
- }
36
-
37
- this.subscriptions.add(subscription)
38
- }
39
-
40
- matchSubscriptions (newAddresses, allAddresses) {
41
- const addresses = new Set()
42
- const subscriptions = new Set()
43
- const knownSubscriptions = new Set()
44
-
45
- // TODO: possible optimization: collect matchedSubscriptions on the fly in Context#setValue
46
- newAddresses.forEach((newAddress) => {
47
- const matchedSubscriptions = this.addressToSubscriptions.get(newAddress)
48
-
49
- if (matchedSubscriptions === undefined) return
50
-
51
- for (let j = 0; j < matchedSubscriptions.length; ++j) {
52
- const subscription = matchedSubscriptions[j]
53
-
54
- if (knownSubscriptions.has(subscription) === true) continue
55
- knownSubscriptions.add(subscription)
56
-
57
- const isFulfilled = subscription.addresses.every(allAddresses.has, allAddresses)
58
-
59
- if (isFulfilled === true) {
60
- for (let k = 0; k < subscription.addresses.length; ++k) {
61
- addresses.add(subscription.addresses[k])
62
- }
63
-
64
- subscriptions.add(subscription)
65
- }
66
- }
67
- })
68
-
69
- return { addresses, subscriptions }
70
- }
71
-
72
- dispatch (newAddresses, allAddresses, context) {
73
- const matches = this.matchSubscriptions(newAddresses, allAddresses)
74
-
75
- // TODO: possible optimization
76
- // check if matches.subscriptions is empty here instead of in runner.js
77
-
78
- const params = {}
79
-
80
- matches.addresses.forEach((address) => {
81
- params[address] = context.resolve(address)
82
- })
83
-
84
- return Runner.runSubscriptions(matches.subscriptions, params)
85
- }
86
- }
87
-
88
- class Context {
89
- static setManager (manager) {
90
- this.manager = manager
91
- }
92
-
93
- constructor () {
94
- // TODO: this probably don't need to be a Map()
95
- this.store = new Map()
96
- this.allAddresses = new Set()
97
- this.newAddresses = new Set()
98
- }
99
-
100
- clear () {
101
- this.store = new Map()
102
- this.allAddresses = new Set()
103
- this.newAddresses = new Set()
104
- }
105
-
106
- setValue (address, value) {
107
- if (this.allAddresses.size >= MAX_CONTEXT_SIZE) return this
108
-
109
- // cannot optimize for objects because they're pointers
110
- if (typeof value !== 'object') {
111
- const oldValue = this.store.get(address)
112
- if (oldValue === value) return this
113
- }
114
-
115
- this.store.set(address, value)
116
- this.allAddresses.add(address)
117
- this.newAddresses.add(address)
118
-
119
- return this
120
- }
121
-
122
- dispatch () {
123
- if (this.newAddresses.size === 0) return []
124
-
125
- const result = Context.manager.dispatch(this.newAddresses, this.allAddresses, this)
126
-
127
- this.newAddresses.clear()
128
-
129
- return result
130
- }
131
-
132
- resolve (address) {
133
- return this.store.get(address)
134
- }
135
- }
136
-
137
- module.exports = {
138
- SubscriptionManager,
139
- Context
140
- }
@@ -1,51 +0,0 @@
1
- 'use strict'
2
-
3
- const { SubscriptionManager, Context } = require('./engine')
4
- const als = require('../als')
5
-
6
- const manager = new SubscriptionManager()
7
- Context.setManager(manager)
8
-
9
- function startContext () {
10
- const store = new Map()
11
-
12
- store.set('context', new Context())
13
-
14
- als.enterWith(store)
15
-
16
- return store
17
- }
18
-
19
- function getContext () {
20
- const store = als.getStore()
21
-
22
- return store && store.get('context')
23
- }
24
-
25
- function needsAddress (address) {
26
- return manager.addresses.has(address)
27
- }
28
-
29
- function propagate (data, context = getContext()) {
30
- if (!context) return
31
-
32
- const keys = Object.keys(data)
33
-
34
- for (let i = 0; i < keys.length; ++i) {
35
- const key = keys[i]
36
-
37
- if (needsAddress(key)) {
38
- context.setValue(key, data[key])
39
- }
40
- }
41
-
42
- return context.dispatch()
43
- }
44
-
45
- module.exports = {
46
- manager,
47
- startContext,
48
- getContext,
49
- needsAddress,
50
- propagate
51
- }