oysterun 1.5.3-beta.1 → 1.5.3-beta.2
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.
|
@@ -6,7 +6,15 @@ import { fileURLToPath } from "url";
|
|
|
6
6
|
import { resolveDefaultBrowsePathAsync, resolveDirectoryPathAsync } from "./config.mjs";
|
|
7
7
|
import { completeSchedulerSessionSetupPayloadForRuntime } from "./scheduler-setup-snapshot-contract.mjs";
|
|
8
8
|
import { deriveBrowserRootProjectId } from "./browser-root-project-index.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
normalizePortableSchedulerDefinitionsToHostTimezone,
|
|
11
|
+
normalizePortableSetupSnapshot,
|
|
12
|
+
} from "./portable-scheduler-definitions.mjs";
|
|
13
|
+
import {
|
|
14
|
+
computeNextScheduleRunAt,
|
|
15
|
+
getHostSystemTimezone,
|
|
16
|
+
normalizeScheduleRule,
|
|
17
|
+
} from "./scheduler-rule-model.mjs";
|
|
10
18
|
import { FULL_DISK_ACCESS_SETTINGS_URI as MACOS_FULL_DISK_ACCESS_SETTINGS_URI } from "./macos-permissions.mjs";
|
|
11
19
|
|
|
12
20
|
const DEFAULT_LIMIT = 100;
|
|
@@ -92,47 +100,6 @@ function nowIso() {
|
|
|
92
100
|
return new Date().toISOString();
|
|
93
101
|
}
|
|
94
102
|
|
|
95
|
-
function taipeiDateParts(date) {
|
|
96
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
97
|
-
timeZone: "Asia/Taipei",
|
|
98
|
-
year: "numeric",
|
|
99
|
-
month: "2-digit",
|
|
100
|
-
day: "2-digit",
|
|
101
|
-
hour: "2-digit",
|
|
102
|
-
minute: "2-digit",
|
|
103
|
-
second: "2-digit",
|
|
104
|
-
hour12: false,
|
|
105
|
-
}).formatToParts(date);
|
|
106
|
-
const values = Object.fromEntries(
|
|
107
|
-
parts
|
|
108
|
-
.filter((part) => part.type !== "literal")
|
|
109
|
-
.map((part) => [part.type, Number(part.value)])
|
|
110
|
-
);
|
|
111
|
-
return {
|
|
112
|
-
year: values.year,
|
|
113
|
-
month: values.month,
|
|
114
|
-
day: values.day,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function computeNextTaipeiDailyRunAt(time = "11:30", now = new Date()) {
|
|
119
|
-
const match = String(time || "11:30").match(/^(\d{1,2}):(\d{2})$/);
|
|
120
|
-
const hour = match ? Number(match[1]) : 11;
|
|
121
|
-
const minute = match ? Number(match[2]) : 30;
|
|
122
|
-
const parts = taipeiDateParts(now);
|
|
123
|
-
let targetUtc = Date.UTC(
|
|
124
|
-
parts.year,
|
|
125
|
-
parts.month - 1,
|
|
126
|
-
parts.day,
|
|
127
|
-
hour - 8,
|
|
128
|
-
minute,
|
|
129
|
-
0,
|
|
130
|
-
0
|
|
131
|
-
);
|
|
132
|
-
if (targetUtc <= now.getTime()) targetUtc += 24 * 60 * 60 * 1000;
|
|
133
|
-
return new Date(targetUtc).toISOString();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
103
|
async function normalizeCopiedDemoAgentSchedulers(copiedPath) {
|
|
137
104
|
const schedulersPath = join(copiedPath, ".oysterun", "schedulers.json");
|
|
138
105
|
const raw = await readFile(schedulersPath, "utf8");
|
|
@@ -145,6 +112,7 @@ async function normalizeCopiedDemoAgentSchedulers(copiedPath) {
|
|
|
145
112
|
);
|
|
146
113
|
}
|
|
147
114
|
const timestamp = nowIso();
|
|
115
|
+
const hostTimezone = getHostSystemTimezone();
|
|
148
116
|
const projectId = deriveBrowserRootProjectId(basename(copiedPath));
|
|
149
117
|
const normalizedSchedulers = payload.schedulers.map((entry) => {
|
|
150
118
|
const schedule = JSON.parse(JSON.stringify(entry));
|
|
@@ -155,17 +123,23 @@ async function normalizeCopiedDemoAgentSchedulers(copiedPath) {
|
|
|
155
123
|
"demo_agent_scheduler_invalid"
|
|
156
124
|
);
|
|
157
125
|
}
|
|
158
|
-
const time =
|
|
159
|
-
schedule?.schedule_rule?.timezone === "Asia/Taipei"
|
|
160
|
-
? schedule.schedule_rule.time
|
|
161
|
-
: "11:30";
|
|
162
126
|
const id = randomUUID();
|
|
163
127
|
schedule.id = id;
|
|
164
128
|
schedule.agent_id = projectId;
|
|
165
129
|
schedule.created_by = "oysterun-demo-agent-copy";
|
|
166
130
|
schedule.enabled = true;
|
|
167
131
|
schedule.status = "active";
|
|
168
|
-
|
|
132
|
+
const hostRule = normalizeScheduleRule(
|
|
133
|
+
{ ...schedule.schedule_rule, timezone: hostTimezone },
|
|
134
|
+
{ timezone: hostTimezone }
|
|
135
|
+
);
|
|
136
|
+
schedule.next_run_at =
|
|
137
|
+
hostRule.type === "once"
|
|
138
|
+
? hostRule.at
|
|
139
|
+
: computeNextScheduleRunAt(hostRule, {
|
|
140
|
+
after: timestamp,
|
|
141
|
+
timezone: hostTimezone,
|
|
142
|
+
});
|
|
169
143
|
schedule.created_at = timestamp;
|
|
170
144
|
schedule.updated_at = timestamp;
|
|
171
145
|
const portableSetupSnapshot = normalizePortableSetupSnapshot(
|
|
@@ -195,9 +169,23 @@ async function normalizeCopiedDemoAgentSchedulers(copiedPath) {
|
|
|
195
169
|
schedulersPath,
|
|
196
170
|
`${JSON.stringify({ version: 1, schedulers: normalizedSchedulers }, null, 2)}\n`
|
|
197
171
|
);
|
|
172
|
+
const timezoneNormalization =
|
|
173
|
+
normalizePortableSchedulerDefinitionsToHostTimezone(copiedPath, {
|
|
174
|
+
hostTimezone,
|
|
175
|
+
source: "demo_agent_copy",
|
|
176
|
+
now: timestamp,
|
|
177
|
+
});
|
|
178
|
+
const finalPayload = JSON.parse(await readFile(schedulersPath, "utf8"));
|
|
198
179
|
return {
|
|
199
|
-
scheduler_count:
|
|
200
|
-
scheduler_ids:
|
|
180
|
+
scheduler_count: finalPayload.schedulers.length,
|
|
181
|
+
scheduler_ids: finalPayload.schedulers.map((entry) => entry.id),
|
|
182
|
+
timezone_normalization: {
|
|
183
|
+
status: timezoneNormalization.status,
|
|
184
|
+
host_timezone: timezoneNormalization.host_timezone,
|
|
185
|
+
corrected_count: timezoneNormalization.corrected_count,
|
|
186
|
+
},
|
|
187
|
+
timezone_correction_count: timezoneNormalization.corrected_count,
|
|
188
|
+
timezone_corrections: timezoneNormalization.corrections,
|
|
201
189
|
};
|
|
202
190
|
}
|
|
203
191
|
|
|
@@ -523,6 +511,12 @@ export async function copyDemoAgent({
|
|
|
523
511
|
copied_path: copiedPath,
|
|
524
512
|
portable_scheduler_count: schedulerRegistration.scheduler_count,
|
|
525
513
|
portable_scheduler_ids: schedulerRegistration.scheduler_ids,
|
|
514
|
+
portable_scheduler_timezone_normalization:
|
|
515
|
+
schedulerRegistration.timezone_normalization,
|
|
516
|
+
portable_scheduler_timezone_correction_count:
|
|
517
|
+
schedulerRegistration.timezone_correction_count || 0,
|
|
518
|
+
portable_scheduler_timezone_corrections:
|
|
519
|
+
schedulerRegistration.timezone_corrections || [],
|
|
526
520
|
start_session_available: true,
|
|
527
521
|
};
|
|
528
522
|
}
|
|
@@ -12,6 +12,7 @@ import { completeSchedulerSessionSetupPayloadForRuntime } from "./scheduler-setu
|
|
|
12
12
|
import { deriveBrowserRootProjectId } from "./browser-root-project-index.mjs";
|
|
13
13
|
import {
|
|
14
14
|
computeNextScheduleRunAt,
|
|
15
|
+
getHostSystemTimezone,
|
|
15
16
|
normalizeScheduleRule,
|
|
16
17
|
} from "./scheduler-rule-model.mjs";
|
|
17
18
|
|
|
@@ -330,6 +331,151 @@ function writePortableFile(filePath, payload) {
|
|
|
330
331
|
renameSync(tmpPath, filePath);
|
|
331
332
|
}
|
|
332
333
|
|
|
334
|
+
function hasOwn(value, key) {
|
|
335
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function existingTimezoneFields(schedule) {
|
|
339
|
+
const fields = [];
|
|
340
|
+
const add = (container, key, field) => {
|
|
341
|
+
if (!isObjectRecord(container) || !hasOwn(container, key)) return;
|
|
342
|
+
const timezone = normalizeOptionalString(container[key]);
|
|
343
|
+
fields.push({ container, key, field, timezone });
|
|
344
|
+
};
|
|
345
|
+
add(schedule, "timezone", "definition.timezone");
|
|
346
|
+
add(schedule.schedule_rule, "timezone", "definition.schedule_rule.timezone");
|
|
347
|
+
add(schedule.metadata, "timezone", "definition.metadata.timezone");
|
|
348
|
+
add(
|
|
349
|
+
schedule.metadata?.schedule_rule,
|
|
350
|
+
"timezone",
|
|
351
|
+
"definition.metadata.schedule_rule.timezone"
|
|
352
|
+
);
|
|
353
|
+
return fields;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function buildPortableTimezoneCorrection({
|
|
357
|
+
agentFolder,
|
|
358
|
+
schedule,
|
|
359
|
+
fields,
|
|
360
|
+
filePath,
|
|
361
|
+
hostTimezone,
|
|
362
|
+
source,
|
|
363
|
+
}) {
|
|
364
|
+
const mismatched = fields.find((field) => field.timezone !== hostTimezone);
|
|
365
|
+
const firstKnown = fields.find((field) => field.timezone);
|
|
366
|
+
const oldTimezone = mismatched?.timezone || firstKnown?.timezone || null;
|
|
367
|
+
const timePreserved =
|
|
368
|
+
normalizeOptionalString(schedule.schedule_rule?.time) ||
|
|
369
|
+
normalizeOptionalString(schedule.metadata?.schedule_rule?.time) ||
|
|
370
|
+
normalizeOptionalString(schedule.schedule_rule?.at) ||
|
|
371
|
+
null;
|
|
372
|
+
return {
|
|
373
|
+
agent_folder: agentFolder,
|
|
374
|
+
schedule_id: normalizeOptionalString(schedule.id) || null,
|
|
375
|
+
old_timezone: oldTimezone,
|
|
376
|
+
host_timezone: hostTimezone,
|
|
377
|
+
time_preserved: timePreserved,
|
|
378
|
+
portable_file_path: filePath,
|
|
379
|
+
source,
|
|
380
|
+
message: `Portable scheduler timezone normalized from ${
|
|
381
|
+
oldTimezone || "unspecified"
|
|
382
|
+
} to ${hostTimezone}; scheduled time preserved as ${
|
|
383
|
+
timePreserved || "absolute timestamp"
|
|
384
|
+
}.`,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function recomputePortableNextRunAtForHost(schedule, { hostTimezone, now }) {
|
|
389
|
+
if (!hasOwn(schedule, "next_run_at")) return schedule.next_run_at;
|
|
390
|
+
if (!isObjectRecord(schedule.schedule_rule)) return schedule.next_run_at;
|
|
391
|
+
const hostRule = { ...schedule.schedule_rule, timezone: hostTimezone };
|
|
392
|
+
const normalizedRule = normalizeScheduleRule(hostRule, {
|
|
393
|
+
timezone: hostTimezone,
|
|
394
|
+
});
|
|
395
|
+
if (normalizedRule.type === "once") {
|
|
396
|
+
return normalizeOptionalString(schedule.next_run_at) || normalizedRule.at;
|
|
397
|
+
}
|
|
398
|
+
return computeNextScheduleRunAt(normalizedRule, {
|
|
399
|
+
after: now,
|
|
400
|
+
timezone: hostTimezone,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function normalizePortableSchedulerDefinitionsToHostTimezone(
|
|
405
|
+
agentFolder,
|
|
406
|
+
{
|
|
407
|
+
hostTimezone = getHostSystemTimezone(),
|
|
408
|
+
source = "browser_root_portable_scheduler_import",
|
|
409
|
+
now = null,
|
|
410
|
+
clock = () => new Date(),
|
|
411
|
+
} = {}
|
|
412
|
+
) {
|
|
413
|
+
const realFolder = realpathSync(
|
|
414
|
+
normalizeRequiredString(agentFolder, "agentFolder")
|
|
415
|
+
);
|
|
416
|
+
const normalizedHostTimezone = normalizeScheduleRule(
|
|
417
|
+
{ type: "daily", time: "00:00", timezone: hostTimezone },
|
|
418
|
+
{ timezone: hostTimezone }
|
|
419
|
+
).timezone;
|
|
420
|
+
const filePath = getPortableSchedulersJsonPath(realFolder);
|
|
421
|
+
const payload = readPortableFile(filePath);
|
|
422
|
+
const correctionTime = nowIso(now === null ? clock : () => now);
|
|
423
|
+
let changed = false;
|
|
424
|
+
const corrections = [];
|
|
425
|
+
const schedulers = payload.schedulers.map((entry) => {
|
|
426
|
+
if (!isObjectRecord(entry)) return entry;
|
|
427
|
+
const schedule = deepCloneJson(entry, "portable scheduler definition");
|
|
428
|
+
const fields = existingTimezoneFields(schedule);
|
|
429
|
+
const mismatched = fields.filter(
|
|
430
|
+
(field) => field.timezone !== normalizedHostTimezone
|
|
431
|
+
);
|
|
432
|
+
if (mismatched.length === 0) return schedule;
|
|
433
|
+
|
|
434
|
+
for (const field of fields) {
|
|
435
|
+
if (field.timezone !== normalizedHostTimezone) {
|
|
436
|
+
field.container[field.key] = normalizedHostTimezone;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (hasOwn(schedule, "next_run_at")) {
|
|
440
|
+
schedule.next_run_at = recomputePortableNextRunAtForHost(schedule, {
|
|
441
|
+
hostTimezone: normalizedHostTimezone,
|
|
442
|
+
now: correctionTime,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
schedule.updated_at = correctionTime;
|
|
446
|
+
corrections.push(
|
|
447
|
+
buildPortableTimezoneCorrection({
|
|
448
|
+
agentFolder: realFolder,
|
|
449
|
+
schedule,
|
|
450
|
+
fields,
|
|
451
|
+
filePath,
|
|
452
|
+
hostTimezone: normalizedHostTimezone,
|
|
453
|
+
source,
|
|
454
|
+
})
|
|
455
|
+
);
|
|
456
|
+
changed = true;
|
|
457
|
+
return schedule;
|
|
458
|
+
});
|
|
459
|
+
if (changed) {
|
|
460
|
+
writePortableFile(filePath, {
|
|
461
|
+
...payload,
|
|
462
|
+
schedulers,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
status: changed
|
|
467
|
+
? "portable_scheduler_timezone_normalized"
|
|
468
|
+
: "portable_scheduler_timezone_already_host",
|
|
469
|
+
agent_folder: realFolder,
|
|
470
|
+
portable_file_path: filePath,
|
|
471
|
+
host_timezone: normalizedHostTimezone,
|
|
472
|
+
scheduler_count: payload.schedulers.length,
|
|
473
|
+
corrected_count: corrections.length,
|
|
474
|
+
corrections,
|
|
475
|
+
changed,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
333
479
|
function normalizeBrowserRootProjectEntry(entry) {
|
|
334
480
|
if (!isObjectRecord(entry)) {
|
|
335
481
|
throw new Error("Browser Root project entry must be an object");
|
|
@@ -450,6 +596,66 @@ export class PortableSchedulerDefinitionStore {
|
|
|
450
596
|
return filePath;
|
|
451
597
|
}
|
|
452
598
|
|
|
599
|
+
normalizeDefinitionsForFolderToHostTimezone(
|
|
600
|
+
agentFolder,
|
|
601
|
+
{
|
|
602
|
+
hostTimezone = getHostSystemTimezone(),
|
|
603
|
+
source = "browser_root_portable_scheduler_import",
|
|
604
|
+
} = {}
|
|
605
|
+
) {
|
|
606
|
+
const result = normalizePortableSchedulerDefinitionsToHostTimezone(
|
|
607
|
+
agentFolder,
|
|
608
|
+
{
|
|
609
|
+
hostTimezone,
|
|
610
|
+
source,
|
|
611
|
+
clock: this.clock,
|
|
612
|
+
}
|
|
613
|
+
);
|
|
614
|
+
if (result.changed) this.invalidateScheduleCache();
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
normalizeBrowserRootPortableSchedulersToHostTimezone({
|
|
619
|
+
hostTimezone = getHostSystemTimezone(),
|
|
620
|
+
source = "browser_root_portable_scheduler_import",
|
|
621
|
+
} = {}) {
|
|
622
|
+
const projects = this.getBrowserRootProjectFolders();
|
|
623
|
+
const results = [];
|
|
624
|
+
const corrections = [];
|
|
625
|
+
let schedulerCount = 0;
|
|
626
|
+
for (const project of projects) {
|
|
627
|
+
const result = this.normalizeDefinitionsForFolderToHostTimezone(
|
|
628
|
+
project.agent_folder,
|
|
629
|
+
{ hostTimezone, source }
|
|
630
|
+
);
|
|
631
|
+
schedulerCount += result.scheduler_count;
|
|
632
|
+
corrections.push(...result.corrections);
|
|
633
|
+
results.push({
|
|
634
|
+
project_id: project.project_id,
|
|
635
|
+
agent_folder: project.agent_folder,
|
|
636
|
+
portable_file_path: result.portable_file_path,
|
|
637
|
+
scheduler_count: result.scheduler_count,
|
|
638
|
+
corrected_count: result.corrected_count,
|
|
639
|
+
status: result.status,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
status:
|
|
644
|
+
corrections.length > 0
|
|
645
|
+
? "browser_root_portable_scheduler_timezone_normalized"
|
|
646
|
+
: "browser_root_portable_scheduler_timezone_already_host",
|
|
647
|
+
host_timezone: normalizeScheduleRule(
|
|
648
|
+
{ type: "daily", time: "00:00", timezone: hostTimezone },
|
|
649
|
+
{ timezone: hostTimezone }
|
|
650
|
+
).timezone,
|
|
651
|
+
project_count: projects.length,
|
|
652
|
+
scheduler_count: schedulerCount,
|
|
653
|
+
corrected_count: corrections.length,
|
|
654
|
+
corrections,
|
|
655
|
+
results,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
453
659
|
buildPortableHash(definition) {
|
|
454
660
|
return hashPortableScheduleDefinition({
|
|
455
661
|
id: definition.id,
|
|
@@ -1120,6 +1120,29 @@ export class SchedulerService {
|
|
|
1120
1120
|
.map((schedule) => this.serializeHostScheduleForDashboardUi(schedule));
|
|
1121
1121
|
}
|
|
1122
1122
|
|
|
1123
|
+
normalizeBrowserRootPortableSchedules({ hostTimezone = null } = {}) {
|
|
1124
|
+
this.initialize();
|
|
1125
|
+
const timezoneNormalization =
|
|
1126
|
+
this.portableDefinitionStore.normalizeBrowserRootPortableSchedulersToHostTimezone(
|
|
1127
|
+
{
|
|
1128
|
+
hostTimezone: hostTimezone || getHostSystemTimezone(),
|
|
1129
|
+
source: "browser_root_portable_scheduler_import",
|
|
1130
|
+
}
|
|
1131
|
+
);
|
|
1132
|
+
return {
|
|
1133
|
+
status: timezoneNormalization.status,
|
|
1134
|
+
host_timezone: timezoneNormalization.host_timezone,
|
|
1135
|
+
project_count: timezoneNormalization.project_count,
|
|
1136
|
+
scheduler_count: timezoneNormalization.scheduler_count,
|
|
1137
|
+
timezone_correction_count: timezoneNormalization.corrected_count,
|
|
1138
|
+
timezone_corrections: timezoneNormalization.corrections,
|
|
1139
|
+
timezone_correction_notifications: timezoneNormalization.corrections.map(
|
|
1140
|
+
(correction) => correction.message
|
|
1141
|
+
),
|
|
1142
|
+
results: timezoneNormalization.results,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1123
1146
|
getHostScheduleForDashboard(scheduleId) {
|
|
1124
1147
|
return this.serializeHostScheduleForDashboardUi(
|
|
1125
1148
|
this.requireHostSchedule(scheduleId)
|
package/host-service/server.mjs
CHANGED
|
@@ -14811,11 +14811,28 @@ const server = createServer(async (req, res) => {
|
|
|
14811
14811
|
reason: "scheduler_owner_refresh",
|
|
14812
14812
|
rebuildWatchers: true,
|
|
14813
14813
|
});
|
|
14814
|
+
const timezoneNormalization =
|
|
14815
|
+
schedulerService.normalizeBrowserRootPortableSchedules({
|
|
14816
|
+
hostTimezone: getHostSystemTimezone(),
|
|
14817
|
+
});
|
|
14814
14818
|
return respond(res, 200, {
|
|
14815
14819
|
status: "scheduler_browser_root_refreshed",
|
|
14816
14820
|
browser_root: projectIndex.root,
|
|
14817
14821
|
project_count: projectIndex.projects.length,
|
|
14818
14822
|
diagnostic_count: projectIndex.diagnostics.length,
|
|
14823
|
+
timezone_normalization: {
|
|
14824
|
+
status: timezoneNormalization.status,
|
|
14825
|
+
host_timezone: timezoneNormalization.host_timezone,
|
|
14826
|
+
project_count: timezoneNormalization.project_count,
|
|
14827
|
+
scheduler_count: timezoneNormalization.scheduler_count,
|
|
14828
|
+
corrected_count:
|
|
14829
|
+
timezoneNormalization.timezone_correction_count,
|
|
14830
|
+
},
|
|
14831
|
+
timezone_correction_count:
|
|
14832
|
+
timezoneNormalization.timezone_correction_count,
|
|
14833
|
+
timezone_corrections: timezoneNormalization.timezone_corrections,
|
|
14834
|
+
timezone_correction_notifications:
|
|
14835
|
+
timezoneNormalization.timezone_correction_notifications,
|
|
14819
14836
|
schedules: schedulerService.listHostSchedulesForDashboard(),
|
|
14820
14837
|
project_registration_issues:
|
|
14821
14838
|
buildBrowserRootProjectRegistrationIssues(),
|