@inferencesh/sdk 0.8.0 → 0.8.1
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/dist/api/sockets.d.ts +5 -0
- package/dist/api/sockets.js +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/live/schema.d.ts +17 -5
- package/dist/live/schema.js +24 -8
- package/dist/live/session.d.ts +48 -6
- package/dist/live/session.js +70 -24
- package/package.json +1 -1
package/dist/api/sockets.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HttpClient } from '../http/client';
|
|
2
2
|
import type { Response } from '../http/response';
|
|
3
3
|
import { LiveSession, type LiveHandlers, type WebSocketConstructor } from '../live/session';
|
|
4
|
+
import type { JsonSchema } from '../live/schema';
|
|
4
5
|
import { CursorListRequest, CursorListResponse, SocketAccess, SocketDTO, TaskDTO as Task } from '../types';
|
|
5
6
|
import type { TasksAPI } from './tasks';
|
|
6
7
|
/** What identifies the socket to open: the run response (which carries the access), a task, or a task id. */
|
|
@@ -16,6 +17,10 @@ export interface OpenSocketOptions {
|
|
|
16
17
|
watchTask?: boolean;
|
|
17
18
|
/** The WebSocket to dial with; defaults to the runtime's global one. */
|
|
18
19
|
webSocket?: WebSocketConstructor;
|
|
20
|
+
/** The function's input schema: `session.sendField` routes by it. */
|
|
21
|
+
inputSchema?: JsonSchema | null;
|
|
22
|
+
/** The function's output schema: frames arrive at `onUpdate` as fields. */
|
|
23
|
+
outputSchema?: JsonSchema | null;
|
|
19
24
|
}
|
|
20
25
|
/**
|
|
21
26
|
* Sockets API: the duplex connection of a stream task.
|
package/dist/api/sockets.js
CHANGED
|
@@ -54,6 +54,8 @@ export class SocketsAPI {
|
|
|
54
54
|
renew: async () => (await this.access(access.id)).data,
|
|
55
55
|
task: options.watchTask === false ? undefined : this.tasks.watch(task),
|
|
56
56
|
webSocket: options.webSocket,
|
|
57
|
+
inputSchema: options.inputSchema,
|
|
58
|
+
outputSchema: options.outputSchema,
|
|
57
59
|
});
|
|
58
60
|
session.connect();
|
|
59
61
|
return session;
|
package/dist/index.d.ts
CHANGED
|
@@ -25,8 +25,8 @@ export { SearchAPI } from './api/search';
|
|
|
25
25
|
export { ProjectsAPI } from './api/projects';
|
|
26
26
|
export { MCPServersAPI } from './api/mcp-servers';
|
|
27
27
|
export { LiveSession, accessUrl } from './live/session';
|
|
28
|
-
export type { LiveState, LiveEnd, LiveHandlers, LiveSessionOptions, WebSocketLike, WebSocketConstructor } from './live/session';
|
|
29
|
-
export { STREAM_FORMAT, CLEAR_KEY, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, } from './live/schema';
|
|
28
|
+
export type { LiveState, LiveEnd, LiveHandlers, LiveSessionOptions, LiveUpdate, WebSocketLike, WebSocketConstructor } from './live/session';
|
|
29
|
+
export { STREAM_FORMAT, CLEAR_KEY, ERROR_KEY, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, alternativeTag, } from './live/schema';
|
|
30
30
|
export type { JsonSchema, MediaType, PCMFormat, LiveField } from './live/schema';
|
|
31
31
|
export { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
|
|
32
32
|
export type { ClientTool, ClientToolHandler } from './tool-builder';
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ export { ProjectsAPI } from './api/projects';
|
|
|
28
28
|
export { MCPServersAPI } from './api/mcp-servers';
|
|
29
29
|
// Live: the socket of a stream task and the live fields of its schemas
|
|
30
30
|
export { LiveSession, accessUrl } from './live/session';
|
|
31
|
-
export { STREAM_FORMAT, CLEAR_KEY, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, } from './live/schema';
|
|
31
|
+
export { STREAM_FORMAT, CLEAR_KEY, ERROR_KEY, isLiveField, parseMediaType, pcmFormat, splitLiveSchema, binaryLiveField, alternativeLabel, alternativeTag, } from './live/schema';
|
|
32
32
|
// Tool Builder (fluent API)
|
|
33
33
|
export { tool, appTool, agentTool, webhookTool, httpTool, callTool, mcpTool, internalTools, string, number, integer, boolean, enumOf, object, array, optional, } from './tool-builder';
|
|
34
34
|
// Hook Builder (fluent API)
|
package/dist/live/schema.d.ts
CHANGED
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
* JSON Schema type keeps it.
|
|
18
18
|
*/
|
|
19
19
|
export declare const STREAM_FORMAT = "stream";
|
|
20
|
+
/** `{"$clear": "audio"}`: drop what has been buffered of a live output field. */
|
|
21
|
+
export declare const CLEAR_KEY = "$clear";
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* `{"$error": {"field": ..., "message": ...}}`: a refused frame, or anything
|
|
24
|
+
* else the caller should be told went wrong. The stream goes on.
|
|
23
25
|
*/
|
|
24
|
-
export declare const
|
|
26
|
+
export declare const ERROR_KEY = "$error";
|
|
25
27
|
/** The part of JSON Schema these helpers read. */
|
|
26
28
|
export interface JsonSchema {
|
|
27
29
|
$ref?: string;
|
|
@@ -37,6 +39,9 @@ export interface JsonSchema {
|
|
|
37
39
|
required?: string[];
|
|
38
40
|
anyOf?: JsonSchema[];
|
|
39
41
|
oneOf?: JsonSchema[];
|
|
42
|
+
discriminator?: {
|
|
43
|
+
propertyName?: string;
|
|
44
|
+
};
|
|
40
45
|
}
|
|
41
46
|
export declare function isLiveField(schema: JsonSchema | undefined | null): boolean;
|
|
42
47
|
export interface MediaType {
|
|
@@ -65,6 +70,8 @@ export interface LiveField<S extends JsonSchema = JsonSchema> {
|
|
|
65
70
|
* or the single item schema. Empty for a binary field.
|
|
66
71
|
*/
|
|
67
72
|
alternatives: S[];
|
|
73
|
+
/** The property that tells the alternatives apart, when the schema names one. */
|
|
74
|
+
discriminator?: string;
|
|
68
75
|
}
|
|
69
76
|
/**
|
|
70
77
|
* Splits a function schema into what a form renders (the ordinary
|
|
@@ -76,5 +83,10 @@ export declare function splitLiveSchema<S extends JsonSchema>(schema: S | undefi
|
|
|
76
83
|
};
|
|
77
84
|
/** The schema's one binary live field. A binary frame carries no field name. */
|
|
78
85
|
export declare function binaryLiveField<S extends JsonSchema>(live: LiveField<S>[]): LiveField<S> | null;
|
|
79
|
-
/**
|
|
80
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The property whose constant names this alternative: the schema's
|
|
88
|
+
* discriminator, else `type`, else the first property with a constant.
|
|
89
|
+
*/
|
|
90
|
+
export declare function alternativeTag(schema: JsonSchema, discriminator?: string): string | undefined;
|
|
91
|
+
/** A label for one alternative of a JSON live field: the constant that tags it, else its title. */
|
|
92
|
+
export declare function alternativeLabel(schema: JsonSchema, index: number, discriminator?: string): string;
|
package/dist/live/schema.js
CHANGED
|
@@ -17,11 +17,15 @@
|
|
|
17
17
|
* JSON Schema type keeps it.
|
|
18
18
|
*/
|
|
19
19
|
export const STREAM_FORMAT = 'stream';
|
|
20
|
+
// Control frames. Reserved keys start with `$`, which no field name can, so a
|
|
21
|
+
// control frame is never mistaken for an output field.
|
|
22
|
+
/** `{"$clear": "audio"}`: drop what has been buffered of a live output field. */
|
|
23
|
+
export const CLEAR_KEY = '$clear';
|
|
20
24
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
25
|
+
* `{"$error": {"field": ..., "message": ...}}`: a refused frame, or anything
|
|
26
|
+
* else the caller should be told went wrong. The stream goes on.
|
|
23
27
|
*/
|
|
24
|
-
export const
|
|
28
|
+
export const ERROR_KEY = '$error';
|
|
25
29
|
export function isLiveField(schema) {
|
|
26
30
|
return !!schema && schema.format === STREAM_FORMAT;
|
|
27
31
|
}
|
|
@@ -99,6 +103,7 @@ export function splitLiveSchema(schema) {
|
|
|
99
103
|
binary,
|
|
100
104
|
media: binary ? parseMediaType(resolvedItems.contentMediaType) : null,
|
|
101
105
|
alternatives: binary ? [] : itemAlternatives(items, schema),
|
|
106
|
+
discriminator: binary ? undefined : resolvedItems.discriminator?.propertyName,
|
|
102
107
|
});
|
|
103
108
|
}
|
|
104
109
|
return {
|
|
@@ -110,10 +115,21 @@ export function splitLiveSchema(schema) {
|
|
|
110
115
|
export function binaryLiveField(live) {
|
|
111
116
|
return live.find((field) => field.binary) ?? null;
|
|
112
117
|
}
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The property whose constant names this alternative: the schema's
|
|
120
|
+
* discriminator, else `type`, else the first property with a constant.
|
|
121
|
+
*/
|
|
122
|
+
export function alternativeTag(schema, discriminator) {
|
|
123
|
+
const constants = Object.entries(schema.properties ?? {})
|
|
124
|
+
.filter(([, property]) => property && 'const' in property)
|
|
125
|
+
.map(([key]) => key);
|
|
126
|
+
return [discriminator, 'type', ...constants].find((key) => !!key && constants.includes(key));
|
|
127
|
+
}
|
|
128
|
+
/** A label for one alternative of a JSON live field: the constant that tags it, else its title. */
|
|
129
|
+
export function alternativeLabel(schema, index, discriminator) {
|
|
130
|
+
const tag = alternativeTag(schema, discriminator);
|
|
131
|
+
const value = tag ? schema.properties?.[tag]?.const : undefined;
|
|
132
|
+
if (typeof value === 'string')
|
|
133
|
+
return value;
|
|
118
134
|
return schema.title ?? `option ${index + 1}`;
|
|
119
135
|
}
|
package/dist/live/session.d.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* name.
|
|
12
12
|
*/
|
|
13
13
|
import type { SocketAccess } from '../types';
|
|
14
|
+
import type { TaskWatch as TasksWatch } from '../api/tasks';
|
|
15
|
+
import { type JsonSchema } from './schema';
|
|
14
16
|
export type LiveState = 'connecting' | 'waiting' | 'live' | 'ended';
|
|
15
17
|
export interface LiveEnd {
|
|
16
18
|
/** The WebSocket close code, or 1006 when the session ended without one. */
|
|
@@ -33,16 +35,37 @@ export interface LiveHandlers {
|
|
|
33
35
|
* handler the control frame reaches onPatch as `{"$clear": field}`.
|
|
34
36
|
*/
|
|
35
37
|
onClear?: (field: string) => void;
|
|
38
|
+
/**
|
|
39
|
+
* The app refused a frame, or has something to report, and goes on:
|
|
40
|
+
* `{"$error": {field, message}}`. Apps on SDKs before 0.7.2 / 0.9.2 send it
|
|
41
|
+
* as `{"error": ...}`, which counts too unless the output has an `error`
|
|
42
|
+
* field. Without this handler it stays in the patch.
|
|
43
|
+
*/
|
|
44
|
+
onError?: (field: string | null, message: string) => void;
|
|
45
|
+
/** A text frame that is not a JSON object: text the app sent as text. */
|
|
46
|
+
onText?: (text: string) => void;
|
|
47
|
+
/**
|
|
48
|
+
* With `outputSchema`, each frame mapped to its output field: an item of a
|
|
49
|
+
* live field (an ArrayBuffer for the binary one) or a new value of an
|
|
50
|
+
* ordinary field. Takes the place of onBinary and onPatch where those are
|
|
51
|
+
* not set.
|
|
52
|
+
*/
|
|
53
|
+
onUpdate?: (update: LiveUpdate) => void;
|
|
54
|
+
}
|
|
55
|
+
/** One thing the app sent, mapped to its output field. */
|
|
56
|
+
export interface LiveUpdate {
|
|
57
|
+
field: string;
|
|
58
|
+
value: unknown;
|
|
36
59
|
}
|
|
37
60
|
/**
|
|
38
|
-
* Follows the task the socket belongs to
|
|
39
|
-
*
|
|
40
|
-
* leaves `done` pending.
|
|
61
|
+
* Follows the task the socket belongs to (what `client.tasks.watch` returns):
|
|
62
|
+
* `done` settles when the task ends, rejected when it failed or was
|
|
63
|
+
* cancelled; `stop` ends the watch early and leaves `done` pending. Only
|
|
64
|
+
* settling matters here, so any promise will do.
|
|
41
65
|
*/
|
|
42
|
-
export
|
|
66
|
+
export type TaskWatch = Pick<TasksWatch, 'stop'> & {
|
|
43
67
|
done: Promise<unknown>;
|
|
44
|
-
|
|
45
|
-
}
|
|
68
|
+
};
|
|
46
69
|
/** The part of the WebSocket API the session uses; browsers, Node 22+ and `ws` all provide it. */
|
|
47
70
|
export interface WebSocketLike {
|
|
48
71
|
binaryType: string;
|
|
@@ -72,6 +95,10 @@ export interface LiveSessionOptions {
|
|
|
72
95
|
task?: TaskWatch;
|
|
73
96
|
/** The WebSocket to dial with; defaults to the runtime's global one. */
|
|
74
97
|
webSocket?: WebSocketConstructor;
|
|
98
|
+
/** The function's input schema: `sendField` routes by it. */
|
|
99
|
+
inputSchema?: JsonSchema | null;
|
|
100
|
+
/** The function's output schema: frames arrive at onUpdate as fields. */
|
|
101
|
+
outputSchema?: JsonSchema | null;
|
|
75
102
|
}
|
|
76
103
|
/**
|
|
77
104
|
* Where to dial. A browser cannot set headers on a WebSocket, so the
|
|
@@ -88,6 +115,11 @@ export declare class LiveSession {
|
|
|
88
115
|
private readonly renew;
|
|
89
116
|
private readonly task;
|
|
90
117
|
private readonly WebSocket;
|
|
118
|
+
private readonly mapped;
|
|
119
|
+
private readonly outputBinary;
|
|
120
|
+
private readonly outputFields;
|
|
121
|
+
private readonly inputKnown;
|
|
122
|
+
private readonly inputBinary;
|
|
91
123
|
private resolveEnded;
|
|
92
124
|
/** Settles when the session has ended, however it ended. */
|
|
93
125
|
readonly ended: Promise<LiveEnd>;
|
|
@@ -96,6 +128,10 @@ export declare class LiveSession {
|
|
|
96
128
|
get isOpen(): boolean;
|
|
97
129
|
/** Dials the relay. The session reports its progress through onState. */
|
|
98
130
|
connect(): void;
|
|
131
|
+
private deliverText;
|
|
132
|
+
/** `{"error": {"message": ...}}` from an app on an older SDK, unless the output has an `error` field. */
|
|
133
|
+
private legacyError;
|
|
134
|
+
private deliverBinary;
|
|
99
135
|
private watched;
|
|
100
136
|
private watchTask;
|
|
101
137
|
private redial;
|
|
@@ -104,6 +140,12 @@ export declare class LiveSession {
|
|
|
104
140
|
private closeSocket;
|
|
105
141
|
/** One item of the input's binary live field. */
|
|
106
142
|
sendBinary(data: ArrayBuffer | ArrayBufferView): void;
|
|
143
|
+
/**
|
|
144
|
+
* One item of an input live field, or a new value of an ordinary one: a
|
|
145
|
+
* binary frame for the binary live field, a JSON frame otherwise. Needs
|
|
146
|
+
* `inputSchema`.
|
|
147
|
+
*/
|
|
148
|
+
sendField(field: string, value: unknown): void;
|
|
107
149
|
/** A partial input object keyed by field name. */
|
|
108
150
|
sendPatch(patch: Record<string, unknown>): void;
|
|
109
151
|
/** Ends the stream; the function returns and the task completes. */
|
package/dist/live/session.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLEAR_KEY } from './schema';
|
|
1
|
+
import { binaryLiveField, CLEAR_KEY, ERROR_KEY, splitLiveSchema } from './schema';
|
|
2
2
|
const WS_OPEN = 1;
|
|
3
3
|
// 1012: the relay is restarting and closed an end that still waited for its
|
|
4
4
|
// peer. 1013: the peer did not come in time. Both mean "dial again" while the
|
|
@@ -32,6 +32,11 @@ export class LiveSession {
|
|
|
32
32
|
this.renew = options.renew;
|
|
33
33
|
this.task = options.task;
|
|
34
34
|
this.WebSocket = options.webSocket ?? globalWebSocket();
|
|
35
|
+
this.mapped = !!options.outputSchema;
|
|
36
|
+
this.outputBinary = binaryLiveField(splitLiveSchema(options.outputSchema).live)?.key;
|
|
37
|
+
this.outputFields = new Set(Object.keys(options.outputSchema?.properties ?? {}));
|
|
38
|
+
this.inputKnown = !!options.inputSchema;
|
|
39
|
+
this.inputBinary = binaryLiveField(splitLiveSchema(options.inputSchema).live)?.key;
|
|
35
40
|
this.ended = new Promise((resolve) => {
|
|
36
41
|
this.resolveEnded = resolve;
|
|
37
42
|
});
|
|
@@ -55,29 +60,10 @@ export class LiveSession {
|
|
|
55
60
|
this.setState('live');
|
|
56
61
|
this.task?.stop(); // the app is there; the task's fate now shows on the socket
|
|
57
62
|
}
|
|
58
|
-
if (typeof event.data === 'string')
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
patch = { text: event.data };
|
|
65
|
-
}
|
|
66
|
-
if (patch && typeof patch === 'object' && !Array.isArray(patch)) {
|
|
67
|
-
const record = patch;
|
|
68
|
-
const clear = record[CLEAR_KEY];
|
|
69
|
-
if (this.handlers.onClear && typeof clear === 'string') {
|
|
70
|
-
this.handlers.onClear(clear);
|
|
71
|
-
delete record[CLEAR_KEY];
|
|
72
|
-
if (Object.keys(record).length === 0)
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
this.handlers.onPatch?.(record);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
this.handlers.onBinary?.(event.data);
|
|
80
|
-
}
|
|
63
|
+
if (typeof event.data === 'string')
|
|
64
|
+
this.deliverText(event.data);
|
|
65
|
+
else
|
|
66
|
+
this.deliverBinary(event.data);
|
|
81
67
|
};
|
|
82
68
|
ws.onclose = (event) => {
|
|
83
69
|
if (this.ws !== ws)
|
|
@@ -92,6 +78,53 @@ export class LiveSession {
|
|
|
92
78
|
this.end({ code: event.code, reason: event.reason, byCaller: this.closedByCaller, taskEnded: false });
|
|
93
79
|
};
|
|
94
80
|
}
|
|
81
|
+
deliverText(text) {
|
|
82
|
+
let patch;
|
|
83
|
+
try {
|
|
84
|
+
patch = JSON.parse(text);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
patch = undefined;
|
|
88
|
+
}
|
|
89
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
90
|
+
this.handlers.onText?.(text); // text the app sent as text
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const record = patch;
|
|
94
|
+
const arrivedEmpty = Object.keys(record).length === 0;
|
|
95
|
+
const { onClear, onError } = this.handlers;
|
|
96
|
+
if (onClear && typeof record[CLEAR_KEY] === 'string') {
|
|
97
|
+
onClear(record[CLEAR_KEY]);
|
|
98
|
+
delete record[CLEAR_KEY];
|
|
99
|
+
}
|
|
100
|
+
if (onError) {
|
|
101
|
+
const key = ERROR_KEY in record ? ERROR_KEY : this.legacyError(record) ? 'error' : undefined;
|
|
102
|
+
if (key) {
|
|
103
|
+
const err = record[key];
|
|
104
|
+
delete record[key];
|
|
105
|
+
const { field = null, message } = (err && typeof err === 'object' ? err : { message: String(err) });
|
|
106
|
+
onError(field, typeof message === 'string' ? message : JSON.stringify(err));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!arrivedEmpty && Object.keys(record).length === 0)
|
|
110
|
+
return; // it was only control frames
|
|
111
|
+
if (this.handlers.onPatch)
|
|
112
|
+
this.handlers.onPatch(record);
|
|
113
|
+
else if (this.mapped)
|
|
114
|
+
for (const [field, value] of Object.entries(record))
|
|
115
|
+
this.handlers.onUpdate?.({ field, value });
|
|
116
|
+
}
|
|
117
|
+
/** `{"error": {"message": ...}}` from an app on an older SDK, unless the output has an `error` field. */
|
|
118
|
+
legacyError(record) {
|
|
119
|
+
const err = record.error;
|
|
120
|
+
return !!err && typeof err === 'object' && 'message' in err && !this.outputFields.has('error');
|
|
121
|
+
}
|
|
122
|
+
deliverBinary(data) {
|
|
123
|
+
if (this.handlers.onBinary)
|
|
124
|
+
this.handlers.onBinary(data);
|
|
125
|
+
else if (this.mapped && this.outputBinary)
|
|
126
|
+
this.handlers.onUpdate?.({ field: this.outputBinary, value: data });
|
|
127
|
+
}
|
|
95
128
|
watchTask() {
|
|
96
129
|
if (!this.task || this.watched)
|
|
97
130
|
return;
|
|
@@ -136,6 +169,19 @@ export class LiveSession {
|
|
|
136
169
|
if (this.isOpen)
|
|
137
170
|
this.ws.send(data);
|
|
138
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* One item of an input live field, or a new value of an ordinary one: a
|
|
174
|
+
* binary frame for the binary live field, a JSON frame otherwise. Needs
|
|
175
|
+
* `inputSchema`.
|
|
176
|
+
*/
|
|
177
|
+
sendField(field, value) {
|
|
178
|
+
if (!this.inputKnown)
|
|
179
|
+
throw new Error("sendField needs the function's inputSchema");
|
|
180
|
+
if (field === this.inputBinary)
|
|
181
|
+
this.sendBinary(value);
|
|
182
|
+
else
|
|
183
|
+
this.sendPatch({ [field]: value });
|
|
184
|
+
}
|
|
139
185
|
/** A partial input object keyed by field name. */
|
|
140
186
|
sendPatch(patch) {
|
|
141
187
|
if (this.isOpen)
|