@cequrebackends/cequre-ts 0.13.0 → 0.15.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/dist/adapters/bun/index.js +11 -5
- package/dist/adapters/node/index.js +20 -7
- package/dist/adapters/vercel/index.d.ts +38 -0
- package/dist/adapters/vercel/index.js +341 -0
- package/dist/adapters/vercel/upstash-kv.d.ts +41 -0
- package/dist/adapters/vercel/vercel-cron.d.ts +39 -0
- package/dist/adapters/vercel/vercel-media.d.ts +10 -0
- package/dist/adapters/vercel/vercel-queue.d.ts +36 -0
- package/dist/adapters/vercel/vercel-response.d.ts +4 -0
- package/dist/adapters/vercel/vercel-server.d.ts +24 -0
- package/dist/adapters/vercel/vercel-sse.d.ts +20 -0
- package/dist/adapters/vercel/vercel-storage.d.ts +23 -0
- package/dist/adapters/vercel/vercel-websocket.d.ts +12 -0
- package/dist/{index-kyvy0s1x.js → index-6qbca8zn.js} +4 -2
- package/dist/{index-c3vh32en.js → index-6vf0r9nh.js} +123 -203
- package/dist/index-cpw1bjf1.js +62 -0
- package/dist/{index-17yswtmg.js → index-qzzg2p5r.js} +1 -61
- package/dist/index-z2xccrdm.js +203 -0
- package/dist/index.js +21 -14
- package/dist/shared/core/index.js +3 -2
- package/dist/shared/main/runtime.d.ts +16 -3
- package/dist/shared/utils/kv/types.d.ts +8 -1
- package/package.json +17 -12
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
CequreError
|
|
4
|
+
} from "./index-qzzg2p5r.js";
|
|
5
|
+
import {
|
|
6
|
+
init_logger,
|
|
7
|
+
logger
|
|
8
|
+
} from "./index-6vf0r9nh.js";
|
|
9
|
+
|
|
10
|
+
// shared/utils/queue-manager.ts
|
|
11
|
+
import { Queue as BullQueue, Worker as BullWorker, QueueEvents as BullQueueEvents } from "bullmq";
|
|
12
|
+
import { Effect } from "effect";
|
|
13
|
+
init_logger();
|
|
14
|
+
|
|
15
|
+
class BullMQQueueAdapter {
|
|
16
|
+
queue;
|
|
17
|
+
constructor(queue) {
|
|
18
|
+
this.queue = queue;
|
|
19
|
+
}
|
|
20
|
+
async add(name, data, opts) {
|
|
21
|
+
return this.queue.add(name, data, opts);
|
|
22
|
+
}
|
|
23
|
+
async addBulk(jobs) {
|
|
24
|
+
const bulkJobs = jobs.map((job) => ({
|
|
25
|
+
name: job.name,
|
|
26
|
+
data: job.data,
|
|
27
|
+
opts: job.opts
|
|
28
|
+
}));
|
|
29
|
+
return this.queue.addBulk(bulkJobs);
|
|
30
|
+
}
|
|
31
|
+
async getRepeatableJobs() {
|
|
32
|
+
return this.queue.getRepeatableJobs();
|
|
33
|
+
}
|
|
34
|
+
async removeRepeatableByKey(key) {
|
|
35
|
+
return this.queue.removeRepeatableByKey(key);
|
|
36
|
+
}
|
|
37
|
+
async disconnect() {
|
|
38
|
+
await this.queue.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class CequreQueueManager {
|
|
43
|
+
static instance;
|
|
44
|
+
queues = new Map;
|
|
45
|
+
workers = new Map;
|
|
46
|
+
queueEvents = new Map;
|
|
47
|
+
config;
|
|
48
|
+
closing = false;
|
|
49
|
+
constructor(config) {
|
|
50
|
+
this.config = config;
|
|
51
|
+
if (!this.config.bullmq?.connection) {
|
|
52
|
+
throw CequreError.NotConfigured('CequreQueueManager: bullmq provider requires "bullmq.connection"');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
getBullMQBaseOptions() {
|
|
56
|
+
return {
|
|
57
|
+
...this.config.bullmq.prefix ? { prefix: this.config.bullmq.prefix } : {},
|
|
58
|
+
connection: {
|
|
59
|
+
...this.config.bullmq.connection,
|
|
60
|
+
maxRetriesPerRequest: null
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
static initialize(config) {
|
|
65
|
+
if (!CequreQueueManager.instance) {
|
|
66
|
+
CequreQueueManager.instance = new CequreQueueManager(config);
|
|
67
|
+
}
|
|
68
|
+
return CequreQueueManager.instance;
|
|
69
|
+
}
|
|
70
|
+
static getInstance() {
|
|
71
|
+
if (!CequreQueueManager.instance) {
|
|
72
|
+
throw CequreError.NotConfigured("CequreQueueManager not initialized. Call initialize() first.");
|
|
73
|
+
}
|
|
74
|
+
return CequreQueueManager.instance;
|
|
75
|
+
}
|
|
76
|
+
getQueue(name) {
|
|
77
|
+
const existing = this.queues.get(name);
|
|
78
|
+
if (existing)
|
|
79
|
+
return existing;
|
|
80
|
+
const queue = new BullQueue(name, {
|
|
81
|
+
...this.config.defaultQueueOptions ?? {},
|
|
82
|
+
...this.getBullMQBaseOptions()
|
|
83
|
+
});
|
|
84
|
+
queue.on("error", (err) => {
|
|
85
|
+
if (err?.message?.includes("Connection is closed"))
|
|
86
|
+
return;
|
|
87
|
+
logger.error(err, `[ ${name.toUpperCase()} QUEUE ] Error`);
|
|
88
|
+
});
|
|
89
|
+
const wrapped = new BullMQQueueAdapter(queue);
|
|
90
|
+
this.queues.set(name, wrapped);
|
|
91
|
+
return wrapped;
|
|
92
|
+
}
|
|
93
|
+
async addJob(queueName, jobName, data, opts) {
|
|
94
|
+
const queue = this.getQueue(queueName);
|
|
95
|
+
return queue.add(jobName, data, opts);
|
|
96
|
+
}
|
|
97
|
+
async addBulk(queueName, jobs) {
|
|
98
|
+
const queue = this.getQueue(queueName);
|
|
99
|
+
return queue.addBulk(jobs);
|
|
100
|
+
}
|
|
101
|
+
async registerWorker(queueName, handler, options) {
|
|
102
|
+
const existing = this.workers.get(queueName);
|
|
103
|
+
const queueNameForLogs = `[ ${queueName?.toUpperCase()} WORKER ]`;
|
|
104
|
+
if (existing) {
|
|
105
|
+
logger.warn(`${queueNameForLogs} Replacing existing worker for queue "${queueName}"`);
|
|
106
|
+
try {
|
|
107
|
+
await existing.close();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
logger.error(err, `${queueNameForLogs} Error closing old worker for "${queueName}"`);
|
|
110
|
+
}
|
|
111
|
+
this.workers.delete(queueName);
|
|
112
|
+
}
|
|
113
|
+
const worker = new BullWorker(queueName, async (job) => await handler(job), {
|
|
114
|
+
...this.config.defaultWorkerOptions ?? {},
|
|
115
|
+
...options ?? {},
|
|
116
|
+
...this.getBullMQBaseOptions()
|
|
117
|
+
});
|
|
118
|
+
worker.on("completed", (job) => {
|
|
119
|
+
logger.info(`${queueNameForLogs} Job ${job.id} completed`);
|
|
120
|
+
});
|
|
121
|
+
worker.on("failed", (job, err) => {
|
|
122
|
+
logger.error(err, `${queueNameForLogs} Job ${job?.id ?? "unknown"} failed`);
|
|
123
|
+
});
|
|
124
|
+
worker.on("error", (err) => {
|
|
125
|
+
if (err?.message?.includes("Connection is closed"))
|
|
126
|
+
return;
|
|
127
|
+
logger.error(err, `${queueNameForLogs} Worker error`);
|
|
128
|
+
});
|
|
129
|
+
await worker.waitUntilReady();
|
|
130
|
+
logger.info(`${queueNameForLogs} Worker is ready and listening . . .`);
|
|
131
|
+
const wrapped = {
|
|
132
|
+
close: async () => {
|
|
133
|
+
await worker.close();
|
|
134
|
+
},
|
|
135
|
+
waitUntilReady: async () => {
|
|
136
|
+
await worker.waitUntilReady();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
this.workers.set(queueName, wrapped);
|
|
140
|
+
return wrapped;
|
|
141
|
+
}
|
|
142
|
+
getQueueEvents(name) {
|
|
143
|
+
const existing = this.queueEvents.get(name);
|
|
144
|
+
if (existing)
|
|
145
|
+
return existing;
|
|
146
|
+
const events = new BullQueueEvents(name, {
|
|
147
|
+
...this.getBullMQBaseOptions()
|
|
148
|
+
});
|
|
149
|
+
events.on("error", (err) => {
|
|
150
|
+
if (err?.message?.includes("Connection is closed"))
|
|
151
|
+
return;
|
|
152
|
+
logger.error(err, `[ ${name.toUpperCase()} QUEUE EVENTS ] Error`);
|
|
153
|
+
});
|
|
154
|
+
const wrapped = {
|
|
155
|
+
disconnect: async () => {
|
|
156
|
+
await events.close();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
this.queueEvents.set(name, wrapped);
|
|
160
|
+
return wrapped;
|
|
161
|
+
}
|
|
162
|
+
static _reset() {
|
|
163
|
+
CequreQueueManager.instance = undefined;
|
|
164
|
+
}
|
|
165
|
+
static async _resetAndClose() {
|
|
166
|
+
const existing = CequreQueueManager.instance;
|
|
167
|
+
CequreQueueManager.instance = undefined;
|
|
168
|
+
if (existing) {
|
|
169
|
+
await existing.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async close() {
|
|
173
|
+
if (this.closing)
|
|
174
|
+
return;
|
|
175
|
+
this.closing = true;
|
|
176
|
+
const closeItemEffect = (item, label) => Effect.tryPromise({
|
|
177
|
+
try: async () => {
|
|
178
|
+
if (item.close)
|
|
179
|
+
await item.close();
|
|
180
|
+
else if (item.disconnect)
|
|
181
|
+
await item.disconnect();
|
|
182
|
+
},
|
|
183
|
+
catch: (err) => err
|
|
184
|
+
}).pipe(Effect.catchAll((err) => Effect.sync(() => {
|
|
185
|
+
logger.error(err, `[QueueManager] Error closing ${label}`);
|
|
186
|
+
})));
|
|
187
|
+
const closeGroupEffect = (items, label) => Effect.all(items.map((item) => closeItemEffect(item, label)), { concurrency: "unbounded" });
|
|
188
|
+
const workers = Array.from(this.workers.values());
|
|
189
|
+
const queueEvents = Array.from(this.queueEvents.values());
|
|
190
|
+
const queues = Array.from(this.queues.values());
|
|
191
|
+
await Effect.runPromise(Effect.gen(function* () {
|
|
192
|
+
yield* closeGroupEffect(workers, "worker");
|
|
193
|
+
yield* closeGroupEffect(queueEvents, "queue events");
|
|
194
|
+
yield* closeGroupEffect(queues, "queue");
|
|
195
|
+
}));
|
|
196
|
+
this.workers.clear();
|
|
197
|
+
this.queueEvents.clear();
|
|
198
|
+
this.queues.clear();
|
|
199
|
+
this.closing = false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export { CequreQueueManager };
|
package/dist/index.js
CHANGED
|
@@ -22,34 +22,38 @@ import {
|
|
|
22
22
|
sqlLiteral,
|
|
23
23
|
validateCreate,
|
|
24
24
|
validateUpdate
|
|
25
|
-
} from "./index-
|
|
25
|
+
} from "./index-6qbca8zn.js";
|
|
26
26
|
import {
|
|
27
27
|
CequreStreamMedia,
|
|
28
28
|
methodNotAllowed,
|
|
29
29
|
notFound,
|
|
30
30
|
toResponse
|
|
31
31
|
} from "./index-mfqj7cwr.js";
|
|
32
|
+
import {
|
|
33
|
+
CequreQueueManager
|
|
34
|
+
} from "./index-z2xccrdm.js";
|
|
35
|
+
import {
|
|
36
|
+
CequreError,
|
|
37
|
+
buildErrorResponse,
|
|
38
|
+
extractPathname,
|
|
39
|
+
getConstraintErrorMessage,
|
|
40
|
+
getConstraintErrorStatus,
|
|
41
|
+
parseConstraintError,
|
|
42
|
+
toErrorResponse
|
|
43
|
+
} from "./index-qzzg2p5r.js";
|
|
32
44
|
import {
|
|
33
45
|
CequreErrorLogs,
|
|
34
46
|
CequreKV,
|
|
35
|
-
CequreQueueManager,
|
|
36
47
|
createLogger,
|
|
37
48
|
init_kv,
|
|
38
49
|
init_logger,
|
|
39
50
|
logger
|
|
40
|
-
} from "./index-
|
|
51
|
+
} from "./index-6vf0r9nh.js";
|
|
41
52
|
import {
|
|
42
|
-
CequreError,
|
|
43
53
|
__require,
|
|
44
54
|
__toCommonJS,
|
|
45
|
-
buildErrorResponse,
|
|
46
|
-
extractPathname,
|
|
47
|
-
getConstraintErrorMessage,
|
|
48
|
-
getConstraintErrorStatus,
|
|
49
|
-
parseConstraintError,
|
|
50
|
-
toErrorResponse,
|
|
51
55
|
ulid
|
|
52
|
-
} from "./index-
|
|
56
|
+
} from "./index-cpw1bjf1.js";
|
|
53
57
|
|
|
54
58
|
// shared/main/sse.ts
|
|
55
59
|
var encoder = new TextEncoder;
|
|
@@ -2836,8 +2840,7 @@ class CequreRuntime {
|
|
|
2836
2840
|
return toErrorResponse(err, req, this.adapter);
|
|
2837
2841
|
}
|
|
2838
2842
|
}
|
|
2839
|
-
async
|
|
2840
|
-
if (false) {}
|
|
2843
|
+
async init() {
|
|
2841
2844
|
for (const plugin of this.plugins) {
|
|
2842
2845
|
if (plugin.onInit) {
|
|
2843
2846
|
await plugin.onInit(this);
|
|
@@ -2862,6 +2865,10 @@ class CequreRuntime {
|
|
|
2862
2865
|
}
|
|
2863
2866
|
await this.syncDatabaseSchema();
|
|
2864
2867
|
await this.buildRoutes();
|
|
2868
|
+
}
|
|
2869
|
+
async start(options = {}) {
|
|
2870
|
+
if (false) {}
|
|
2871
|
+
await this.init();
|
|
2865
2872
|
this.queue.startWorker();
|
|
2866
2873
|
if (process.env.CEQURE_CLI_GENERATE) {
|
|
2867
2874
|
const { generateTypeScriptClientSDK: generateTypeScriptClientSDK2 } = (init_sdk(), __toCommonJS(exports_sdk));
|
|
@@ -3076,7 +3083,7 @@ ${r}${b} |_| ${reset}
|
|
|
3076
3083
|
`;
|
|
3077
3084
|
if (!process.env.CEQURE_DEV) {
|
|
3078
3085
|
console.log(banner);
|
|
3079
|
-
} else {
|
|
3086
|
+
} else if (process.env.CEQURE_VITE_MODE !== "1") {
|
|
3080
3087
|
const p = server ? server.port : options.port;
|
|
3081
3088
|
console.log(`\uD83D\uDE80 Cequre API Framework started gracefully on port ${p}`);
|
|
3082
3089
|
}
|
|
@@ -19,10 +19,11 @@ import {
|
|
|
19
19
|
sqlLiteral,
|
|
20
20
|
validateCreate,
|
|
21
21
|
validateUpdate
|
|
22
|
-
} from "../../index-
|
|
22
|
+
} from "../../index-6qbca8zn.js";
|
|
23
|
+
import"../../index-qzzg2p5r.js";
|
|
23
24
|
import {
|
|
24
25
|
ulid
|
|
25
|
-
} from "../../index-
|
|
26
|
+
} from "../../index-cpw1bjf1.js";
|
|
26
27
|
export {
|
|
27
28
|
validateUpdate,
|
|
28
29
|
validateCreate,
|
|
@@ -24,9 +24,9 @@ export interface CequreConfig {
|
|
|
24
24
|
plugins?: CequrePlugin[];
|
|
25
25
|
email?: import("../core/types").EmailConfig;
|
|
26
26
|
streamStore?: DurableStreamStore;
|
|
27
|
-
/** KV store config for rate limiter. Defaults to memory (single-instance). Use redis for multi-instance. */
|
|
27
|
+
/** KV store config for rate limiter. Defaults to memory (single-instance). Use redis for multi-instance. Use upstash for serverless. */
|
|
28
28
|
kv?: {
|
|
29
|
-
driver: "memory" | "sqlite" | "redis";
|
|
29
|
+
driver: "memory" | "sqlite" | "redis" | "upstash";
|
|
30
30
|
redis?: {
|
|
31
31
|
host?: string;
|
|
32
32
|
port?: number;
|
|
@@ -35,13 +35,17 @@ export interface CequreConfig {
|
|
|
35
35
|
sqlite?: {
|
|
36
36
|
path?: string;
|
|
37
37
|
};
|
|
38
|
+
upstash?: {
|
|
39
|
+
url?: string;
|
|
40
|
+
token?: string;
|
|
41
|
+
};
|
|
38
42
|
};
|
|
39
43
|
serverProvider?: import("../core/types").ServerProvider;
|
|
40
44
|
cronProvider?: import("../core/types").CronProvider;
|
|
41
45
|
realtimeProvider?: import("../core/types").RealtimeProvider;
|
|
42
46
|
}
|
|
43
47
|
import { CequreDurableQueue } from "../utils/durable-queue";
|
|
44
|
-
export declare class CequreRuntime<TCollections extends Record<string, any> = Record<string, any
|
|
48
|
+
export declare class CequreRuntime<TCollections extends Record<string, any> = Record<string, any>, TUser = AuthUser> {
|
|
45
49
|
schema: CequreAST;
|
|
46
50
|
adapter: CequreAdapter;
|
|
47
51
|
router: CequreRouter<TCollections>;
|
|
@@ -181,6 +185,15 @@ export declare class CequreRuntime<TCollections extends Record<string, any> = Re
|
|
|
181
185
|
*/
|
|
182
186
|
cron(name: string, schedule: string, handler: (app: CequreRuntime<TCollections>) => unknown): void;
|
|
183
187
|
fetch(req: Request): Promise<Response>;
|
|
188
|
+
/**
|
|
189
|
+
* Initialize the runtime without starting a server. Used by serverless
|
|
190
|
+
* platforms (Vercel, Cloudflare) where the platform handles invocation.
|
|
191
|
+
* Runs: plugin init → adapter connect → schema sync → route building.
|
|
192
|
+
* Does NOT start: server, queue worker, cron scheduler, graceful shutdown.
|
|
193
|
+
*
|
|
194
|
+
* After init(), call `fetch(req)` to handle individual requests.
|
|
195
|
+
*/
|
|
196
|
+
init(): Promise<void>;
|
|
184
197
|
start(options?: {
|
|
185
198
|
port?: number;
|
|
186
199
|
drainTimeoutMs?: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type KVDriverType = "memory" | "sqlite" | "redis";
|
|
1
|
+
export type KVDriverType = "memory" | "sqlite" | "redis" | "upstash";
|
|
2
2
|
export interface KVOptions {
|
|
3
3
|
/** The storage driver to use. Default: "sqlite" */
|
|
4
4
|
driver?: KVDriverType;
|
|
@@ -17,6 +17,13 @@ export interface KVOptions {
|
|
|
17
17
|
/** Redis connection URL. Default: "redis://localhost:6379" */
|
|
18
18
|
url?: string;
|
|
19
19
|
};
|
|
20
|
+
/** Configuration for the Upstash Redis driver (HTTP-based, serverless) */
|
|
21
|
+
upstash?: {
|
|
22
|
+
/** Upstash Redis REST URL */
|
|
23
|
+
url?: string;
|
|
24
|
+
/** Upstash Redis REST token */
|
|
25
|
+
token?: string;
|
|
26
|
+
};
|
|
20
27
|
}
|
|
21
28
|
export interface KVAdapter {
|
|
22
29
|
get<T>(key: string): Promise<T | null> | T | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cequrebackends/cequre-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "The Cequre Universal Runtime Engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
"types": "./dist/adapters/node/index.d.ts",
|
|
27
27
|
"import": "./dist/adapters/node/index.js",
|
|
28
28
|
"default": "./dist/adapters/node/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./adapters/vercel": {
|
|
31
|
+
"types": "./dist/adapters/vercel/index.d.ts",
|
|
32
|
+
"import": "./dist/adapters/vercel/index.js",
|
|
33
|
+
"default": "./dist/adapters/vercel/index.js"
|
|
29
34
|
}
|
|
30
35
|
},
|
|
31
36
|
"files": [
|
|
@@ -40,24 +45,24 @@
|
|
|
40
45
|
"url": "https://github.com/cequrebackends/cequre.git"
|
|
41
46
|
},
|
|
42
47
|
"scripts": {
|
|
43
|
-
"build": "rm -rf dist && bun build ./index.ts ./shared/core/index.ts ./adapters/bun/index.ts ./adapters/node/index.ts --outdir ./dist --target bun --packages external --splitting --format esm && tsc --emitDeclarationOnly"
|
|
48
|
+
"build": "rm -rf dist && bun build ./index.ts ./shared/core/index.ts ./adapters/bun/index.ts ./adapters/node/index.ts ./adapters/vercel/index.ts --outdir ./dist --target bun --packages external --splitting --format esm && tsc --emitDeclarationOnly"
|
|
44
49
|
},
|
|
45
50
|
"dependencies": {
|
|
46
|
-
"@sinclair/typebox": "^0.34.
|
|
47
|
-
"bullmq": "^5.
|
|
48
|
-
"effect": "^3.
|
|
49
|
-
"ioredis": "^5.
|
|
50
|
-
"jose": "^6.2.
|
|
51
|
-
"node-cron": "^
|
|
51
|
+
"@sinclair/typebox": "^0.34.52",
|
|
52
|
+
"bullmq": "^5.81.2",
|
|
53
|
+
"effect": "^3.22.0",
|
|
54
|
+
"ioredis": "^5.11.1",
|
|
55
|
+
"jose": "^6.2.4",
|
|
56
|
+
"node-cron": "^4.6.0",
|
|
52
57
|
"pino": "^10.3.1",
|
|
53
58
|
"pino-pretty": "^13.1.3",
|
|
54
|
-
"ws": "^8.
|
|
59
|
+
"ws": "^8.21.1"
|
|
55
60
|
},
|
|
56
61
|
"devDependencies": {
|
|
57
|
-
"@types/node": "^
|
|
62
|
+
"@types/node": "^26.1.1",
|
|
58
63
|
"@types/node-cron": "^3.0.11",
|
|
59
|
-
"@types/ws": "^8.
|
|
64
|
+
"@types/ws": "^8.18.1",
|
|
60
65
|
"openapi-typescript": "^7.13.0",
|
|
61
|
-
"typescript": "^
|
|
66
|
+
"typescript": "^7.0.2"
|
|
62
67
|
}
|
|
63
68
|
}
|