@qvac/sdk 0.19.0 → 0.20.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 (79) hide show
  1. package/NOTICE +111 -82
  2. package/dist/examples/abot-world.js +3 -3
  3. package/dist/examples/advisory-model-fit.d.ts +59 -0
  4. package/dist/examples/advisory-model-fit.js +156 -0
  5. package/dist/examples/asr/parakeet-nemotron-filesystem.d.ts +2 -0
  6. package/dist/examples/asr/parakeet-nemotron-filesystem.js +170 -0
  7. package/dist/examples/assess-model-fit.js +6 -1
  8. package/dist/examples/audiogen/edit-music.d.ts +2 -0
  9. package/dist/examples/audiogen/edit-music.js +121 -0
  10. package/dist/examples/audiogen/understand-music.d.ts +2 -0
  11. package/dist/examples/audiogen/understand-music.js +73 -0
  12. package/dist/examples/diffusion-ic-lora-ltx.js +3 -3
  13. package/dist/examples/diffusion-img2vid.js +1 -1
  14. package/dist/examples/diffusion-txt2vid-minimax-h3.d.ts +2 -0
  15. package/dist/examples/diffusion-txt2vid-minimax-h3.js +64 -0
  16. package/dist/examples/diffusion-txt2vid.js +1 -1
  17. package/dist/examples/kv-cache-cleanup.js +5 -2
  18. package/dist/examples/model-fit-ui/server.js +33 -7
  19. package/dist/examples/rag/rag-turbovec.d.ts +2 -0
  20. package/dist/examples/rag/rag-turbovec.js +58 -0
  21. package/dist/examples/tts/audio8.js +5 -5
  22. package/dist/examples/tts/chatterbox-enhanced.js +4 -5
  23. package/dist/examples/tts/chatterbox-multilingual.js +5 -5
  24. package/dist/examples/tts/chatterbox.js +5 -5
  25. package/dist/examples/tts/cosyvoice3.js +5 -5
  26. package/dist/examples/tts/llm-to-tts-streaming.js +12 -2
  27. package/dist/examples/tts/parler.js +5 -5
  28. package/dist/examples/tts/supertonic-enhanced.js +4 -5
  29. package/dist/examples/tts/supertonic-multilingual.js +5 -5
  30. package/dist/examples/tts/supertonic.js +5 -5
  31. package/dist/scripts/contract/build-contract.d.ts +1 -1
  32. package/dist/scripts/contract/constants-registry.d.ts +31 -0
  33. package/dist/scripts/contract/constants-registry.js +13 -2
  34. package/dist/scripts/contract/method-shapes.d.ts +3 -0
  35. package/dist/scripts/contract/method-shapes.js +3 -0
  36. package/dist/src/client/api/assess-model-fit.js +2 -0
  37. package/dist/src/client/api/audio-edit.d.ts +36 -0
  38. package/dist/src/client/api/audio-edit.js +38 -0
  39. package/dist/src/client/api/audio-gen-result.d.ts +5 -1
  40. package/dist/src/client/api/audio-gen-result.js +105 -52
  41. package/dist/src/client/api/audio-gen.d.ts +38 -1
  42. package/dist/src/client/api/audio-gen.js +38 -1
  43. package/dist/src/client/api/audio-understand.d.ts +25 -0
  44. package/dist/src/client/api/audio-understand.js +27 -0
  45. package/dist/src/client/api/delete-cache.d.ts +7 -1
  46. package/dist/src/client/api/delete-cache.js +11 -1
  47. package/dist/src/client/api/index.d.ts +3 -0
  48. package/dist/src/client/api/index.js +3 -0
  49. package/dist/src/client/api/text-to-speech.d.ts +31 -4
  50. package/dist/src/client/api/text-to-speech.js +118 -21
  51. package/dist/src/client/api/vector-index.d.ts +143 -0
  52. package/dist/src/client/api/vector-index.js +168 -0
  53. package/dist/src/client/api/world.d.ts +2 -2
  54. package/dist/src/commands/bundle/manifest.d.ts +2 -0
  55. package/dist/src/commands/verify/addon-source.d.ts +6 -0
  56. package/dist/src/commands/verify/bundle-source.d.ts +2 -0
  57. package/dist/src/commands/verify/bundle-source.js +155 -1
  58. package/dist/src/commands/verify/index.js +1 -0
  59. package/dist/src/commands/verify/prebuilds.d.ts +23 -0
  60. package/dist/src/commands/verify/prebuilds.js +129 -13
  61. package/dist/src/commands/verify/resource-collectors.js +2 -1
  62. package/dist/src/expo/plugins/patches/qvac-platform-addons.d.ts +23 -0
  63. package/dist/src/expo/plugins/patches/qvac-platform-addons.js +94 -0
  64. package/dist/src/expo/plugins/withMobileBundle.d.ts +12 -2
  65. package/dist/src/expo/plugins/withMobileBundle.js +62 -31
  66. package/dist/src/index.d.ts +3 -3
  67. package/dist/src/index.js +7 -3
  68. package/dist/src/schemas/sdk-errors-server.d.ts +4 -0
  69. package/dist/src/schemas/sdk-errors-server.js +22 -0
  70. package/dist/src/utils/aggregate-events.d.ts +3 -1
  71. package/dist/src/utils/aggregate-events.js +7 -1
  72. package/dist/src/utils/errors-server.d.ts +12 -0
  73. package/dist/src/utils/errors-server.js +21 -0
  74. package/docs/system-resources-support-matrix.md +19 -19
  75. package/package.json +22 -15
  76. package/src/electron-forge/index.cjs +86 -14
  77. package/src/expo/plugins/patches/android-link.mjs +18 -9
  78. package/src/expo/plugins/patches/ios-link.mjs +18 -9
  79. package/src/expo/plugins/patches/qvac-platform-addons.ts +124 -0
