@camstack/server 0.1.7 → 0.2.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 (135) hide show
  1. package/package.json +11 -9
  2. package/src/__tests__/addon-install-e2e.test.ts +0 -1
  3. package/src/__tests__/addon-pages-e2e.test.ts +40 -18
  4. package/src/__tests__/addon-settings-router.spec.ts +6 -1
  5. package/src/__tests__/addon-upload.spec.ts +91 -29
  6. package/src/__tests__/agent-registry.spec.ts +26 -9
  7. package/src/__tests__/agent-status-page.spec.ts +1 -3
  8. package/src/__tests__/auth-session-cookie.test.ts +28 -1
  9. package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
  10. package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
  11. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +206 -0
  12. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
  13. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
  14. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +292 -0
  15. package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
  16. package/src/__tests__/cap-route-adapter.spec.ts +28 -15
  17. package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
  18. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
  19. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +177 -0
  20. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
  21. package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
  22. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +137 -0
  23. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
  24. package/src/__tests__/cap-routers/harness.ts +11 -7
  25. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
  26. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
  27. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
  28. package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
  29. package/src/__tests__/capability-e2e.test.ts +9 -11
  30. package/src/__tests__/cli-e2e.test.ts +80 -59
  31. package/src/__tests__/core-cap-bridge.spec.ts +3 -1
  32. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
  33. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
  34. package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
  35. package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
  36. package/src/__tests__/framework-allowlist.spec.ts +5 -4
  37. package/src/__tests__/https-e2e.test.ts +12 -6
  38. package/src/__tests__/lifecycle-e2e.test.ts +60 -11
  39. package/src/__tests__/live-events-subscription.spec.ts +17 -18
  40. package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
  41. package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
  42. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +265 -5
  43. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
  44. package/src/__tests__/native-cap-route.spec.ts +42 -19
  45. package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
  46. package/src/__tests__/singleton-contention.test.ts +23 -11
  47. package/src/__tests__/streaming-diagnostic.test.ts +156 -53
  48. package/src/__tests__/streaming-scale.test.ts +69 -35
  49. package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
  50. package/src/agent-status-page.ts +4 -3
  51. package/src/api/__tests__/addons-custom.spec.ts +22 -8
  52. package/src/api/__tests__/capabilities.router.test.ts +18 -9
  53. package/src/api/addon-upload.ts +46 -15
  54. package/src/api/addons-custom.router.ts +7 -6
  55. package/src/api/auth-whoami.ts +3 -1
  56. package/src/api/bridge-addons.router.ts +3 -1
  57. package/src/api/capabilities.router.ts +117 -78
  58. package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
  59. package/src/api/core/__tests__/integration-markers.spec.ts +10 -0
  60. package/src/api/core/addon-settings.router.ts +4 -1
  61. package/src/api/core/agents.router.ts +52 -53
  62. package/src/api/core/auth.router.ts +55 -36
  63. package/src/api/core/bulk-update-coordinator.ts +25 -22
  64. package/src/api/core/cap-providers.ts +459 -166
  65. package/src/api/core/capabilities.router.ts +30 -23
  66. package/src/api/core/hwaccel.router.ts +37 -10
  67. package/src/api/core/live-events.router.ts +16 -9
  68. package/src/api/core/logs.router.ts +58 -25
  69. package/src/api/core/notifications.router.ts +2 -1
  70. package/src/api/core/repl.router.ts +1 -3
  71. package/src/api/core/settings-backend.router.ts +68 -70
  72. package/src/api/core/system-events.router.ts +41 -32
  73. package/src/api/health/health.routes.ts +7 -13
  74. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
  75. package/src/api/oauth2/consent-page.ts +4 -3
  76. package/src/api/oauth2/oauth2-routes.ts +41 -12
  77. package/src/api/trpc/__tests__/client-ip.spec.ts +27 -1
  78. package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
  79. package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
  80. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +136 -0
  81. package/src/api/trpc/cap-mount-helpers.ts +64 -44
  82. package/src/api/trpc/cap-route-error-formatter.ts +17 -9
  83. package/src/api/trpc/client-ip.ts +17 -0
  84. package/src/api/trpc/core-cap-bridge.ts +3 -1
  85. package/src/api/trpc/generated-cap-mounts.ts +801 -286
  86. package/src/api/trpc/generated-cap-routers.ts +5723 -719
  87. package/src/api/trpc/scope-access.ts +7 -7
  88. package/src/api/trpc/trpc.context.ts +7 -4
  89. package/src/api/trpc/trpc.middleware.ts +4 -2
  90. package/src/api/trpc/trpc.router.ts +117 -48
  91. package/src/auth/session-cookie.ts +10 -0
  92. package/src/boot/__tests__/integration-id-backfill.spec.ts +131 -0
  93. package/src/boot/boot-config.ts +103 -122
  94. package/src/boot/integration-id-backfill.ts +109 -0
  95. package/src/boot/post-boot.service.ts +5 -3
  96. package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
  97. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +62 -0
  98. package/src/core/addon/addon-call-gateway.ts +20 -6
  99. package/src/core/addon/addon-package.service.ts +183 -89
  100. package/src/core/addon/addon-registry.service.ts +1212 -1267
  101. package/src/core/addon/addon-row-manifest.ts +29 -0
  102. package/src/core/addon/addon-search.service.ts +2 -1
  103. package/src/core/addon/addon-settings-provider.ts +27 -7
  104. package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
  105. package/src/core/addon-pages/addon-pages.service.ts +3 -1
  106. package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
  107. package/src/core/agent/agent-registry.service.ts +60 -38
  108. package/src/core/auth/auth.service.spec.ts +6 -8
  109. package/src/core/config/config.service.spec.ts +1 -1
  110. package/src/core/events/event-bus.service.spec.ts +44 -21
  111. package/src/core/events/event-bus.service.ts +5 -1
  112. package/src/core/feature/feature.service.spec.ts +4 -1
  113. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
  114. package/src/core/logging/logging.service.spec.ts +61 -21
  115. package/src/core/logging/logging.service.ts +19 -5
  116. package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
  117. package/src/core/moleculer/cap-call-fn.ts +5 -1
  118. package/src/core/moleculer/cap-route-authority.ts +18 -6
  119. package/src/core/moleculer/moleculer.service.ts +145 -29
  120. package/src/core/network/network-quality.service.spec.ts +7 -1
  121. package/src/core/notification/notification-wrapper.service.ts +1 -3
  122. package/src/core/notification/toast-wrapper.service.ts +1 -5
  123. package/src/core/repl/repl-engine.service.spec.ts +66 -39
  124. package/src/core/repl/repl-engine.service.ts +11 -12
  125. package/src/core/storage/storage-location-manager.spec.ts +12 -3
  126. package/src/core/streaming/stream-probe.service.ts +22 -13
  127. package/src/core/topology/topology-emitter.service.ts +5 -1
  128. package/src/launcher.ts +14 -9
  129. package/src/main.ts +658 -495
  130. package/src/manual-boot.ts +133 -154
  131. package/tsconfig.json +20 -8
  132. package/src/core/storage/settings-store.spec.ts +0 -213
  133. package/src/core/storage/settings-store.ts +0 -2
  134. package/src/core/storage/sql-schema.spec.ts +0 -140
  135. package/src/core/storage/sql-schema.ts +0 -3
