@pikku/core 0.12.69 → 0.12.71

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 (118) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +21 -0
  3. package/README.md +34 -2
  4. package/dist/function/functions.types.d.ts +27 -0
  5. package/dist/index.d.ts +1 -1
  6. package/dist/internal.d.ts +1 -1
  7. package/dist/internal.js +1 -1
  8. package/dist/pikku-state.js +1 -0
  9. package/dist/services/http-scenario-actors.d.ts +12 -4
  10. package/dist/services/http-scenario-actors.js +47 -45
  11. package/dist/services/in-memory-queue-service.d.ts +6 -0
  12. package/dist/services/in-memory-queue-service.js +8 -1
  13. package/dist/services/in-memory-workflow-service.d.ts +3 -5
  14. package/dist/services/in-memory-workflow-service.js +10 -19
  15. package/dist/services/index.d.ts +2 -1
  16. package/dist/services/index.js +1 -0
  17. package/dist/services/meta-service.d.ts +5 -1
  18. package/dist/services/meta-service.js +44 -18
  19. package/dist/services/scenario-actors-service.d.ts +108 -2
  20. package/dist/services/scenario-actors-service.js +40 -1
  21. package/dist/services/workflow-service.d.ts +7 -5
  22. package/dist/types/core.types.d.ts +28 -3
  23. package/dist/types/state.types.d.ts +3 -1
  24. package/dist/wirings/actor-flow/actor-flow.types.d.ts +1 -1
  25. package/dist/wirings/actor-flow/index.d.ts +1 -1
  26. package/dist/wirings/actor-flow/run-conversation.d.ts +10 -10
  27. package/dist/wirings/actor-flow/run-conversation.js +27 -27
  28. package/dist/wirings/ai-agent/ai-agent-agui.js +0 -8
  29. package/dist/wirings/ai-agent/ai-agent-prepare.js +1 -2
  30. package/dist/wirings/ai-agent/ai-agent.types.d.ts +0 -6
  31. package/dist/wirings/cli/command-parser.js +11 -1
  32. package/dist/wirings/rpc/rpc-runner.js +1 -1
  33. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +52 -3
  34. package/dist/wirings/workflow/feature.d.ts +28 -0
  35. package/dist/wirings/workflow/feature.js +57 -0
  36. package/dist/wirings/workflow/graph/graph-runner.js +3 -2
  37. package/dist/wirings/workflow/graph/graph-validation.d.ts +0 -2
  38. package/dist/wirings/workflow/graph/graph-validation.js +0 -142
  39. package/dist/wirings/workflow/graph/index.d.ts +1 -1
  40. package/dist/wirings/workflow/graph/index.js +1 -1
  41. package/dist/wirings/workflow/index.d.ts +13 -3
  42. package/dist/wirings/workflow/index.js +15 -2
  43. package/dist/wirings/workflow/pikku-scenario-service.d.ts +121 -0
  44. package/dist/wirings/workflow/pikku-scenario-service.js +419 -0
  45. package/dist/wirings/workflow/pikku-workflow-service.d.ts +170 -23
  46. package/dist/wirings/workflow/pikku-workflow-service.js +338 -297
  47. package/dist/wirings/workflow/scenario-cookie-jar.d.ts +29 -0
  48. package/dist/wirings/workflow/scenario-cookie-jar.js +51 -0
  49. package/dist/wirings/workflow/scenario-poll.d.ts +20 -0
  50. package/dist/wirings/workflow/scenario-poll.js +25 -0
  51. package/dist/wirings/workflow/scenario-prose.d.ts +38 -0
  52. package/dist/wirings/workflow/scenario-prose.js +45 -0
  53. package/dist/wirings/workflow/scenario-step-guards.d.ts +16 -0
  54. package/dist/wirings/workflow/scenario-step-guards.js +29 -0
  55. package/dist/wirings/workflow/scenario-step.types.d.ts +148 -0
  56. package/dist/wirings/workflow/scenario-step.types.js +1 -0
  57. package/dist/wirings/workflow/workflow.types.d.ts +82 -8
  58. package/package.json +3 -1
  59. package/src/function/functions.types.ts +32 -0
  60. package/src/index.ts +1 -0
  61. package/src/internal.ts +5 -1
  62. package/src/pikku-state.ts +1 -0
  63. package/src/services/http-scenario-actors.test.ts +85 -1
  64. package/src/services/http-scenario-actors.ts +65 -51
  65. package/src/services/in-memory-queue-service.test.ts +66 -1
  66. package/src/services/in-memory-queue-service.ts +13 -2
  67. package/src/services/in-memory-workflow-service.ts +12 -25
  68. package/src/services/index.ts +5 -0
  69. package/src/services/meta-service.test.ts +79 -0
  70. package/src/services/meta-service.ts +61 -26
  71. package/src/services/scenario-actors-service.ts +157 -2
  72. package/src/services/workflow-service.ts +7 -4
  73. package/src/types/core.types.ts +34 -2
  74. package/src/types/state.types.ts +3 -0
  75. package/src/wirings/actor-flow/actor-flow.types.ts +1 -1
  76. package/src/wirings/actor-flow/index.ts +1 -1
  77. package/src/wirings/actor-flow/run-conversation.test.ts +12 -6
  78. package/src/wirings/actor-flow/run-conversation.ts +36 -41
  79. package/src/wirings/ai-agent/ai-agent-agui.test.ts +0 -16
  80. package/src/wirings/ai-agent/ai-agent-agui.ts +0 -9
  81. package/src/wirings/ai-agent/ai-agent-prepare.ts +1 -2
  82. package/src/wirings/ai-agent/ai-agent.types.ts +0 -7
  83. package/src/wirings/cli/command-parser.test.ts +60 -0
  84. package/src/wirings/cli/command-parser.ts +12 -1
  85. package/src/wirings/rpc/rpc-runner.test.ts +28 -5
  86. package/src/wirings/rpc/rpc-runner.ts +1 -1
  87. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +86 -2
  88. package/src/wirings/workflow/feature.test.ts +131 -0
  89. package/src/wirings/workflow/feature.ts +78 -0
  90. package/src/wirings/workflow/graph/graph-runner.ts +3 -2
  91. package/src/wirings/workflow/graph/graph-validation.test.ts +1 -144
  92. package/src/wirings/workflow/graph/graph-validation.ts +0 -196
  93. package/src/wirings/workflow/graph/index.ts +1 -5
  94. package/src/wirings/workflow/index.ts +73 -6
  95. package/src/wirings/workflow/pikku-scenario-service.ts +682 -0
  96. package/src/wirings/workflow/pikku-workflow-service.test.ts +55 -0
  97. package/src/wirings/workflow/pikku-workflow-service.ts +572 -419
  98. package/src/wirings/workflow/scenario-cookie-jar.test.ts +108 -0
  99. package/src/wirings/workflow/scenario-cookie-jar.ts +65 -0
  100. package/src/wirings/workflow/scenario-expectations.test.ts +153 -0
  101. package/src/wirings/workflow/scenario-hooks.test.ts +212 -0
  102. package/src/wirings/workflow/scenario-poll.test.ts +66 -0
  103. package/src/wirings/workflow/scenario-poll.ts +36 -0
  104. package/src/wirings/workflow/scenario-prose.test.ts +152 -0
  105. package/src/wirings/workflow/scenario-prose.ts +79 -0
  106. package/src/wirings/workflow/scenario-service.test.ts +155 -0
  107. package/src/wirings/workflow/scenario-step-guards.ts +43 -0
  108. package/src/wirings/workflow/scenario-step.test.ts +442 -9
  109. package/src/wirings/workflow/scenario-step.types.ts +157 -0
  110. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  111. package/src/wirings/workflow/workflow-dispatch-payload.test.ts +59 -0
  112. package/src/wirings/workflow/workflow-mirror.test.ts +178 -0
  113. package/src/wirings/workflow/workflow-replay-snapshot.test.ts +139 -0
  114. package/src/wirings/workflow/workflow-run-context.test.ts +177 -0
  115. package/src/wirings/workflow/workflow-run-polling.test.ts +132 -0
  116. package/src/wirings/workflow/workflow-step-ordinal.test.ts +4 -4
  117. package/src/wirings/workflow/workflow.types.ts +99 -5
  118. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,152 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { composeStepProse, renderStepTemplate } from './scenario-prose.js'