@@ -2,8 +2,8 @@
2
2
  * @qvac/sdk/electron-forge
3
3
  *
4
4
  * Electron Forge plugin: bundles the QVAC worker, verifies its native addons,
5
- * then configures Electron Packager to tree-shake unused @qvac/* addons and
6
- * non-target prebuilds.
5
+ * then configures Electron Packager to tree-shake unused @qvac/* addons (along
6
+ * with their per-platform prebuild packages) and non-target prebuilds.
7
7
  *
8
8
  * macOS universal (`arch: "universal"`) is not supported — native addon
9
9
  * prebuilds are arch-specific. Build darwin-arm64 and darwin-x64 separately.
@@ -146,16 +146,45 @@ function findQvacScopeDir(startDir) {
146
146
  }
147
147
 
148
148
  /**
149
- * Discovers installed @qvac addon packages by scanning node_modules/@qvac
150
- * for packages that have `addon: true` in package.json.
149
+ * Reads the addon a per-platform prebuild package ships binaries for.
150
+ *
151
+ * Addons that split their prebuilds (`@qvac/tts-ggml` since 0.9.0, along with
152
+ * `@qvac/asr-ggml` 0.5.0 and `@qvac/audiogen-ggml` 0.4.0) publish the host's
153
+ * `.bare` in `@qvac/<addon>-<host>` (iOS flavours grouped as
154
+ * `@qvac/<addon>-ios`). Those packages carry no top-level `addon: true`;
155
+ * instead they embed an inner `addon/` package named after the meta addon.
156
+ * Returns that addon name, or null when `pkgDir` is not a platform package.
157
+ * Exposed for unit testing.
158
+ */
159
+ function readPlatformPackageAddon(pkgDir) {
160
+ const addonPkgJsonPath = path.join(pkgDir, 'addon', 'package.json')
161
+ if (!fs.existsSync(addonPkgJsonPath)) return null
162
+
163
+ try {
164
+ const pkg = JSON.parse(fs.readFileSync(addonPkgJsonPath, 'utf8'))
165
+ if (pkg.addon === true && typeof pkg.name === 'string' && pkg.name.length > 0) {
166
+ return pkg.name
167
+ }
168
+ } catch (err) {
169
+ logger.warn(`Failed to parse ${addonPkgJsonPath}: ${err.message}`)
170
+ }
171
+ return null
172
+ }
173
+
174
+ /**
175
+ * Discovers installed @qvac packages by scanning node_modules/@qvac: the
176
+ * addons (`addon: true` in package.json) and the per-platform prebuild
177
+ * packages, each mapped to the addon it belongs to.
151
178
  */