@@ -43,23 +43,22 @@ const AuthProviderSummarySchema = z.object({
43
43
  })
44
44
 
45
45
  /** Wire shape of the authenticated user returned by `auth.me`. */
46
- const MeSchema = z.object({
47
- id: z.string(),
48
- username: z.string(),
49
- isAdmin: z.boolean(),
50
- permissions: z.object({
46
+ const MeSchema = z
47
+ .object({
48
+ id: z.string(),
49
+ username: z.string(),
51
50
  isAdmin: z.boolean(),
52
- allowedProviders: z.union([z.literal('*'), z.array(z.string())]),
53
- allowedDevices: z.record(z.string(), z.unknown()),
54
- }),
55
- isApiKey: z.boolean(),
56
- agentId: z.string().optional(),
57
- }).nullable()
51
+ permissions: z.object({
52
+ isAdmin: z.boolean(),
53
+ allowedProviders: z.union([z.literal('*'), z.array(z.string())]),
54
+ allowedDevices: z.record(z.string(), z.unknown()),
55
+ }),
56
+ isApiKey: z.boolean(),
57
+ agentId: z.string().optional(),
58
+ })
59
+ .nullable()
58
60
 
59
- export function createAuthRouter(
60
- auth: AuthService,
61
- registry: CapabilityRegistry | null,
62
- ) {
61
+ export function createAuthRouter(auth: AuthService, registry: CapabilityRegistry | null) {
63
62
  return trpcRouter({
64
63
  login: publicProcedure
65
64
  .input(z.object({ username: z.string(), password: z.string() }))
@@ -73,13 +72,16 @@ export function createAuthRouter(
73
72
  // common cause is a settings-store regression that breaks
74
73
  // the `users` collection query path.
75
74
  throw new Error(
76
- 'Login unavailable — `user-management` capability not registered. '
77
- + 'The `local-auth` addon failed to initialize; check server logs '
78
- + 'for an error tagged `[hub/local-auth]`.',
75
+ 'Login unavailable — `user-management` capability not registered. ' +
76
+ 'The `local-auth` addon failed to initialize; check server logs ' +
77
+ 'for an error tagged `[hub/local-auth]`.',
79
78
  )
80
79
  }
81
80
 
82
- const user = await userMgmt.validateCredentials({ username: input.username, password: input.password })
81
+ const user = await userMgmt.validateCredentials({
82
+ username: input.username,
83
+ password: input.password,
84
+ })
83
85
  if (!user) throw new Error('Invalid credentials')
84
86
 
85
87
  // ── TOTP gate ────────────────────────────────────────────────
@@ -91,9 +93,10 @@ export function createAuthRouter(
91
93
  // `kind: 'totp-challenge'` so it can't be replayed against
92
94
  // protected endpoints (the auth middleware rejects anything
93
95
  // without the standard session shape).
94
- const totpStatus = typeof userMgmt.getTotpStatus === 'function'
95
- ? await userMgmt.getTotpStatus({ userId: user.id })
96
- : { enabled: false }
96
+ const totpStatus =
97
+ typeof userMgmt.getTotpStatus === 'function'
98
+ ? await userMgmt.getTotpStatus({ userId: user.id })
99
+ : { enabled: false }
97
100
  if (totpStatus.enabled) {
98
101
  const challengeToken = auth.signTotpChallengeToken({
99
102
  userId: user.id,
@@ -154,18 +157,20 @@ export function createAuthRouter(
154
157
  if (!userMgmt) {
155
158
  throw new Error('Login unavailable — `user-management` capability not registered')
156
159
  }
157
- const verify = typeof userMgmt.verifyTotp === 'function'
158
- ? await userMgmt.verifyTotp({ userId: claims.userId, code: input.code.trim() })
159
- : { valid: false }
160
+ const verify =
161
+ typeof userMgmt.verifyTotp === 'function'
162
+ ? await userMgmt.verifyTotp({ userId: claims.userId, code: input.code.trim() })
163
+ : { valid: false }
160
164
  if (!verify.valid) {
161
165
  throw new Error('Invalid TOTP code')
162
166
  }
163
167
  // Re-fetch the user record so scopes / allowedDevices reflect
164
168
  // any admin change made between the two legs. Without this we
165
169
  // could mint a stale-scope session.
166
- const fresh = typeof userMgmt.listUsers === 'function'
167
- ? (await userMgmt.listUsers()).find((u) => u.id === claims.userId)
168
- : null
170
+ const fresh =
171
+ typeof userMgmt.listUsers === 'function'
172
+ ? (await userMgmt.listUsers()).find((u) => u.id === claims.userId)
173
+ : null
169
174
  if (!fresh) {
170
175
  throw new Error('User no longer exists')
171
176
  }
@@ -202,15 +207,20 @@ export function createAuthRouter(
202
207
  // exists only to block third-party tampering — operators acting on
203
208
  // themselves bypass it intentionally.
204
209
  changeOwnPassword: protectedProcedure
205
- .input(z.object({
206
- currentPassword: z.string().min(1),
207
- newPassword: z.string().min(8),
208
- }))
210
+ .input(
211
+ z.object({
212
+ currentPassword: z.string().min(1),
213
+ newPassword: z.string().min(8),
214
+ }),
215
+ )
209
216
  .output(z.object({ success: z.literal(true) }))
210
217
  .mutation(async ({ input, ctx }) => {
211
218
  const userMgmt = registry?.getSingleton('user-management') as
212
219
  | {
213
- validateCredentials: (i: { username: string; password: string }) => Promise<{ id: string } | null>
220
+ validateCredentials: (i: {
221
+ username: string
222
+ password: string
223
+ }) => Promise<{ id: string } | null>
214
224
  resetPassword: (i: { id: string; newPassword: string }) => Promise<{ success: true }>
215
225
  }
216
226
  | null
@@ -220,7 +230,10 @@ export function createAuthRouter(
220
230
  // Re-validate the current password against the live store to
221
231
  // confirm session identity → password ownership match. Stops a
222
232
  // stolen session-token from rotating credentials silently.
223
- const ok = await userMgmt.validateCredentials({ username: ctx.user.username, password: input.currentPassword })
233
+ const ok = await userMgmt.validateCredentials({
234
+ username: ctx.user.username,
235
+ password: input.currentPassword,
236
+ })
224
237
  if (!ok) throw new Error('Current password is incorrect')
225
238
  await userMgmt.resetPassword({ id: ctx.user.id, newPassword: input.newPassword })
226
239
  return { success: true as const }
@@ -231,7 +244,9 @@ export function createAuthRouter(
231
244
  .output(z.object({ secret: z.string(), otpauthUrl: z.string() }))
232
245
  .mutation(async ({ ctx }) => {
233
246
  const userMgmt = registry?.getSingleton('user-management') as
234
- | { setupTotp: (i: { userId: string }) => Promise<{ secret: string; otpauthUrl: string }> }
247
+ | {
248
+ setupTotp: (i: { userId: string }) => Promise<{ secret: string; otpauthUrl: string }>
249
+ }
235
250
  | null
236
251
  | undefined
237
252
  if (!userMgmt) throw new Error('user-management capability not available')
@@ -270,7 +285,11 @@ export function createAuthRouter(
270
285
  .output(z.object({ enabled: z.boolean(), confirmedAt: z.number().nullable() }))
271
286
  .query(async ({ ctx }) => {
272
287
  const userMgmt = registry?.getSingleton('user-management') as
273
- | { getTotpStatus: (i: { userId: string }) => Promise<{ enabled: boolean; confirmedAt: number | null }> }
288
+ | {
289
+ getTotpStatus: (i: {
290
+ userId: string
291
+ }) => Promise<{ enabled: boolean; confirmedAt: number | null }>
292
+ }
274
293
  | null
275
294
  | undefined
276
295
  if (!userMgmt || !ctx.user) return { enabled: false, confirmedAt: null }
@@ -29,7 +29,11 @@ export interface IBulkUpdateLogger {
29
29
  export interface BulkUpdateCoordinatorDeps {
30
30
  readonly eventBus: IBulkUpdateEventBus
31
31
  readonly updateAddon: (input: { name: string; version: string }) => Promise<void>
32
- readonly updateFrameworkPackage: (input: { packageName: string; version: string; deferRestart: boolean }) => Promise<void>
32
+ readonly updateFrameworkPackage: (input: {
33
+ packageName: string
34
+ version: string
35
+ deferRestart: boolean
36
+ }) => Promise<void>
33
37
  readonly restartServer: (input: { confirm: true }) => Promise<void>
34
38
  readonly logger: IBulkUpdateLogger
35
39
  /** Injectable clock for tests. Default: `() => Date.now()`. */
@@ -77,7 +81,7 @@ export class BulkUpdateCoordinator {
77
81
 
78
82
  const id = randomUUID()
79
83
 
80
- const items: BulkUpdateItem[] = input.items.map(i => ({
84
+ const items: BulkUpdateItem[] = input.items.map((i) => ({
81
85
  name: i.name,
82
86
  isSystem: i.isSystem,
83
87
  // fromVersion: the cap interface receives name+version+isSystem only;
@@ -110,7 +114,7 @@ export class BulkUpdateCoordinator {
110
114
  // Emit initial state so clients see the bulk as started immediately
111
115
  this.emit(state)
112
116
 
113
- void this.runLoop(id, cancelFlag).catch(err => {
117
+ void this.runLoop(id, cancelFlag).catch((err) => {
114
118
  this.deps.logger.error('BulkUpdateCoordinator: loop crashed unexpectedly', err)
115
119
  })
116
120
 
@@ -133,9 +137,9 @@ export class BulkUpdateCoordinator {
133
137
 
134
138
  list(nodeId?: string): readonly BulkUpdateState[] {
135
139
  const all = [...this.states.keys()]
136
- .map(id => this.get(id)) // get() applies lazy-cleanup
140
+ .map((id) => this.get(id)) // get() applies lazy-cleanup
137
141
  .filter((s): s is BulkUpdateState => s !== null)
138
- return nodeId === undefined ? all : all.filter(s => s.nodeId === nodeId)
142
+ return nodeId === undefined ? all : all.filter((s) => s.nodeId === nodeId)
139
143
  }
140
144
 
141
145
  cancel(id: string): { cancelled: boolean } {
@@ -148,7 +152,7 @@ export class BulkUpdateCoordinator {
148
152
  if (state.completedAtMs !== undefined) return { cancelled: false }
149
153
 
150
154
  flag.cancelled = true
151
- this.mutate(id, s => ({ ...s, cancelled: true }))
155
+ this.mutate(id, (s) => ({ ...s, cancelled: true }))
152
156
  return { cancelled: true }
153
157
  }
154
158
 
@@ -160,24 +164,24 @@ export class BulkUpdateCoordinator {
160
164
  // ── Phase 1: regular addons ──────────────────────────────────────
161
165
  this.transitionPhase(id, 'regular')
162
166
 
163
- for (const item of initial.items.filter(i => !i.isSystem)) {
167
+ for (const item of initial.items.filter((i) => !i.isSystem)) {
164
168
  if (cancelFlag.cancelled) break
165
169
  await this.processItem(id, item, false)
166
170
  }
167
171
 
168
172
  // ── Phase 2: system packages (deferRestart: true) ────────────────
169
- if (!cancelFlag.cancelled && initial.items.some(i => i.isSystem)) {
173
+ if (!cancelFlag.cancelled && initial.items.some((i) => i.isSystem)) {
170
174
  this.transitionPhase(id, 'system')
171
175
 
172
- for (const item of initial.items.filter(i => i.isSystem)) {
176
+ for (const item of initial.items.filter((i) => i.isSystem)) {
173
177
  if (cancelFlag.cancelled) break
174
178
  await this.processItem(id, item, true)
175
179
  }
176
180
 
177
181
  // ── Phase 3: single restart ──────────────────────────────────
178
- const anySystemPendingRestart = this.states.get(id)!.items.some(
179
- i => i.isSystem && i.status === 'done-pending-restart',
180
- )
182
+ const anySystemPendingRestart = this.states
183
+ .get(id)!
184
+ .items.some((i) => i.isSystem && i.status === 'done-pending-restart')
181
185
 
182
186
  if (anySystemPendingRestart && !cancelFlag.cancelled) {
183
187
  this.transitionPhase(id, 'restarting')
@@ -214,7 +218,7 @@ export class BulkUpdateCoordinator {
214
218
 
215
219
  private async processItem(id: string, item: BulkUpdateItem, isSystem: boolean): Promise<void> {
216
220
  this.setItemStatus(id, item.name, 'updating', { startedAtMs: this.now() })
217
- this.mutate(id, s => ({ ...s, current: item.name }))
221
+ this.mutate(id, (s) => ({ ...s, current: item.name }))
218
222
  this.emit(this.states.get(id)!)
219
223
 
220
224
  try {
@@ -234,7 +238,7 @@ export class BulkUpdateCoordinator {
234
238
  this.setItemStatus(id, item.name, 'failed', { error: msg, completedAtMs: this.now() })
235
239
  }
236
240
 
237
- this.mutate(id, s => ({ ...s, current: null }))
241
+ this.mutate(id, (s) => ({ ...s, current: null }))
238
242
  this.emit(this.states.get(id)!)
239
243
  }
240
244
 
@@ -246,26 +250,25 @@ export class BulkUpdateCoordinator {
246
250
  status: BulkUpdateItemStatus,
247
251
  fields: { error?: string; startedAtMs?: number; completedAtMs?: number } = {},
248
252
  ): void {
249
- this.mutate(id, s => {
250
- const items = s.items.map(it =>
251
- it.name === name ? { ...it, status, ...fields } : it,
252
- )
253
+ this.mutate(id, (s) => {
254
+ const items = s.items.map((it) => (it.name === name ? { ...it, status, ...fields } : it))
253
255
  // completed = all terminal states: done | done-pending-restart | failed
254
256
  const completed = items.filter(
255
- it => it.status === 'done' || it.status === 'done-pending-restart' || it.status === 'failed',
257
+ (it) =>
258
+ it.status === 'done' || it.status === 'done-pending-restart' || it.status === 'failed',
256
259
  ).length
257
- const failed = items.filter(it => it.status === 'failed').length
260
+ const failed = items.filter((it) => it.status === 'failed').length
258
261
  return { ...s, items, completed, failed }
259
262
  })
260
263
  }
261
264
 
262
265
  private transitionPhase(id: string, phase: BulkUpdatePhase): void {
263
- this.mutate(id, s => ({ ...s, phase }))
266
+ this.mutate(id, (s) => ({ ...s, phase }))
264
267
  this.emit(this.states.get(id)!)
265
268
  }
266
269
 
267
270
  private completeBulk(id: string): void {
268
- this.mutate(id, s => ({ ...s, completedAtMs: this.now(), current: null }))
271
+ this.mutate(id, (s) => ({ ...s, completedAtMs: this.now(), current: null }))
269
272
  this.emit(this.states.get(id)!)
270
273
 
271
274
  // Free the nodeId slot so a new bulk for the same node can be started