@toa.io/core 1.0.0-alpha.25 → 1.0.0-alpha.251

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/package.json +7 -9
  2. package/src/call.js +12 -0
  3. package/src/cascade.js +7 -5
  4. package/src/component.js +41 -17
  5. package/src/composition.js +1 -1
  6. package/src/connector.js +21 -14
  7. package/src/context.js +4 -0
  8. package/src/contract/contract.js +22 -0
  9. package/src/contract/reply.js +26 -9
  10. package/src/contract/request.js +15 -5
  11. package/src/contract/schemas/query.yaml +14 -1
  12. package/src/discovery.js +13 -7
  13. package/src/effect.js +19 -0
  14. package/src/entities/changeset.js +5 -8
  15. package/src/entities/entity.js +48 -23
  16. package/src/entities/factory.js +15 -4
  17. package/src/entities/newid.js +11 -0
  18. package/src/entities/set.js +13 -0
  19. package/src/event.js +19 -1
  20. package/src/exceptions.js +26 -19
  21. package/src/exposition.js +3 -2
  22. package/src/guard.js +17 -0
  23. package/src/index.js +6 -0
  24. package/src/observation.js +1 -9
  25. package/src/operation.js +28 -7
  26. package/src/query/options.js +3 -2
  27. package/src/query.js +3 -1
  28. package/src/receiver.js +60 -11
  29. package/src/remote.js +7 -7
  30. package/src/state.js +69 -33
  31. package/src/transition.js +8 -19
  32. package/src/transmission.js +12 -3
  33. package/src/unmanaged.js +11 -0
  34. package/test/component.test.js +4 -3
  35. package/test/connector.fixtures.js +8 -0
  36. package/test/connector.test.js +20 -0
  37. package/test/contract/conditions.test.js +5 -5
  38. package/test/contract/request.test.js +7 -7
  39. package/test/discovery.test.js +56 -0
  40. package/test/entities/entity.test.js +0 -45
  41. package/test/entities/factory.test.js +3 -3
  42. package/test/event.test.js +4 -4
  43. package/test/receiver.fixtures.js +1 -0
  44. package/test/state.test.js +0 -14
  45. package/types/bindings.d.ts +7 -5
  46. package/types/bridges.ts +3 -0
  47. package/types/component.d.ts +4 -1
  48. package/types/extensions.d.ts +6 -3
  49. package/types/index.ts +1 -0
  50. package/types/message.d.ts +1 -0
  51. package/types/operations.d.ts +6 -0
  52. package/types/query.d.ts +2 -0
  53. package/types/remote.d.ts +18 -0
  54. package/types/request.d.ts +5 -0
  55. package/types/storages.d.ts +11 -9
  56. package/src/contract/conditions.js +0 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/core",
3
- "version": "1.0.0-alpha.25",
3
+ "version": "1.0.0-alpha.251",
4
4
  "description": "Toa Core",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -21,13 +21,11 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@rsql/parser": "1.2.4",
24
- "@toa.io/console": "1.0.0-alpha.25",
25
- "@toa.io/generic": "1.0.0-alpha.25",
26
- "@toa.io/yaml": "1.0.0-alpha.25",
27
- "error-value": "0.3.0"
24
+ "@toa.io/generic": "1.0.0-alpha.225",
25
+ "@toa.io/yaml": "1.0.0-alpha.246",
26
+ "error-value": "0.3.0",
27
+ "openspan": "1.0.0-alpha.251",
28
+ "uuid": "11.1.1"
28
29
  },
29
- "devDependencies": {
30
- "clone-deep": "4.0.1"
31
- },
32
- "gitHead": "756ea259f35dda03d45047f8153d441191026877"
30
+ "gitHead": "c3857e14dde3a910fe28d41a5a97a960c15beba3"
33
31
  }
package/src/call.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { Readable } = require('node:stream')
4
+ const { current, encode } = require('openspan')
4
5
  const { Connector } = require('./connector')
5
6
  const { Err } = require('error-value')
6
7
 
@@ -18,11 +19,18 @@ class Call extends Connector {
18
19
  }
19
20
 
