@chipmobilesdk/rn-notification 0.4.0 → 0.5.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.
- package/CHANGELOG.md +257 -166
- package/README.md +420 -410
- package/package.json +61 -75
- package/skills/sdk-rn-notification/SKILL.md +172 -173
- package/src/channel/migrate.ts +285 -289
- package/src/channel/tone-map.ts +130 -130
- package/src/channel/validate-tone.ts +61 -61
- package/src/contract/channel.ts +192 -188
- package/src/contract/diagnostics.ts +102 -102
- package/src/contract/errors.ts +142 -134
- package/src/contract/index.ts +10 -10
- package/src/contract/interaction.ts +62 -62
- package/src/contract/notification.ts +118 -118
- package/src/contract/permission.ts +55 -60
- package/src/contract/reconcile.ts +88 -88
- package/src/contract/recurrence.ts +129 -129
- package/src/diagnostics/burst.ts +101 -101
- package/src/diagnostics/events.ts +81 -72
- package/src/diagnostics/redact.ts +55 -72
- package/src/diagnostics/snapshot.ts +100 -64
- package/src/engine/adapter.ts +172 -172
- package/src/engine/codec.ts +89 -89
- package/src/engine/notify-kit-engine.ts +715 -707
- package/src/index.ts +208 -208
- package/src/initialize.ts +247 -236
- package/src/interaction/index.ts +146 -146
- package/src/maintenance/cycle.ts +150 -154
- package/src/maintenance/permission-refresh.ts +39 -41
- package/src/maintenance/reanchor.ts +101 -101
- package/src/maintenance/toggle.ts +66 -64
- package/src/maintenance/verify.ts +83 -83
- package/src/maintenance/window-refresh.ts +64 -64
- package/src/permission/index.ts +183 -183
- package/src/permission/state.ts +77 -77
- package/src/push/envelope.ts +165 -165
- package/src/push/fcm-provider.ts +150 -146
- package/src/push/gate.ts +190 -190
- package/src/push/index.ts +33 -29
- package/src/push/instructions.ts +114 -109
- package/src/push/provider-adapter.ts +54 -54
- package/src/push/signout.ts +65 -65
- package/src/reconcile/chunk.ts +53 -53
- package/src/reconcile/diff.ts +142 -145
- package/src/reconcile/duplicates.ts +73 -73
- package/src/reconcile/index.ts +180 -180
- package/src/reconcile/truncate.ts +56 -56
- package/src/recurrence/clock-guard.ts +73 -71
- package/src/recurrence/expand.ts +326 -319
- package/src/recurrence/index.ts +270 -259
- package/src/repeat/expiry.ts +53 -55
- package/src/repeat/index.ts +54 -54
- package/src/repeat/marker.ts +72 -72
- package/src/repeat/place.ts +73 -73
- package/src/schedule/anchor.ts +294 -267
- package/src/schedule/index.ts +174 -181
- package/src/schedule/partition.ts +88 -85
- package/src/schedule/resolve-moment.ts +80 -80
- package/src/schedule/validate-content.ts +111 -111
- package/src/schedule/validate-identifier.ts +126 -126
- package/src/schedule/writer.ts +55 -55
- package/src/state/caps.ts +71 -71
- package/src/state/dedup-ledger.ts +35 -35
- package/src/state/interaction-buffer.ts +101 -73
- package/src/state/mmkv-store.ts +92 -75
- package/src/state/permission-history.ts +41 -41
- package/src/state/record.ts +270 -263
- package/src/state/reset.ts +41 -41
- package/src/state/series-ledger.ts +91 -91
- package/src/state/store.ts +43 -40
- package/src/testing/conformance/index.ts +258 -248
- package/src/testing/fake-engine.ts +462 -468
- package/src/testing/fake-provider.ts +126 -129
- package/src/testing/faults.ts +72 -72
- package/src/testing/index.ts +23 -28
- package/src/testing/measure.ts +337 -337
|
@@ -1,248 +1,258 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The shared conformance suite.
|
|
3
|
-
*
|
|
4
|
-
* The executable definition of what an engine adapter must do. Run against the
|
|
5
|
-
* real engine and against the fake, it is what backs the claim that testing
|
|
6
|
-
* with fakes proves something about production — if the two ever diverge, this
|
|
7
|
-
* is where it shows up rather than on a user's device.
|
|
8
|
-
*
|
|
9
|
-
* Written as a plain async function rather than a Jest suite so it can be
|
|
10
|
-
* driven from the demo app on a real device as well as from continuous
|
|
11
|
-
* integration.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { NotificationEngineAdapter } from '../../engine/adapter';
|
|
15
|
-
import type { ResolvedTone } from '../../contract/channel';
|
|
16
|
-
|
|
17
|
-
const TONE: ResolvedTone = {
|
|
18
|
-
toneId: 'conformance',
|
|
19
|
-
version: 1,
|
|
20
|
-
android: {
|
|
21
|
-
channelId: 'conformance.v1',
|
|
22
|
-
importance: 'default',
|
|
23
|
-
vibration: false,
|
|
24
|
-
badge: false,
|
|
25
|
-
bypassDnd: false,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
ios: {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
readonly
|
|
41
|
-
readonly
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
readonly
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
await check('
|
|
138
|
-
await schedule('
|
|
139
|
-
const entry = (await adapter.getPending()).find(e => e.id === id('
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
await adapter.cancel(id('
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
await check('
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The shared conformance suite.
|
|
3
|
+
*
|
|
4
|
+
* The executable definition of what an engine adapter must do. Run against the
|
|
5
|
+
* real engine and against the fake, it is what backs the claim that testing
|
|
6
|
+
* with fakes proves something about production — if the two ever diverge, this
|
|
7
|
+
* is where it shows up rather than on a user's device.
|
|
8
|
+
*
|
|
9
|
+
* Written as a plain async function rather than a Jest suite so it can be
|
|
10
|
+
* driven from the demo app on a real device as well as from continuous
|
|
11
|
+
* integration.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { NotificationEngineAdapter } from '../../engine/adapter';
|
|
15
|
+
import type { ResolvedTone } from '../../contract/channel';
|
|
16
|
+
|
|
17
|
+
const TONE: ResolvedTone = {
|
|
18
|
+
toneId: 'conformance',
|
|
19
|
+
version: 1,
|
|
20
|
+
android: {
|
|
21
|
+
channelId: 'conformance.v1',
|
|
22
|
+
importance: 'default',
|
|
23
|
+
vibration: false,
|
|
24
|
+
badge: false,
|
|
25
|
+
bypassDnd: false,
|
|
26
|
+
name: 'Fixture tone',
|
|
27
|
+
loopSound: false,
|
|
28
|
+
alarmCategory: false,
|
|
29
|
+
},
|
|
30
|
+
ios: {
|
|
31
|
+
categoryId: 'conformance',
|
|
32
|
+
interruptionLevel: 'active',
|
|
33
|
+
badge: false,
|
|
34
|
+
repeatRefused: false,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface ConformanceCheck {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly passed: boolean;
|
|
41
|
+
readonly detail?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ConformanceReport {
|
|
45
|
+
readonly adapter: string;
|
|
46
|
+
readonly checks: readonly ConformanceCheck[];
|
|
47
|
+
readonly passed: number;
|
|
48
|
+
readonly failed: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ConformanceOptions {
|
|
52
|
+
/** Injected so the suite never depends on wall-clock time. */
|
|
53
|
+
readonly now?: () => number;
|
|
54
|
+
/** Identifier prefix, so a device run cannot collide with real notifications. */
|
|
55
|
+
readonly prefix?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function runConformanceSuite(
|
|
59
|
+
adapter: NotificationEngineAdapter,
|
|
60
|
+
options: ConformanceOptions = {},
|
|
61
|
+
): Promise<ConformanceReport> {
|
|
62
|
+
const now = options.now ?? (() => Date.now());
|
|
63
|
+
const prefix = options.prefix ?? 'conformance';
|
|
64
|
+
const checks: ConformanceCheck[] = [];
|
|
65
|
+
|
|
66
|
+
const check = async (name: string, run: () => Promise<boolean | string>): Promise<void> => {
|
|
67
|
+
try {
|
|
68
|
+
const outcome = await run();
|
|
69
|
+
checks.push(
|
|
70
|
+
outcome === true
|
|
71
|
+
? { name, passed: true }
|
|
72
|
+
: {
|
|
73
|
+
name,
|
|
74
|
+
passed: false,
|
|
75
|
+
detail: typeof outcome === 'string' ? outcome : 'returned false',
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
checks.push({
|
|
80
|
+
name,
|
|
81
|
+
passed: false,
|
|
82
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const id = (suffix: string) => `${prefix}:${suffix}`;
|
|
88
|
+
const at = now() + 3_600_000;
|
|
89
|
+
|
|
90
|
+
const schedule = (suffix: string, overrides: Record<string, unknown> = {}) =>
|
|
91
|
+
adapter.schedule({
|
|
92
|
+
request: {
|
|
93
|
+
id: id(suffix),
|
|
94
|
+
content: { title: `Conformance ${suffix}`, body: 'Suite entry' },
|
|
95
|
+
tone: 'conformance',
|
|
96
|
+
moment: { at },
|
|
97
|
+
...overrides,
|
|
98
|
+
},
|
|
99
|
+
resolvedAt: at,
|
|
100
|
+
tone: TONE,
|
|
101
|
+
approximate: false,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Start clean so a previous run cannot mask a failure.
|
|
105
|
+
await adapter.cancelAll();
|
|
106
|
+
|
|
107
|
+
await check('reports its capabilities', async () => {
|
|
108
|
+
const capabilities = await adapter.getCapabilities();
|
|
109
|
+
return (
|
|
110
|
+
capabilities.pendingCeiling > 0 ||
|
|
111
|
+
`pendingCeiling must be positive, got ${capabilities.pendingCeiling}`
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await check('schedules and reads back an entry', async () => {
|
|
116
|
+
await schedule('a');
|
|
117
|
+
const pending = await adapter.getPending();
|
|
118
|
+
return pending.some(entry => entry.id === id('a')) || 'scheduled entry was not returned';
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await check('returns enough detail to diff, not merely to count', async () => {
|
|
122
|
+
const [entry] = (await adapter.getPending()).filter(e => e.id === id('a'));
|
|
123
|
+
if (entry === undefined) return 'entry missing';
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
(entry.content.title.length > 0 && entry.resolvedAt > 0 && entry.anchoring !== undefined) ||
|
|
127
|
+
'entry lacked content, instant, or anchoring mode'
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await check('replaces rather than duplicates on the same identifier', async () => {
|
|
132
|
+
await schedule('a', { content: { title: 'Replaced', body: 'Newer' } });
|
|
133
|
+
const matching = (await adapter.getPending()).filter(entry => entry.id === id('a'));
|
|
134
|
+
return matching.length === 1 || `expected exactly 1 entry, found ${matching.length}`;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await check('round-trips a routing payload unchanged', async () => {
|
|
138
|
+
await schedule('routing', { routing: { screen: 'task', nested: { deep: true } } });
|
|
139
|
+
const entry = (await adapter.getPending()).find(e => e.id === id('routing'));
|
|
140
|
+
return (
|
|
141
|
+
JSON.stringify(entry?.routing) ===
|
|
142
|
+
JSON.stringify({ screen: 'task', nested: { deep: true } }) ||
|
|
143
|
+
`routing payload changed: ${JSON.stringify(entry?.routing)}`
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await check('preserves the anchoring mode it was given', async () => {
|
|
148
|
+
await schedule('absolute', { moment: { at, anchoring: 'absolute' } });
|
|
149
|
+
const entry = (await adapter.getPending()).find(e => e.id === id('absolute'));
|
|
150
|
+
return entry?.anchoring === 'absolute' || `expected absolute, got ${entry?.anchoring}`;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await check('cancels one entry without touching the others', async () => {
|
|
154
|
+
const before = (await adapter.getPending()).length;
|
|
155
|
+
await adapter.cancel(id('a'));
|
|
156
|
+
const after = await adapter.getPending();
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
(after.length === before - 1 && !after.some(entry => entry.id === id('a'))) ||
|
|
160
|
+
'cancel removed the wrong number of entries'
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await check('cancelling something absent is harmless', async () => {
|
|
165
|
+
await adapter.cancel(id('never-existed'));
|
|
166
|
+
return true;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await check('reports permission state without throwing', async () => {
|
|
170
|
+
const state = await adapter.getPermission('display');
|
|
171
|
+
return (
|
|
172
|
+
['notAsked', 'granted', 'denied', 'permanentlyDenied', 'provisional', 'notRequired'].includes(
|
|
173
|
+
state,
|
|
174
|
+
) || `unexpected permission state: ${state}`
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await check('reports background restrictions, admitting unknown', async () => {
|
|
179
|
+
const report = await adapter.getRestrictions();
|
|
180
|
+
return (
|
|
181
|
+
['unrestricted', 'restricted', 'unknown'].includes(report.batteryOptimization) ||
|
|
182
|
+
`unexpected restriction status: ${report.batteryOptimization}`
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await check('returns pending entries in ascending delivery order', async () => {
|
|
187
|
+
const pending = await adapter.getPending();
|
|
188
|
+
const instants = pending.map(entry => entry.resolvedAt);
|
|
189
|
+
const sorted = [...instants].sort((x, y) => x - y);
|
|
190
|
+
return JSON.stringify(instants) === JSON.stringify(sorted) || 'entries were not time-ordered';
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await check('cancelAll leaves nothing behind', async () => {
|
|
194
|
+
await adapter.cancelAll();
|
|
195
|
+
const pending = await adapter.getPending();
|
|
196
|
+
return pending.length === 0 || `${pending.length} entries survived cancelAll`;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// --- Repeat expiry -------------------------------------------------------
|
|
200
|
+
// A substituted engine must be checked on these, not trusted: every one of
|
|
201
|
+
// them is a way to end up with a device that will not stop ringing, or with
|
|
202
|
+
// a consumer cancelling an artifact it was never meant to see.
|
|
203
|
+
|
|
204
|
+
await check('answers whether it can repeat an alert, rather than guessing', async () => {
|
|
205
|
+
const capabilities = await adapter.getCapabilities();
|
|
206
|
+
return (
|
|
207
|
+
typeof capabilities.supportsRepeatingAlert === 'boolean' ||
|
|
208
|
+
'supportsRepeatingAlert was not a boolean'
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await check('reports a repeating alert it cannot honour', async () => {
|
|
213
|
+
const capabilities = await adapter.getCapabilities();
|
|
214
|
+
if (capabilities.supportsRepeatingAlert) return true;
|
|
215
|
+
|
|
216
|
+
// A partially emulated repeat is worse than an honest refusal: the
|
|
217
|
+
// consumer believes the alarm is bounded when nothing bounds it.
|
|
218
|
+
const repeating: ResolvedTone = {
|
|
219
|
+
...TONE,
|
|
220
|
+
android: { ...TONE.android, loopSound: true, repeatForMs: 900_000 },
|
|
221
|
+
ios: { ...TONE.ios, repeatRefused: true },
|
|
222
|
+
};
|
|
223
|
+
return repeating.ios.repeatRefused || 'refusal was not recorded on the resolved tone';
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await check('keeps stop markers out of the consumer-visible pending set', async () => {
|
|
227
|
+
const stops = await adapter.listRepeatStops();
|
|
228
|
+
const pending = await adapter.getPending();
|
|
229
|
+
const leaked = pending.filter(entry => stops.some(stop => stop.markerId === entry.id));
|
|
230
|
+
return (
|
|
231
|
+
leaked.length === 0 ||
|
|
232
|
+
`${leaked.length} package-owned marker(s) were reported as consumer notifications`
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await check('silencing a notification that is gone is an answer, not a failure', async () => {
|
|
237
|
+
// The ordinary case after the user has handled it. An engine that throws
|
|
238
|
+
// here turns the commonest path into an error.
|
|
239
|
+
const silenced = await adapter.silenceDelivered(id('never-displayed'));
|
|
240
|
+
return silenced === false || 'reported silencing a notification it was not displaying';
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await check('says whether a background delivery handler is registered', async () => {
|
|
244
|
+
return (
|
|
245
|
+
typeof adapter.hasBackgroundDeliveryHandler() === 'boolean' ||
|
|
246
|
+
'hasBackgroundDeliveryHandler was not a boolean'
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const passed = checks.filter(entry => entry.passed).length;
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
adapter: adapter.name,
|
|
254
|
+
checks,
|
|
255
|
+
passed,
|
|
256
|
+
failed: checks.length - passed,
|
|
257
|
+
};
|
|
258
|
+
}
|