5
+
6
+ describe('renderStepTemplate', () => {
7
+ test('substitutes the step input into its placeholders', () => {
8
+ assert.equal(
9
+ renderStepTemplate('sees {packageName}', {
10
+ packageName: '@pikku/addon-stripe',
11
+ }),
12
+ 'sees @pikku/addon-stripe'
13
+ )
14
+ })
15
+
16
+ test('renders numbers and booleans as words rather than JSON', () => {
17
+ assert.equal(
18
+ renderStepTemplate('sees at least {atLeast} addons, listed {listed}', {
19
+ atLeast: 10,
20
+ listed: true,
21
+ }),
22
+ 'sees at least 10 addons, listed true'
23
+ )
24
+ })
25
+
26
+ test('an absent optional value leaves no gap in the sentence', () => {
27
+ assert.equal(
28
+ renderStepTemplate('sees {packageName} {state}', {
29
+ packageName: '@pikku/addon-stripe',
30
+ }),
31
+ 'sees @pikku/addon-stripe'
32
+ )
33
+ })
34
+
35
+ test('a placeholder repeated in the template is substituted each time', () => {
36
+ assert.equal(
37
+ renderStepTemplate('searches {query} and sees {query}', {
38
+ query: 'stripe',
39
+ }),
40
+ 'searches stripe and sees stripe'
41
+ )
42
+ })
43
+
44
+ test('a non-primitive value is serialised compactly', () => {
45
+ assert.equal(
46
+ renderStepTemplate('sends {payload}', { payload: { a: 1 } }),
47
+ 'sends {"a":1}'
48
+ )
49
+ })
50
+
51
+ test('no recorded input leaves the template legible rather than half-rendered', () => {
52
+ assert.equal(renderStepTemplate('sees {packageName}', undefined), 'sees')
53
+ })
54
+ })
55
+
56
+ describe('composeStepProse', () => {
57
+ test('renders a template against the step input in place of the description', () => {
58
+ assert.equal(
59
+ composeStepProse({
60
+ phase: 'then',
61
+ description: 'sees an addon in the gallery',
62
+ template: 'sees {packageName}',
63
+ input: { packageName: '@pikku/addon-todos' },
64
+ actor: 'admin',
65
+ }),
66
+ 'Then the admin sees @pikku/addon-todos'
67
+ )
68
+ })
69
+
70
+ test('falls back to the description when the step declares no template', () => {
71
+ assert.equal(
72
+ composeStepProse({
73
+ phase: 'then',
74
+ description: 'sees an addon in the gallery',
75
+ input: { packageName: '@pikku/addon-todos' },
76
+ actor: 'admin',
77
+ }),
78
+ 'Then the admin sees an addon in the gallery'
79
+ )
80
+ })
81
+ })
82
+
83
+ describe('composeStepProse basics', () => {
84
+ test('renders the gherkin keyword, the actor and the description', () => {
85
+ assert.equal(
86
+ composeStepProse({
87
+ phase: 'given',
88
+ description: 'buys an apple',
89
+ actor: 'shopper',
90
+ }),
91
+ 'Given the shopper buys an apple'
92
+ )
93
+ assert.equal(
94
+ composeStepProse({
95
+ phase: 'then',
96
+ description: 'sees a receipt',
97
+ actor: 'shopper',
98
+ }),
99
+ 'Then the shopper sees a receipt'
100
+ )
101
+ })
102
+
103
+ test('an actorless step reads as a plain sentence', () => {
104
+ assert.equal(
105
+ composeStepProse({
106
+ phase: 'given',
107
+ description: 'the app data is reset',
108
+ }),
109
+ 'Given the app data is reset'
110
+ )
111
+ })
112
+
113
+ test('the neutral `step` phase adds no keyword', () => {
114
+ assert.equal(
115
+ composeStepProse({
116
+ phase: 'step',
117
+ description: 'refreshes the dashboard',
118
+ actor: 'admin',
119
+ }),
120
+ 'the admin refreshes the dashboard'
121
+ )
122
+ })
123
+
124
+ test('a keyword width lines the sentences up under each other', () => {
125
+ const rendered = (
126
+ [
127
+ { phase: 'given', description: 'buys an apple' },
128
+ { phase: 'when', description: 'checks out' },
129
+ { phase: 'then', description: 'sees a receipt' },
130
+ { phase: 'step', description: 'waits' },
131
+ ] as const
132
+ ).map((step) =>
133
+ composeStepProse({ ...step, actor: 'shopper', keywordWidth: 5 })
134
+ )
135
+
136
+ assert.deepEqual(rendered, [
137
+ 'Given the shopper buys an apple',
138
+ 'When the shopper checks out',
139
+ 'Then the shopper sees a receipt',
140
+ ' the shopper waits',
141
+ ])
142
+ const columns = new Set(rendered.map((line) => line.indexOf('the shopper')))
143
+ assert.equal(columns.size, 1, 'every sentence starts in the same column')
144
+ })
145
+
146
+ test('prose degrades to just the description when nothing else is known', () => {
147
+ assert.equal(
148
+ composeStepProse({ phase: 'step', description: 'does the thing' }),
149
+ 'does the thing'
150
+ )
151
+ })
152
+ })
@@ -0,0 +1,79 @@
1
+ import type { ScenarioStepPhase } from './scenario-step.types.js'
2
+
3
+ /**
4
+ * Renders the English sentence a reporter shows for a scenario step.
5
+ *
6
+ * This is the inversion of cucumber: rather than parsing English into a call,
7
+ * we render English out of a typed call, so the readable report survives
8
+ * without a regex registry paying for it.
9
+ *
10
+ * Lives in core so the CLI reporter and the console render identically.
11
+ */
12
+ /**
13
+ * Fill a step's `template` from the input that step was actually called with,
14
+ * so the reported sentence names the values under test — "sees @pikku/addon-todos"
15
+ * rather than "sees an addon in the gallery" repeated three times.
16
+ *
17
+ * A placeholder with no recorded value renders as nothing and the surrounding
18
+ * whitespace collapses, so an optional input that was omitted reads as a shorter
19
+ * sentence rather than a literal `{state}` leaking into the report.
20
+ */
21
+ export const renderStepTemplate = (
22
+ template: string,
23
+ input: unknown
24
+ ): string => {
25
+ const values =
26
+ input && typeof input === 'object' ? (input as Record<string, unknown>) : {}
27
+ return template
28
+ .replace(/\{(\w+)\}/g, (_match, key: string) => formatValue(values[key]))
29
+ .replace(/\s+/g, ' ')
30
+ .trim()
31
+ }
32
+
33
+ const formatValue = (value: unknown): string => {
34
+ if (value === undefined || value === null) {
35
+ return ''
36
+ }
37
+ if (typeof value === 'object') {
38
+ return JSON.stringify(value)
39
+ }
40
+ return String(value)
41
+ }
42
+
43
+ export const composeStepProse = ({
44
+ phase,
45
+ description,
46
+ template,
47
+ input,
48
+ actor,
49
+ keywordWidth,
50
+ }: {
51
+ phase: ScenarioStepPhase
52
+ description: string
53
+ /**
54
+ * The prose this step renders, with `{placeholders}` filled from `input`.
55
+ * Unlike `description`, which documents what the step does, this is what a
56
+ * reader of the report sees. Falls back to `description` when absent.
57
+ */
58
+ template?: string
59
+ /** The input this step was called with, as recorded on the run. */
60
+ input?: unknown
61
+ actor?: string
62
+ /**
63
+ * Pad the keyword to this width so a ladder of steps lines its sentences up
64
+ * under each other. Omit for inline prose.
65
+ */
66
+ keywordWidth?: number
67
+ }): string => {
68
+ const keyword = phase === 'step' ? '' : capitalise(phase)
69
+ const subject = actor ? `the ${actor}` : ''
70
+ const rendered = template ? renderStepTemplate(template, input) : description
71
+ const sentence = [subject, rendered].filter(Boolean).join(' ')
72
+ if (keywordWidth === undefined) {
73
+ return [keyword, sentence].filter(Boolean).join(' ')
74
+ }
75
+ return `${keyword.padEnd(keywordWidth)} ${sentence}`
76
+ }
77
+
78
+ const capitalise = (value: string) =>
79
+ value.charAt(0).toUpperCase() + value.slice(1)
@@ -0,0 +1,155 @@
1
+ import { describe, test, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { InMemoryWorkflowService } from '../../services/in-memory-workflow-service.js'
5
+ import {
6
+ PikkuScenarioService,
7
+ createScenarioRunner,
8
+ } from './pikku-scenario-service.js'
9
+ import { pikkuState, resetPikkuState } from '../../pikku-state.js'
10
+
11
+ const noopLogger = { error() {}, info() {}, warn() {}, debug() {} }
12
+
13
+ /**
14
+ * Members that only a scenario run reaches. None of them may live on the class
15
+ * a production app instantiates: a bundler removes unused *modules*, never
16
+ * unused class members, so anything declared here ships in every server built
17
+ * on Pikku whether or not the app has a single scenario.
18
+ */
19
+ const SCENARIO_MEMBERS = [
20
+ 'scenarioStep',
21
+ 'runScenarioHook',
22
+ 'resolveScenarioActors',
23
+ 'setScenarioBrowserProvider',
24
+ 'getScenarioBrowserProvider',
25
+ 'setScenarioEnvironment',
26
+ 'getScenarioEnvironment',
27
+ ]
28
+
29
+ /** Wire members declared by `PikkuScenarioWire` and absent from `PikkuWorkflowWire`. */
30
+ const SCENARIO_WIRE_MEMBERS = [
31
+ 'step',
32
+ 'given',
33
+ 'when',
34
+ 'then',
35
+ 'expectEventually',
36
+ 'expectError',
37
+ 'expectService',
38
+ 'runScheduledTask',
39
+ ]
40
+
41
+ const setup = async (ws: InMemoryWorkflowService) => {
42
+ pikkuState(null, 'package', 'singletonServices', {
43
+ logger: noopLogger,
44
+ } as any)
45
+ const runId = await ws.createRun('surfaceTest', {}, true, 'hash', {
46
+ type: 'test',
47
+ } as any)
48
+ ws.registerInlineRun(runId)
49
+ return runId
50
+ }
51
+
52
+ describe('the production workflow service carries no scenario surface', () => {
53
+ beforeEach(() => resetPikkuState())
54
+
55
+ test('no scenario method is declared on the workflow service prototype chain', () => {
56
+ const ws = new InMemoryWorkflowService()
57
+ for (const member of SCENARIO_MEMBERS) {
58
+ assert.equal(
59
+ typeof (ws as any)[member],
60
+ 'undefined',
61
+ `'${member}' ships in every production bundle while it is a member of PikkuWorkflowService`
62
+ )
63
+ }
64
+ })
65
+
66
+ test('the workflow wire exposes only workflow members', async () => {
67
+ const ws = new InMemoryWorkflowService()
68
+ const runId = await setup(ws)
69
+ const wire = ws.createWorkflowWire('surfaceTest', runId, {}) as any
70
+
71
+ for (const member of SCENARIO_WIRE_MEMBERS) {
72
+ assert.equal(
73
+ typeof wire[member],
74
+ 'undefined',
75
+ `wire.${member} is a scenario affordance and must not be built for a production workflow run`
76
+ )
77
+ }
78
+ for (const member of ['do', 'sleep', 'suspend', 'approval', 'getRun']) {
79
+ assert.equal(
80
+ typeof wire[member],
81
+ 'function',
82
+ `wire.${member} is a workflow member and must survive`
83
+ )
84
+ }
85
+ })
86
+
87
+ test('a service with no extension installed runs a plain workflow untouched', async () => {
88
+ const ws = new InMemoryWorkflowService()
89
+ assert.equal(ws.getRunExtension(), undefined)
90
+ const runId = await setup(ws)
91
+ const wire = ws.createWorkflowWire('surfaceTest', runId, {}) as any
92
+ assert.equal(typeof wire.do, 'function')
93
+ })
94
+
95
+ test('the actor client is not reachable from the workflow service module graph', async () => {
96
+ const { readFileSync } = await import('node:fs')
97
+ const source = readFileSync(
98
+ new URL('./pikku-workflow-service.ts', import.meta.url),
99
+ 'utf8'
100
+ )
101
+ assert.ok(
102
+ !source.includes('http-scenario-actors'),
103
+ 'PikkuWorkflowService must not reference the HTTP actor client — it pulls the AI persona loop into every bundle'
104
+ )
105
+ })
106
+ })
107
+
108
+ describe('PikkuScenarioService carries the scenario surface', () => {
109
+ beforeEach(() => resetPikkuState())
110
+
111
+ test('every scenario method is declared on the scenario service', () => {
112
+ const { scenarioService } = createScenarioRunner()
113
+ for (const member of SCENARIO_MEMBERS) {
114
+ assert.equal(
115
+ typeof (scenarioService as any)[member],
116
+ 'function',
117
+ `PikkuScenarioService is missing '${member}'`
118
+ )
119
+ }
120
+ })
121
+
122
+ test('the wire it decorates carries both the workflow and the scenario members', async () => {
123
+ const { workflowService: ws } = createScenarioRunner()
124
+ const runId = await setup(ws)
125
+ const wire = ws.createWorkflowWire('surfaceTest', runId, {}) as any
126
+
127
+ for (const member of [
128
+ ...SCENARIO_WIRE_MEMBERS,
129
+ 'do',
130
+ 'sleep',
131
+ 'approval',
132
+ ]) {
133
+ assert.equal(
134
+ typeof wire[member],
135
+ 'function',
136
+ `PikkuScenarioService's wire is missing ${member}`
137
+ )
138
+ }
139
+ })
140
+
141
+ test('it is not a workflow service, so it cannot be registered as one by mistake', () => {
142
+ const { scenarioService } = createScenarioRunner()
143
+ assert.ok(!(scenarioService instanceof InMemoryWorkflowService))
144
+ assert.equal(
145
+ typeof (scenarioService as any).startWorkflow,
146
+ 'undefined',
147
+ 'the scenario capability is layered onto a run, it does not own one'
148
+ )
149
+ })
150
+
151
+ test('the runner installs it as the workflow service extension', () => {
152
+ const { workflowService, scenarioService } = createScenarioRunner()
153
+ assert.equal(workflowService.getRunExtension(), scenarioService)
154
+ })
155
+ })
@@ -0,0 +1,43 @@
1
+ import type {
2
+ PikkuScenarioStepWire,
3
+ ScenarioEnvironment,
4
+ } from './scenario-step.types.js'
5
+
6
+ /**
7
+ * The actor this step was called with, or a loud error naming the step.
8
+ *
9
+ * A step that talks to the target app needs an identity, but `actor` is
10
+ * optional on the wire because a pure assertion step needs none. This is the
11
+ * one place that narrowing happens, so every step file stops writing its own
12
+ * `actorOf(...)` guard.
13
+ */
14
+ export const requireActor = <TActor>(
15
+ scenarioStep: PikkuScenarioStepWire<TActor> | undefined
16
+ ): TActor => {
17
+ const actor = scenarioStep?.actor
18
+ if (!actor) {
19
+ throw new Error(
20
+ `[scenario] step '${scenarioStep?.name ?? 'unknown'}' was called without an actor. ` +
21
+ `Pass { actor: actors.<name> } so it runs as that persona.`
22
+ )
23
+ }
24
+ return actor
25
+ }
26
+
27
+ /**
28
+ * The environment this run targets, or a loud error naming the step. A run
29
+ * started outside `pikku scenario run` only carries one when the server has
30
+ * `API_URL` configured.
31
+ */
32
+ export const requireScenarioEnv = (
33
+ scenarioStep: PikkuScenarioStepWire<unknown> | undefined
34
+ ): ScenarioEnvironment => {
35
+ const env = scenarioStep?.env
36
+ if (!env) {
37
+ throw new Error(
38
+ `[scenario] step '${scenarioStep?.name ?? 'unknown'}' needs the target environment, but this run carries none. ` +
39
+ `Run it through 'pikku scenario run <environment>' with scenarios.environments.<environment> declared in pikku.config.json.`
40
+ )
41
+ }
42
+ return env
43
+ }