152
179
  function discoverQvacAddonPackages(projectDir) {
180
+ const nothing = { addons: [], platformPackages: [] }
181
+
153
182
  let scopeDir
154
183
  try {
155
184
  scopeDir = findQvacScopeDir(projectDir)
156
185
  } catch (err) {
157
186
  logger.warn(err.message)
158
- return []
187
+ return nothing
159
188
  }
160
189
 
161
190
  let entries
@@ -163,10 +192,11 @@ function discoverQvacAddonPackages(projectDir) {
163
192
  entries = fs.readdirSync(scopeDir)
164
193
  } catch (err) {
165
194
  logger.fsError('discoverQvacAddonPackages', err)
166
- return []
195
+ return nothing
167
196
  }
168
197
 
169
- const discovered = []
198
+ const addons = []
199
+ const platformPackages = []
170
200
 
171
201
  for (const name of entries) {
172
202
  const pkgDir = path.join(scopeDir, name)
@@ -175,18 +205,25 @@ function discoverQvacAddonPackages(projectDir) {
175
205
  const pkgJsonPath = path.join(pkgDir, 'package.json')
176
206
  if (!fs.existsSync(pkgJsonPath)) continue
177
207
 
208
+ const packageName = `@qvac/${name}`
178
209
  try {
179
210
  const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
180
211
  if (pkg.addon === true) {
181
- discovered.push(`@qvac/${name}`)
212
+ addons.push(packageName)
213
+ continue
182
214
  }
183
215
  } catch (err) {
184
216
  logger.warn(`Failed to parse ${pkgJsonPath}: ${err.message}`)
217
+ continue
185
218
  }
219
+
220
+ const addon = readPlatformPackageAddon(pkgDir)
221
+ if (addon !== null) platformPackages.push({ name: packageName, addon })
186
222
  }
187
223
 
188
- discovered.sort()
189
- return discovered
224
+ addons.sort()
225
+ platformPackages.sort((a, b) => a.name.localeCompare(b.name))
226
+ return { addons, platformPackages }
190
227
  }
191
228
 
192
229
  /**
@@ -203,20 +240,36 @@ function diffAddons(installed, required) {
203
240
  }
204
241
 
205
242
  /**
206
- * Computes the list of installed @qvac addons that aren't in `required`.
243
+ * Pure diff: per-platform prebuild packages whose addon is not in the
244
+ * required set. A platform package ships only its addon's binaries, so it
245
+ * follows that addon's include/exclude decision.
246
+ * Exposed for unit testing.
247
+ */
248
+ function diffPlatformPackages(platformPackages, required) {
249
+ const requiredSet = new Set(required)
250
+ const exclusions = []
251
+ for (const { name, addon } of platformPackages) {
252
+ if (!requiredSet.has(addon)) exclusions.push(name)
253
+ }
254
+ return exclusions
255
+ }
256
+
257
+ /**
258
+ * Computes the list of installed @qvac addons that aren't in `required`,
259
+ * plus the per-platform prebuild packages of those excluded addons.
207
260
  * Logs include/exclude decisions for each discovered package.
208
261
  */
209
262
  function computeExclusions(required, projectDir) {
210
- const installed = discoverQvacAddonPackages(projectDir)
263
+ const { addons, platformPackages } = discoverQvacAddonPackages(projectDir)
211
264
 
212
- if (installed.length === 0) {
265
+ if (addons.length === 0) {
213
266
  logger.warn('No @qvac addon packages discovered. Skipping addon exclusions.')
214
267
  return []
215
268
  }
216
269
 
217
270
  const requiredSet = new Set(required)
218
271
  const exclusions = []
219
- for (const pkg of installed) {
272
+ for (const pkg of addons) {
220
273
  if (requiredSet.has(pkg)) {
221
274
  logger.info(`Including required addon: ${pkg}`)
222
275
  } else {
@@ -224,6 +277,14 @@ function computeExclusions(required, projectDir) {
224
277
  exclusions.push(pkg)
225
278
  }
226
279
  }
280
+ for (const { name, addon } of platformPackages) {
281
+ if (requiredSet.has(addon)) {
282
+ logger.info(`Including platform package ${name} (prebuilds for ${addon})`)
283
+ } else {
284
+ logger.info(`Excluding platform package ${name} (prebuilds for unused addon ${addon})`)
285
+ exclusions.push(name)
286
+ }
287
+ }
227
288
  return exclusions
228
289
  }
229
290
 
@@ -594,6 +655,15 @@ async function runBundleAndVerify(commands, projectDir, options) {
594
655
  )
595
656
  }
596
657
 
658
+ // Narrowed prebuild checks are otherwise invisible in the log.
659
+ const narrowed = verifyResult.addons.filter(
660
+ (addon) => Array.isArray(addon.linkedHosts) && addon.linkedHosts.length < hosts.length
661
+ )
662
+ for (const addon of narrowed) {
663
+ const where = addon.linkedHosts.length === 0 ? 'no host' : addon.linkedHosts.join(', ')
664
+ logger.info(`verifyBundle: ${addon.name}@${addon.version} is linked on ${where}`)
665
+ }
666
+
597
667
  return bundleResult
598
668
  }
599
669
 
@@ -772,6 +842,8 @@ module.exports.QvacForgePluginError = QvacForgePluginError
772
842
  // Internal helpers exposed for unit tests. Not part of the stable API.
773
843
  module.exports.createIgnore = createIgnore
774
844
  module.exports.diffAddons = diffAddons
845
+ module.exports.diffPlatformPackages = diffPlatformPackages
846
+ module.exports.readPlatformPackageAddon = readPlatformPackageAddon
775
847
  module.exports.detectTargetHosts = detectTargetHosts
776
848
  module.exports.resolveHosts = resolveHosts
777
849
  module.exports.runBundleAndVerify = runBundleAndVerify
@@ -5,12 +5,15 @@
5
5
  * Otherwise, falls back to linking all installed addons.
6
6
  *
7
7
  * This file is copied over react-native-bare-kit/android/link.mjs
8
- * by withMobileBundle.ts during expo prebuild.
8
+ * by withMobileBundle.ts during expo prebuild. The resolver it imports is
9
+ * compiled from qvac-platform-addons.ts and installed beside this file as
10
+ * qvac-platform-addons.mjs.
9
11
  */
10
12
  import fs from 'fs'
11
13
  import path from 'path'
12
14
  import { fileURLToPath } from 'url'
13
15
  import link from 'bare-link'
16
+ import { resolvePlatformAddonRoots } from './qvac-platform-addons.mjs'
14
17
 
15
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
16
19
  const projectRoot = path.join(__dirname, '..', '..', '..')
@@ -24,10 +27,12 @@ if (fs.existsSync(addonsDir)) {
24
27
  const manifestPath = path.join(projectRoot, 'qvac', 'addons.manifest.json')
25
28
 
26
29
  let pkg = null
30
+ let addonNames = []
27
31
  if (fs.existsSync(manifestPath)) {
28
32
  try {
29
33
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
30
34
  const addons = Array.isArray(manifest.addons) ? manifest.addons : []
35
+ addonNames = addons
31
36
 
32
37
  if (addons.length > 0) {
33
38
  console.log(`[QVAC] Using addons manifest (${addons.length} addons): ${addons.join(', ')}`)
@@ -46,13 +51,17 @@ if (fs.existsSync(manifestPath)) {
46
51
  console.log('[QVAC] No addons manifest found, linking all addons')
47
52
  }
48
53
 
49
- for await (const resource of link(
50
- projectRoot,
51
- {
52
- hosts: ['android-arm64', 'android-arm', 'android-ia32', 'android-x64'],
53
- out: addonsDir
54
- },
55
- pkg
56
- )) {
54
+ const hosts = ['android-arm64', 'android-arm', 'android-ia32', 'android-x64']
55
+
56
+ for await (const resource of link(projectRoot, { hosts, out: addonsDir }, pkg)) {
57
57
  console.log('Wrote', resource)
58
58
  }
59
+
60
+ // Split addons keep their binaries in a per-platform package; bare-link only
61
+ // reaches the meta, which has shipped no prebuilds/ since the split.
62
+ for (const addon of resolvePlatformAddonRoots(projectRoot, addonNames, 'android')) {
63
+ console.log(`[QVAC] Linking split addon ${addon.pkg.name} from its platform package`)
64
+ for await (const resource of link(addon.dir, { hosts, out: addonsDir }, addon.pkg)) {
65
+ console.log('Wrote', resource)
66
+ }
67
+ }
@@ -5,12 +5,15 @@
5
5
  * Otherwise, falls back to linking all installed addons.
6
6
  *
7
7
  * This file is copied over react-native-bare-kit/ios/link.mjs
8
- * by withMobileBundle.ts during expo prebuild.
8
+ * by withMobileBundle.ts during expo prebuild. The resolver it imports is
9
+ * compiled from qvac-platform-addons.ts and installed beside this file as
10
+ * qvac-platform-addons.mjs.
9
11
  */
10
12
  import fs from 'fs'
11
13
  import path from 'path'
12
14
  import { fileURLToPath } from 'url'
13
15
  import link from 'bare-link'
16
+ import { resolvePlatformAddonRoots } from './qvac-platform-addons.mjs'
14
17
 
15
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
16
19
  const projectRoot = path.join(__dirname, '..', '..', '..')
@@ -24,10 +27,12 @@ if (fs.existsSync(addonsDir)) {
24
27
  const manifestPath = path.join(projectRoot, 'qvac', 'addons.manifest.json')
25
28
 
26
29
  let pkg = null
30
+ let addonNames = []
27
31
  if (fs.existsSync(manifestPath)) {
28
32
  try {
29
33
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
30
34
  const addons = Array.isArray(manifest.addons) ? manifest.addons : []
35
+ addonNames = addons
31
36
 
32
37
  if (addons.length > 0) {
33
38
  console.log(`[QVAC] Using addons manifest (${addons.length} addons): ${addons.join(', ')}`)
@@ -46,13 +51,17 @@ if (fs.existsSync(manifestPath)) {
46
51
  console.log('[QVAC] No addons manifest found, linking all addons')
47
52
  }
48
53
 
49
- for await (const resource of link(
50
- projectRoot,
51
- {
52
- hosts: ['ios-arm64', 'ios-arm64-simulator', 'ios-x64-simulator'],
53
- out: addonsDir
54
- },
55
- pkg
56
- )) {
54
+ const hosts = ['ios-arm64', 'ios-arm64-simulator', 'ios-x64-simulator']
55
+
56
+ for await (const resource of link(projectRoot, { hosts, out: addonsDir }, pkg)) {
57
57
  console.log('Wrote', resource)
58
58
  }
59
+
60
+ // Split addons keep their binaries in a per-platform package; bare-link only
61
+ // reaches the meta, which has shipped no prebuilds/ since the split.
62
+ for (const addon of resolvePlatformAddonRoots(projectRoot, addonNames, 'ios')) {
63
+ console.log(`[QVAC] Linking split addon ${addon.pkg.name} from its platform package`)
64
+ for await (const resource of link(addon.dir, { hosts, out: addonsDir }, addon.pkg)) {
65
+ console.log('Wrote', resource)
66
+ }
67
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Resolves split-addon platform packages from the meta package's `#host-addon`
3
+ * map. Linker, verify, and the missing-prebuild error all call this module so
4
+ * the slice name cannot drift from what the addon actually imports.
5
+ *
6
+ * Keep this file free of `@/` imports: the compiled JS is copied next to the
7
+ * patched `link.mjs` as `qvac-platform-addons.mjs`.
8
+ */
9
+ import * as fs from 'fs'
10
+ import * as path from 'path'
11
+
12
+ export const HOST_ADDON_IMPORT = '#host-addon'
13
+
14
+ const DEFAULT_ANDROID_CPU = 'arm64'
15
+
16
+ export interface PlatformAddonRoot {
17
+ dir: string
18
+ pkg: { name?: string; version?: string; addon?: boolean }
19
+ }
20
+
21
+ export function resolvePlatformAddonRoots(
22
+ projectRoot: string,
23
+ addonNames: string[],
24
+ platform: string
25
+ ): PlatformAddonRoot[] {
26
+ const roots: PlatformAddonRoot[] = []
27
+ for (const name of addonNames) {
28
+ const root = resolvePlatformAddonRoot(projectRoot, name, platform)
29
+ if (root !== null) roots.push(root)
30
+ }
31
+ return roots
32
+ }
33
+
34
+ function resolvePlatformAddonRoot(
35
+ projectRoot: string,
36
+ metaName: string,
37
+ platform: string
38
+ ): PlatformAddonRoot | null {
39
+ const metaManifest = readManifest(packageDir(projectRoot, metaName))
40
+ if (metaManifest === null) return null
41
+
42
+ const platformPackage = resolveAddonPlatformPackage(
43
+ metaName,
44
+ metaManifest.imports?.[HOST_ADDON_IMPORT],
45
+ platform
46
+ )
47
+ if (platformPackage === null) return null
48
+
49
+ const addonDir = path.join(packageDir(projectRoot, platformPackage), 'addon')
50
+ const addonManifest = readManifest(addonDir)
51
+ if (addonManifest === null || addonManifest.addon !== true) return null
52
+ if (!fs.existsSync(path.join(addonDir, 'prebuilds'))) return null
53
+
54
+ return { dir: addonDir, pkg: addonManifest }
55
+ }
56
+
57
+ function packageDir(projectRoot: string, packageName: string): string {
58
+ return path.join(projectRoot, 'node_modules', ...packageName.split('/'))
59
+ }
60
+
61
+ interface AddonManifest {
62
+ name?: string
63
+ version?: string
64
+ addon?: boolean
65
+ imports?: Record<string, unknown>
66
+ }
67
+
68
+ function readManifest(dir: string): AddonManifest | null {
69
+ try {
70
+ const parsed: unknown = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
71
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return null
72
+ return parsed as AddonManifest
73
+ } catch {
74
+ return null
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Package `#host-addon` names for `host`, if that name belongs to `metaName`.
80
+ * `host` is a Bare host (`android-arm64`, `ios-arm64-simulator`, `darwin-arm64`)
81
+ * or a mobile build target (`android`, `ios`).
82
+ */
83
+ export function resolveAddonPlatformPackage(
84
+ metaName: string,
85
+ hostAddon: unknown,
86
+ host: string
87
+ ): string | null {
88
+ const name = resolvePlatformPackageName(hostAddon, host)
89
+ if (name === null) return null
90
+ if (!name.startsWith(`${metaName}-`)) return null
91
+ return name
92
+ }
93
+
94
+ /**
95
+ * First package name a `#host-addon` map points at for `host`.
96
+ * Android nests the package under its architecture; iOS is a flat list.
97
+ * A platform-only `android` target uses arm64, matching the current mobile host.
98
+ */
99
+ export function resolvePlatformPackageName(hostAddon: unknown, host: string): string | null {
100
+ if (typeof host !== 'string' || host.length === 0) return null
101
+
102
+ const separator = host.indexOf('-')
103
+ const platform = separator === -1 ? host : host.slice(0, separator)
104
+ const cpu = separator === -1 ? '' : host.slice(separator + 1)
105
+
106
+ const branch = readBranch(hostAddon, platform)
107
+ const direct = packageName(branch)
108
+ if (direct !== null) return direct
109
+
110
+ const cpuKey = cpu.length > 0 ? cpu : DEFAULT_ANDROID_CPU
111
+ return packageName(readBranch(branch, cpuKey))
112
+ }
113
+
114
+ function packageName(candidate: unknown): string | null {
115
+ const name = Array.isArray(candidate) ? candidate[0] : candidate
116
+ if (typeof name !== 'string' || name.startsWith('.')) return null
117
+ return name
118
+ }
119
+
120
+ function readBranch(value: unknown, key: string): unknown {
121
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined
122
+ if (!Object.hasOwn(value, key)) return undefined
123
+ return (value as Record<string, unknown>)[key]
124
+ }