20
21
  async invoke (request = {}) {
22
+ request.input ??= null
23
+
21
24
  this.#contract.fit(request)
22
25
 
23
26
  // avoid validation on the recipient's side
24
27
  request.authentic = true
25
28
 
29
+ const context = current()
30
+
31
+ if (context !== undefined)
32
+ request.telemetry = encode(context)
33
+
26
34
  const reply = await this.#transmitter.request(request)
27
35
 
28
36
  if (reply === null) return null
@@ -37,6 +45,10 @@ class Call extends Connector {
37
45
  return reply.output
38
46
  }
39
47
  }
48
+
49
+ explain () {
50
+ return this.#contract.discovery
51
+ }
40
52
  }
41
53
 
42
54
  exports.Call = Call
package/src/cascade.js CHANGED
@@ -1,19 +1,21 @@
1
1
  'use strict'
2
2
 
3
- const { merge } = require('@toa.io/generic')
4
3
  const { Connector } = require('./connector')
5
4
 
6
5
  class Cascade extends Connector {
7
- #bridges
6
+ // #bridges
8
7
  #last
9
8
 
10
- constructor (bridges) {
9
+ constructor (bridges, rc) {
11
10
  super()
12
11
 
13
- this.#bridges = bridges
12
+ // this.#bridges = bridges
14
13
  this.#last = bridges[bridges.length - 1]
15
14
 
16
- this.depends(bridges)
15
+ if (rc === undefined)
16
+ this.depends(bridges)
17
+ else
18
+ this.depends(bridges).depends(rc)
17
19
  }
18
20
 
19
21
  async run (...args) {
package/src/component.js CHANGED
@@ -1,40 +1,64 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
3
+ const assert = require('node:assert')
4
+ const { console, current, decode, run } = require('openspan')
4
5
  const { Connector } = require('./connector')
5
- const { NotImplementedException } = require('./exceptions')
6
6
 
7
- /**
8
- * @implements {toa.core.Component}
9
- */
10
7
  class Component extends Connector {
11
8
  locator
12
9
 
13
- #operations
10
+ /** @protected */
11
+ operations
12
+
13
+ /** @protected */
14
+ kind = 'server'
14
15
 
15
16
  constructor (locator, operations) {
16
17
  super()
17
18
 
18
19
  this.locator = locator
19
- this.#operations = operations
20
+ this.operations = operations
20
21
 
21
22
  Object.values(operations).forEach((operation) => this.depends(operation))
22
23
  }
23
24
 
24
- async open () {
25
- console.info(`Runtime '${this.locator.id}' connected`)
26
- }
25
+ async invoke (endpoint, request) {
26
+ assert.ok(endpoint in this.operations,
27
+ `Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
27
28
 
28
- async dispose () {
29
- console.info(`Runtime '${this.locator.id}' disconnected`)
29
+ // if the request carries no telemetry, the trace starts here
30
+ const remote = request?.telemetry === undefined ? null : decode(request.telemetry)
31
+ const task = () => this.process(endpoint, request)
32
+
33
+ if (remote === null)
34
+ return task()
35
+ else
36
+ return run(remote, task)
30
37
  }
31
38
 
32
- async invoke (endpoint, request) {
33
- if (!(endpoint in this.#operations)) {
34
- throw new NotImplementedException(`Endpoint '${endpoint}' not found in '${this.locator.id}'`)
35
- }
39
+ /** @private */
40
+ async process (endpoint, request) {
41
+ const options = { name: `${this.locator.id}.${endpoint}`, kind: this.kind }
42
+
43
+ // the server span is emitted by the component itself,
44
+ // while the client span belongs to the calling service and inherits it from the context
45
+ if (this.kind === 'server')
46
+ options.service = this.locator.id
47
+
48
+ return console.span(options, async () => {
49
+ const reply = await this.operations[endpoint].invoke(request)
50
+
51
+ if (reply?.exception !== undefined) {
52
+ current().status = 'error'
53
+
54
+ console.error('Failed to execute operation', {
55
+ endpoint: `${this.locator.id}.${endpoint}`,
56
+ exception: reply.exception
57
+ })
58
+ }
36
59
 
37
- return this.#operations[endpoint].invoke(request)
60
+ return reply
61
+ })
38
62
  }
39
63
  }
40
64
 
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
3
+ const { console } = require('openspan')
4
4
  const { Connector } = require('./connector')
5
5
 
6
6
  class Composition extends Connector {
package/src/connector.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
4
- const { newid } = require('@toa.io/generic')
3
+ const { console } = require('openspan')
5
4
 
6
5
  /**
7
6
  * Abstract connections hierarchy
@@ -26,7 +25,7 @@ class Connector {
26
25
  connected = false
27
26
 
28
27
  constructor () {
29
- this.id = this.constructor.name + '#' + newid().substring(0, 8)
28
+ this.id = this.constructor.name + '#' + Math.random().toString(36).substring(2, 8)
30
29
  }
31
30
 
32
31
  /**
@@ -86,12 +85,18 @@ class Connector {
86
85
 
87
86
  this.#disconnecting = undefined
88
87
 
89
- console.debug(`Connecting '${this.id}' with ${this.#dependencies.length} dependencies...`)
90
-
91
- this.#connecting = (async () => {
88
+ const work = async () => {
92
89
  await Promise.all(this.#dependencies.map((connector) => connector.connect()))
93
90
  await this.open()
94
- })()
91
+ }
92
+
93
+ // anonymous grouping nodes are not worth a span
94
+ this.#connecting = TRACE_BOOT && this.constructor.name !== 'Connector'
95
+ ? console.span({
96
+ name: `connect ${this.constructor.name}`,
97
+ attributes: { id: this.locator?.id ?? this.id }
98
+ }, work)
99
+ : work()
95
100
 
96
101
  try {
97
102
  await this.#connecting
@@ -100,8 +105,6 @@ class Connector {
100
105
  await this.disconnect(true)
101
106
  throw e
102
107
  }
103
-
104
- console.debug(`Connector '${this.id}' connected`)
105
108
  }
106
109
 
107
110
  /**
@@ -115,7 +118,11 @@ class Connector {
115
118
  * @returns {Promise<void>}
116
119
  */
117
120
  async disconnect (interrupt) {
118
- if (interrupt !== true) await this.#connecting
121
+ // a connector that has not finished connecting has nothing to close, and
122
+ // awaiting a connection that may never settle outlives any grace period
123
+ const pending = interrupt === true || this.connected === false
124
+
125
+ if (!pending) await this.#connecting
119
126
 
120
127
  if (this.#disconnecting) return this.#disconnecting
121
128
 
@@ -132,10 +139,11 @@ class Connector {
132
139
  const interval = setInterval(() => {
133
140
  const delay = +new Date() - start
134
141
 
135
- if (delay > DELAY) console.warn(`Connector ${this.id} still disconnecting (${delay})`)
142
+ if (delay > DELAY)
143
+ console.warn(`Connector ${this.id} still disconnecting (${delay})`)
136
144
  }, DELAY)
137
145
 
138
- if (interrupt !== true) await this.close()
146
+ if (!pending) await this.close()
139
147
 
140
148
  clearInterval(interval)
141
149
 
@@ -145,8 +153,6 @@ class Connector {
145
153
  })()
146
154
 
147
155
  await this.#disconnecting
148
-
149
- console.debug(`Connector '${this.id}' disconnected`)
150
156
  }
151
157
 
152
158
  async reconnect () {
@@ -179,5 +185,6 @@ class Connector {
179
185
  }
180
186
 
181
187
  const DELAY = 5000
188
+ const TRACE_BOOT = process.env.TOA_BOOT_TRACE === '1'
182
189
 
183
190
  exports.Connector = Connector
package/src/context.js CHANGED
@@ -6,6 +6,8 @@ const { Connector } = require('./connector')
6
6
  * @implements {toa.core.Context}
7
7
  */
8
8
  class Context extends Connector {
9
+ env
10
+ name
9
11
  aspects
10
12
 
11
13
  #local
@@ -15,6 +17,8 @@ class Context extends Connector {
15
17
  constructor (local, discover, aspects = []) {
16
18
  super()
17
19
 
20
+ this.env = process.env.TOA_ENV
21
+ this.name = process.env.TOA_CONTEXT
18
22
  this.aspects = aspects
19
23
 
20
24
  this.#local = local
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ const { SystemException } = require('../exceptions')
4
+
5
+ class Contract {
6
+ schema
7
+
8
+ constructor (schema) {
9
+ this.schema = schema
10
+ }
11
+
12
+ fit (value) {
13
+ const error = this.schema.fit(value)
14
+
15
+ if (error !== null)
16
+ throw new this.constructor.Exception(error, value)
17
+ }
18
+
19
+ static Exception = SystemException
20
+ }
21
+
22
+ exports.Contract = Contract
@@ -1,22 +1,39 @@
1
1
  'use strict'
2
2
 
3
3
  const schemas = require('./schemas')
4
- const { Conditions } = require('./conditions')
4
+ const { Contract } = require('./contract')
5
5
  const { ResponseContractException } = require('../exceptions')
6
6
 
7
- class Reply extends Conditions {
7
+ class Reply extends Contract {
8
8
  static Exception = ResponseContractException
9
9
 
10
- /**
11
- * @returns {toa.schema.JSON}
12
- */
13
- static schema (output, error) {
10
+ static schema (output, errors) {
14
11
  const schema = { type: 'object', properties: {}, additionalProperties: false }
15
12
 
16
- if (output !== undefined) schema.properties.output = output
13
+ if (output !== undefined) {
14
+ if (output.type === 'object')
15
+ output.additionalProperties = true
16
+ else if (output.type === 'array' && output.items?.type === 'object')
17
+ output.items.additionalProperties = true
17
18
 
18
- if (error !== undefined) schema.properties.error = error
19
- else schema.properties.error = schemas.error
19
+ schema.properties.output = output
20
+ }
21
+
22
+ if (errors !== undefined)
23
+ schema.properties.error = {
24
+ type: 'object',
25
+ properties: {
26
+ code: {
27
+ enum: errors
28
+ },
29
+ message: {
30
+ type: 'string'
31
+ }
32
+ },
33
+ required: ['code']
34
+ }
35
+ else
36
+ schema.properties.error = schemas.error
20
37
 
21
38
  return schema
22
39
  }
@@ -2,9 +2,20 @@
2
2
 
3
3
  const schemas = require('./schemas')
4
4
  const { RequestContractException } = require('../exceptions')
5
- const { Conditions } = require('./conditions')
5
+ const { Contract } = require('./contract')
6
+
7
+ class Request extends Contract {
8
+ /** @readonly */
9
+ discovery = {}
10
+
11
+ constructor (schema, definition) {
12
+ super(schema)
13
+
14
+ for (const key of ['input', 'output', 'errors'])
15
+ if (definition[key] !== undefined)
16
+ this.discovery[key] = definition[key]
17
+ }
6
18
 
7
- class Request extends Conditions {
8
19
  static Exception = RequestContractException
9
20
 
10
21
  /**
@@ -13,7 +24,7 @@ class Request extends Conditions {
13
24
  static schema (definition, entity) {
14
25
  const schema = {
15
26
  type: 'object',
16
- properties: { authentic: { type: 'boolean' } },
27
+ properties: { authentic: { type: 'boolean' }, task: { type: 'boolean' } },
17
28
  additionalProperties: true
18
29
  }
19
30
 
@@ -22,9 +33,8 @@ class Request extends Conditions {
22
33
  if (definition.input !== undefined) {
23
34
  schema.properties.input = definition.input
24
35
  required.push('input')
25
- } else {
36
+ } else
26
37
  schema.properties.input = { type: 'null' }
27
- }
28
38
 
29
39
  if (entity === undefined)
30
40
  definition.query = false
@@ -1,11 +1,22 @@
1
1
  type: object
2
2
  properties:
3
- id: { }
3
+ id:
4
+ type: string
5
+ ids:
6
+ type: array
7
+ uniqueItems: true
8
+ minItems: 1
9
+ items:
10
+ type: string
4
11
  version:
5
12
  type: integer
6
13
  minimum: 0
7
14
  criteria:
8
15
  type: string
16
+ search:
17
+ type: string
18
+ sample:
19
+ type: number
9
20
  omit:
10
21
  type: integer
11
22
  minimum: 0
@@ -27,4 +38,6 @@ properties:
27
38
  type: string
28
39
  not:
29
40
  const: id
41
+ deleted:
42
+ type: boolean
30
43
  additionalProperties: false
package/src/discovery.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
3
+ const { console } = require('openspan')
4
4
  const { Connector } = require('./connector')
5
5
 
6
6
  class Discovery extends Connector {
@@ -25,20 +25,26 @@ class Discovery extends Connector {
25
25
  this.depends(this.#lookups[id])
26
26
  }
27
27
 
28
- console.debug(`Sending lookup request to '${id}'`)
28
+ const since = Date.now()
29
29
 
30
- const warning = () => console.warn(`Waiting for lookup response from '${id}'...`)
31
- const timeout = setTimeout(warning, TIMEOUT)
30
+ // the wait is unbounded by design, as a dependency may still be starting,
31
+ // so repeating is the only way a component stuck on one stays visible
32
+ const warning = setInterval(() => {
33
+ const waiting = Math.round((Date.now() - since) / 1000)
34
+
35
+ console.warn('Waiting for lookup response', { component: id, waiting })
36
+ }, INTERVAL)
37
+
38
+ warning.unref()
32
39
 
33
40
  const output = await this.#lookups[id].invoke()
34
41
 
35
- console.debug(`Lookup response from '${id}' received`)
36
- clearTimeout(timeout)
42
+ clearInterval(warning)
37
43
 
38
44
  return output
39
45
  }
40
46
  }
41
47
 
42
- const TIMEOUT = 5000
48
+ const INTERVAL = 5000
43
49
 
44
50
  exports.Discovery = Discovery
package/src/effect.js ADDED
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const { Observation } = require('./observation')
4
+
5
+ class Effect extends Observation {
6
+
7
+ async acquire (store) {
8
+ const { query, entity, input } = store.request
9
+
10
+ if (entity === undefined)
11
+ return super.acquire(store)
12
+
13
+ store.scope = await this.scope.ensure(query, entity, input)
14
+ store.state = store.scope.get()
15
+ }
16
+
17
+ }
18
+
19
+ exports.Effect = Effect
@@ -1,10 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const {
4
- merge,
5
- overwrite,
6
- newid
7
- } = require('@toa.io/generic')
8
3
  const { EntityContractException } = require('../exceptions')
9
4
 
10
5
  class Changeset {
@@ -18,7 +13,7 @@ class Changeset {
18
13
  this.query = query
19
14
 
20
15
  this.#schema = schema
21
- this.#state = schema.system()
16
+ this.#state = {}
22
17
  }
23
18
 
24
19
  get () {
@@ -26,10 +21,12 @@ class Changeset {
26
21
  }
27
22
 
28
23
  set (value) {
29
- const error = this.#schema.adapt(value)
24
+ const error = this.#schema.match(value)
30
25
 
31
- if (error !== null) throw new EntityContractException(error)
26
+ if (error !== null)
27
+ throw new EntityContractException(error, value)
32
28
 
29
+ delete value._version
33
30
  value._updated = Date.now()
34
31
 
35
32
  this.#state = value
@@ -1,23 +1,23 @@
1
1
  'use strict'
2
2
 
3
- const {
4
- difference,
5
- newid
6
- } = require('@toa.io/generic')
7
-
8
- const { EntityContractException } = require('../exceptions')
3
+ const { difference } = require('@toa.io/generic')
4
+ const { EntityContractException, EntityGuardException } = require('../exceptions')
5
+ const { newid } = require('./newid')
9
6
 
10
7
  class Entity {
8
+ deleted = false
11
9
  #schema
10
+ #guards
12
11
  #origin = null
13
12
  #state
14
13
 
15
- constructor (schema, argument) {
14
+ constructor (schema, argument, guards) {
16
15
  this.#schema = schema
16
+ this.#guards = guards
17
17
 
18
18
  if (typeof argument === 'object') {
19
19
  const object = structuredClone(argument)
20
- this.set(object)
20
+ this.#set(object)
21
21
  this.#origin = argument
22
22
  } else {
23
23
  const id = argument === undefined ? newid() : argument
@@ -29,37 +29,62 @@ class Entity {
29
29
  return this.#state
30
30
  }
31
31
 
32
- set (value) {
33
- const error = this.#schema.fit(value)
32
+ set (value, optional = false) {
33
+ if (!optional)
34
+ this.#guard(value)
35
+
36
+ const error = optional ? this.#schema.fitOptional(value) : this.#schema.fit(value)
34
37
 
35
- if (error !== null) throw new EntityContractException(error)
38
+ if (error !== null)
39
+ throw new EntityContractException(error, value)
36
40
 
37
41
  this.#set(value)
38
42
  }
39
43
 
40
- event () {
44
+ event (input = undefined) {
41
45
  return {
42
46
  origin: this.#origin,
43
47
  state: this.#state,
44
- changeset: this.#origin === null ? this.#state : difference(this.#origin, this.#state)
48
+ changeset: this.#origin === null ? this.#state : difference(this.#origin, this.#state),
49
+ trailers: this.#state._trailers,
50
+ input
45
51
  }
46
52
  }
47
53
 
48
54
  #init (id) {
49
- const value = {
50
- ...this.#schema.defaults({ id }),
51
- _version: 0,
52
- _created: Date.now()
53
- }
55
+ const value = { id, _version: 0 }
54
56
 
55
- this.#set(value)
57
+ this.set(value, true)
58
+ }
59
+
60
+ #guard (value) {
61
+ if (this.#guards === undefined)
62
+ return
63
+
64
+ for (const guard of this.#guards) {
65
+ const ok = guard.fit(value, this.#origin)
66
+
67
+ if (ok === false)
68
+ throw new EntityGuardException(guard.name, value)
69
+ }
56
70
  }
57
71
 
58
72
  #set (value) {
59
- Object.defineProperty(value, 'id', {
60
- writable: false,
61
- configurable: false
62
- })
73
+ if (!('_trailers' in value))
74
+ Object.defineProperty(value, '_trailers', {
75
+ writable: false,
76
+ configurable: false,
77
+ enumerable: false,
78
+ value: {}
79
+ })
80
+
81
+ if (!('_created' in value)) {
82
+ value._created = Date.now()
83
+ value._updated ??= value._created
84
+ }
85
+
86
+ if ('_deleted' in value && value._deleted !== null)
87
+ this.deleted = true
63
88
 
64
89
  if (this.#state !== undefined) {
65
90
  value._updated = Date.now()
@@ -1,27 +1,38 @@
1
1
  'use strict'
2
2
 
3
+ const { newid } = require('./newid')
3
4
  const { Entity } = require('./entity')
4
5
  const { EntitySet } = require('./set')
5
6
  const { Changeset } = require('./changeset')
6
7
 
7
8
  class Factory {
8
9
  #schema
10
+ #guards
9
11
 
10
- constructor (schema) {
12
+ constructor (schema, guards) {
11
13
  this.#schema = schema
14
+ this.#guards = guards
15
+ }
16
+
17
+ fit (values) {
18
+ this.#schema.validate({ id: newid(), ...values }, 'Entity')
12
19
  }
13
20
 
14
21
  init (id) {
15
- return new Entity(this.#schema, id)
22
+ return new Entity(this.#schema, id, this.#guards)
16
23
  }
17
24
 
18
25
  object (record) {
19
- return new Entity(this.#schema, record)
26
+ return new Entity(this.#schema, record, this.#guards)
20
27
  }
21
28
 
22
- objects (recordset) {
29
+ objects (recordset, init) {
23
30
  const set = recordset.map((record) => this.object(record))
24
31
 
32
+ if (init !== undefined)
33
+ for (const id of init)
34
+ set.unshift(this.init(id))
35
+
25
36
  return new EntitySet(set)
26
37
  }
27
38