@lotargo/memory_plugin 1.1.5 → 1.1.7

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.
@@ -1,303 +1,303 @@
1
- import { readFileSync, writeFileSync, existsSync, readdirSync, rmSync, statSync } from "node:fs";
2
- import { gzipSync, gunzipSync } from "node:zlib";
3
- import { join } from "node:path";
4
- import { getDatabase, BLOBS_DIR } from "../db/database.js";
5
- import { readBlob, saveBlob } from "../storage/blob_store.js";
6
- import { ensureExportsDir } from "../ingest/exporter.js";
7
-
8
- export function listAvailableSnapshots() {
9
- const exportsDir = ensureExportsDir();
10
- if (!existsSync(exportsDir)) return [];
11
-
12
- const files = readdirSync(exportsDir);
13
- const snapshotFiles = [];
14
-
15
- for (const f of files) {
16
- if (f.endsWith(".json") || f.endsWith(".json.gz")) {
17
- const fullPath = join(exportsDir, f);
18
- try {
19
- const st = statSync(fullPath);
20
- if (st.isFile()) {
21
- snapshotFiles.push({
22
- name: f,
23
- path: fullPath,
24
- sizeBytes: st.size,
25
- sizeMB: Number((st.size / (1024 * 1024)).toFixed(2)),
26
- mtime: st.mtime,
27
- dateStr: st.mtime.toISOString().substring(0, 16).replace("T", " "),
28
- });
29
- }
30
- } catch {}
31
- }
32
- }
33
-
34
- snapshotFiles.sort((a, b) => b.mtime - a.mtime);
35
- return snapshotFiles;
36
- }
37
-
38
- export async function exportSnapshot({ customDb = null, customBlobDir = BLOBS_DIR, outputPath = null } = {}) {
39
- const db = customDb || getDatabase();
40
-
41
- const documents = db.prepare("SELECT * FROM documents").all();
42
- const sections = db.prepare("SELECT * FROM sections").all();
43
- const mediumChunks = db.prepare("SELECT * FROM medium_chunks").all();
44
- const rawMicroChunks = db.prepare("SELECT * FROM micro_chunks").all();
45
- const graphEdges = db.prepare("SELECT * FROM graph_edges").all();
46
-
47
- const microChunks = rawMicroChunks.map((mc) => {
48
- let vecBase64 = "";
49
- if (mc.vector) {
50
- const buf = Buffer.isBuffer(mc.vector) ? mc.vector : Buffer.from(mc.vector);
51
- vecBase64 = buf.toString("base64");
52
- }
53
- return {
54
- ...mc,
55
- vector: vecBase64,
56
- };
57
- });
58
-
59
- const uniqueBlobHashes = [...new Set(documents.map((d) => d.blob_hash).filter(Boolean))];
60
- const blobs = [];
61
- for (const hash of uniqueBlobHashes) {
62
- try {
63
- const content = await readBlob(hash, customBlobDir);
64
- blobs.push({ hash, content });
65
- } catch {
66
- // Blob missing, ignore
67
- }
68
- }
69
-
70
- const snapshot = {
71
- version: 2,
72
- created_at: new Date().toISOString(),
73
- documents,
74
- sections,
75
- medium_chunks: mediumChunks,
76
- micro_chunks: microChunks,
77
- graph_edges: graphEdges,
78
- blobs,
79
- };
80
-
81
- const jsonStr = JSON.stringify(snapshot, null, 2);
82
- const targetPath = outputPath || join(ensureExportsDir(), `rag_snapshot_${Date.now()}.json.gz`);
83
-
84
- if (targetPath.endsWith(".gz")) {
85
- const gzipped = gzipSync(Buffer.from(jsonStr, "utf-8"));
86
- writeFileSync(targetPath, gzipped);
87
- } else {
88
- writeFileSync(targetPath, jsonStr, "utf-8");
89
- }
90
-
91
- return { snapshot, outputPath: targetPath };
92
- }
93
-
94
- export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DIR, snapshotPathOrData } = {}) {
95
- const db = customDb || getDatabase();
96
- let snapshot;
97
-
98
- if (typeof snapshotPathOrData === "string") {
99
- if (!existsSync(snapshotPathOrData)) {
100
- throw new Error(`Snapshot file not found: ${snapshotPathOrData}`);
101
- }
102
- const raw = readFileSync(snapshotPathOrData);
103
- if (snapshotPathOrData.endsWith(".gz")) {
104
- const decompressed = gunzipSync(raw);
105
- snapshot = JSON.parse(decompressed.toString("utf-8"));
106
- } else {
107
- snapshot = JSON.parse(raw.toString("utf-8"));
108
- }
109
- } else {
110
- snapshot = snapshotPathOrData;
111
- }
112
-
113
- if (!snapshot || !snapshot.version) {
114
- throw new Error("Invalid snapshot format");
115
- }
116
-
117
- // 1. Restore Blobs
118
- let blobCount = 0;
119
- if (Array.isArray(snapshot.blobs)) {
120
- for (const b of snapshot.blobs) {
121
- if (b.content) {
122
- await saveBlob(b.content, customBlobDir);
123
- blobCount++;
124
- }
125
- }
126
- }
127
-
128
- // 2. Database Inserts
129
- const insertDoc = db.prepare(`
130
- INSERT INTO documents (id, path, blob_hash, title, checksum, toc_json, metadata_json, created_at, updated_at)
131
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
132
- ON CONFLICT(id) DO UPDATE SET
133
- path=excluded.path,
134
- blob_hash=excluded.blob_hash,
135
- title=excluded.title,
136
- checksum=excluded.checksum,
137
- toc_json=excluded.toc_json,
138
- metadata_json=excluded.metadata_json,
139
- updated_at=excluded.updated_at
140
- `);
141
-
142
- const insertSection = db.prepare(`
143
- INSERT INTO sections (id, doc_id, heading, breadcrumbs, content, token_count)
144
- VALUES (?, ?, ?, ?, ?, ?)
145
- ON CONFLICT(id) DO UPDATE SET
146
- heading=excluded.heading,
147
- breadcrumbs=excluded.breadcrumbs,
148
- content=excluded.content,
149
- token_count=excluded.token_count
150
- `);
151
-
152
- const insertMedium = db.prepare(`
153
- INSERT INTO medium_chunks (id, section_id, doc_id, content, block_type, token_count, created_at)
154
- VALUES (?, ?, ?, ?, ?, ?, ?)
155
- ON CONFLICT(id) DO UPDATE SET
156
- content=excluded.content,
157
- block_type=excluded.block_type,
158
- token_count=excluded.token_count
159
- `);
160
-
161
- const insertChunk = db.prepare(`
162
- INSERT INTO micro_chunks (id, section_id, doc_id, content, vector, token_count, medium_id)
163
- VALUES (?, ?, ?, ?, ?, ?, ?)
164
- ON CONFLICT(id) DO UPDATE SET
165
- content=excluded.content,
166
- vector=excluded.vector,
167
- token_count=excluded.token_count,
168
- medium_id=excluded.medium_id
169
- `);
170
-
171
- const insertFts = db.prepare(`
172
- INSERT INTO micro_chunks_fts (id, content, breadcrumbs)
173
- VALUES (?, ?, ?)
174
- `);
175
-
176
- const deleteFts = db.prepare("DELETE FROM micro_chunks_fts WHERE id = ?");
177
-
178
- const insertEdge = db.prepare(`
179
- INSERT INTO graph_edges (source_id, target_id, relation_type)
180
- VALUES (?, ?, ?)
181
- ON CONFLICT(source_id, target_id, relation_type) DO NOTHING
182
- `);
183
-
184
- db.exec("BEGIN IMMEDIATE;");
185
- try {
186
- if (Array.isArray(snapshot.documents)) {
187
- for (const d of snapshot.documents) {
188
- insertDoc.run(
189
- d.id,
190
- d.path,
191
- d.blob_hash,
192
- d.title,
193
- d.checksum,
194
- d.toc_json,
195
- d.metadata_json,
196
- d.created_at,
197
- d.updated_at
198
- );
199
- }
200
- }
201
-
202
- if (Array.isArray(snapshot.sections)) {
203
- for (const s of snapshot.sections) {
204
- insertSection.run(s.id, s.doc_id, s.heading, s.breadcrumbs, s.content, s.token_count);
205
- }
206
- }
207
-
208
- if (Array.isArray(snapshot.medium_chunks)) {
209
- for (const m of snapshot.medium_chunks) {
210
- insertMedium.run(m.id, m.section_id, m.doc_id, m.content, m.block_type, m.token_count, m.created_at || Date.now());
211
- }
212
- }
213
-
214
- if (Array.isArray(snapshot.micro_chunks)) {
215
- for (const mc of snapshot.micro_chunks) {
216
- let vecBuf = Buffer.alloc(0);
217
- if (mc.vector) {
218
- vecBuf = Buffer.from(mc.vector, "base64");
219
- }
220
- insertChunk.run(mc.id, mc.section_id, mc.doc_id, mc.content, vecBuf, mc.token_count, mc.medium_id || null);
221
-
222
- try {
223
- deleteFts.run(mc.id);
224
- } catch {}
225
- insertFts.run(mc.id, mc.content, mc.breadcrumbs || "");
226
- }
227
- }
228
-
229
- if (Array.isArray(snapshot.graph_edges)) {
230
- for (const e of snapshot.graph_edges) {
231
- insertEdge.run(e.source_id, e.target_id, e.relation_type);
232
- }
233
- }
234
- db.exec("COMMIT;");
235
- } catch (err) {
236
- db.exec("ROLLBACK;");
237
- throw err;
238
- }
239
-
240
- return {
241
- documents: snapshot.documents ? snapshot.documents.length : 0,
242
- sections: snapshot.sections ? snapshot.sections.length : 0,
243
- medium_chunks: snapshot.medium_chunks ? snapshot.medium_chunks.length : 0,
244
- micro_chunks: snapshot.micro_chunks ? snapshot.micro_chunks.length : 0,
245
- graph_edges: snapshot.graph_edges ? snapshot.graph_edges.length : 0,
246
- blobs: blobCount,
247
- };
248
- }
249
-
250
- export function hardResetDatabase({ customDb = null, customBlobDir = BLOBS_DIR } = {}) {
251
- const db = customDb || getDatabase();
252
-
253
- let docCount = 0;
254
- let chunkCount = 0;
255
- let blobCount = 0;
256
-
257
- try {
258
- docCount = db.prepare("SELECT COUNT(*) as cnt FROM documents").get().cnt;
259
- chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get().cnt;
260
- } catch {}
261
-
262
- db.exec("BEGIN IMMEDIATE;");
263
- try {
264
- try { db.exec("DELETE FROM micro_chunks_fts;"); } catch {}
265
- try { db.exec("DELETE FROM micro_chunks;"); } catch {}
266
- try { db.exec("DELETE FROM medium_chunks;"); } catch {}
267
- try { db.exec("DELETE FROM sections;"); } catch {}
268
- try { db.exec("DELETE FROM graph_edges;"); } catch {}
269
- try { db.exec("DELETE FROM knowledge_links;"); } catch {}
270
- try { db.exec("DELETE FROM documents;"); } catch {}
271
- db.exec("COMMIT;");
272
- } catch (err) {
273
- db.exec("ROLLBACK;");
274
- throw err;
275
- }
276
-
277
- // Clear Blobs Directory
278
- if (existsSync(customBlobDir)) {
279
- try {
280
- const files = readdirSync(customBlobDir, { recursive: true });
281
- for (const f of files) {
282
- const fullPath = join(customBlobDir, f);
283
- try {
284
- const st = statSync(fullPath);
285
- if (st.isFile()) {
286
- rmSync(fullPath, { force: true });
287
- blobCount++;
288
- }
289
- } catch {}
290
- }
291
- } catch {}
292
- }
293
-
294
- try {
295
- db.exec("VACUUM;");
296
- } catch {}
297
-
298
- return {
299
- purgedDocuments: docCount,
300
- purgedChunks: chunkCount,
301
- purgedBlobs: blobCount,
302
- };
303
- }
1
+ import { readFileSync, writeFileSync, existsSync, readdirSync, rmSync, statSync } from "node:fs";
2
+ import { gzipSync, gunzipSync } from "node:zlib";
3
+ import { join } from "node:path";
4
+ import { getDatabase, BLOBS_DIR } from "../db/database.js";
5
+ import { readBlob, saveBlob } from "../storage/blob_store.js";
6
+ import { ensureExportsDir } from "../ingest/exporter.js";
7
+
8
+ export function listAvailableSnapshots() {
9
+ const exportsDir = ensureExportsDir();
10
+ if (!existsSync(exportsDir)) return [];
11
+
12
+ const files = readdirSync(exportsDir);
13
+ const snapshotFiles = [];
14
+
15
+ for (const f of files) {
16
+ if (f.endsWith(".json") || f.endsWith(".json.gz")) {
17
+ const fullPath = join(exportsDir, f);
18
+ try {
19
+ const st = statSync(fullPath);
20
+ if (st.isFile()) {
21
+ snapshotFiles.push({
22
+ name: f,
23
+ path: fullPath,
24
+ sizeBytes: st.size,
25
+ sizeMB: Number((st.size / (1024 * 1024)).toFixed(2)),
26
+ mtime: st.mtime,
27
+ dateStr: st.mtime.toISOString().substring(0, 16).replace("T", " "),
28
+ });
29
+ }
30
+ } catch {}
31
+ }
32
+ }
33
+
34
+ snapshotFiles.sort((a, b) => b.mtime - a.mtime);
35
+ return snapshotFiles;
36
+ }
37
+
38
+ export async function exportSnapshot({ customDb = null, customBlobDir = BLOBS_DIR, outputPath = null } = {}) {
39
+ const db = customDb || getDatabase();
40
+
41
+ const documents = db.prepare("SELECT * FROM documents").all();
42
+ const sections = db.prepare("SELECT * FROM sections").all();
43
+ const mediumChunks = db.prepare("SELECT * FROM medium_chunks").all();
44
+ const rawMicroChunks = db.prepare("SELECT * FROM micro_chunks").all();
45
+ const graphEdges = db.prepare("SELECT * FROM graph_edges").all();
46
+
47
+ const microChunks = rawMicroChunks.map((mc) => {
48
+ let vecBase64 = "";
49
+ if (mc.vector) {
50
+ const buf = Buffer.isBuffer(mc.vector) ? mc.vector : Buffer.from(mc.vector);
51
+ vecBase64 = buf.toString("base64");
52
+ }
53
+ return {
54
+ ...mc,
55
+ vector: vecBase64,
56
+ };
57
+ });
58
+
59
+ const uniqueBlobHashes = [...new Set(documents.map((d) => d.blob_hash).filter(Boolean))];
60
+ const blobs = [];
61
+ for (const hash of uniqueBlobHashes) {
62
+ try {
63
+ const content = await readBlob(hash, customBlobDir);
64
+ blobs.push({ hash, content });
65
+ } catch {
66
+ // Blob missing, ignore
67
+ }
68
+ }
69
+
70
+ const snapshot = {
71
+ version: 2,
72
+ created_at: new Date().toISOString(),
73
+ documents,
74
+ sections,
75
+ medium_chunks: mediumChunks,
76
+ micro_chunks: microChunks,
77
+ graph_edges: graphEdges,
78
+ blobs,
79
+ };
80
+
81
+ const jsonStr = JSON.stringify(snapshot, null, 2);
82
+ const targetPath = outputPath || join(ensureExportsDir(), `rag_snapshot_${Date.now()}.json.gz`);
83
+
84
+ if (targetPath.endsWith(".gz")) {
85
+ const gzipped = gzipSync(Buffer.from(jsonStr, "utf-8"));
86
+ writeFileSync(targetPath, gzipped);
87
+ } else {
88
+ writeFileSync(targetPath, jsonStr, "utf-8");
89
+ }
90
+
91
+ return { snapshot, outputPath: targetPath };
92
+ }
93
+
94
+ export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DIR, snapshotPathOrData } = {}) {
95
+ const db = customDb || getDatabase();
96
+ let snapshot;
97
+
98
+ if (typeof snapshotPathOrData === "string") {
99
+ if (!existsSync(snapshotPathOrData)) {
100
+ throw new Error(`Snapshot file not found: ${snapshotPathOrData}`);
101
+ }
102
+ const raw = readFileSync(snapshotPathOrData);
103
+ if (snapshotPathOrData.endsWith(".gz")) {
104
+ const decompressed = gunzipSync(raw);
105
+ snapshot = JSON.parse(decompressed.toString("utf-8"));
106
+ } else {
107
+ snapshot = JSON.parse(raw.toString("utf-8"));
108
+ }
109
+ } else {
110
+ snapshot = snapshotPathOrData;
111
+ }
112
+
113
+ if (!snapshot || !snapshot.version) {
114
+ throw new Error("Invalid snapshot format");
115
+ }
116
+
117
+ // 1. Restore Blobs
118
+ let blobCount = 0;
119
+ if (Array.isArray(snapshot.blobs)) {
120
+ for (const b of snapshot.blobs) {
121
+ if (b.content) {
122
+ await saveBlob(b.content, customBlobDir);
123
+ blobCount++;
124
+ }
125
+ }
126
+ }
127
+
128
+ // 2. Database Inserts
129
+ const insertDoc = db.prepare(`
130
+ INSERT INTO documents (id, path, blob_hash, title, checksum, toc_json, metadata_json, created_at, updated_at)
131
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
132
+ ON CONFLICT(id) DO UPDATE SET
133
+ path=excluded.path,
134
+ blob_hash=excluded.blob_hash,
135
+ title=excluded.title,
136
+ checksum=excluded.checksum,
137
+ toc_json=excluded.toc_json,
138
+ metadata_json=excluded.metadata_json,
139
+ updated_at=excluded.updated_at
140
+ `);
141
+
142
+ const insertSection = db.prepare(`
143
+ INSERT INTO sections (id, doc_id, heading, breadcrumbs, content, token_count)
144
+ VALUES (?, ?, ?, ?, ?, ?)
145
+ ON CONFLICT(id) DO UPDATE SET
146
+ heading=excluded.heading,
147
+ breadcrumbs=excluded.breadcrumbs,
148
+ content=excluded.content,
149
+ token_count=excluded.token_count
150
+ `);
151
+
152
+ const insertMedium = db.prepare(`
153
+ INSERT INTO medium_chunks (id, section_id, doc_id, content, block_type, token_count, created_at)
154
+ VALUES (?, ?, ?, ?, ?, ?, ?)
155
+ ON CONFLICT(id) DO UPDATE SET
156
+ content=excluded.content,
157
+ block_type=excluded.block_type,
158
+ token_count=excluded.token_count
159
+ `);
160
+
161
+ const insertChunk = db.prepare(`
162
+ INSERT INTO micro_chunks (id, section_id, doc_id, content, vector, token_count, medium_id)
163
+ VALUES (?, ?, ?, ?, ?, ?, ?)
164
+ ON CONFLICT(id) DO UPDATE SET
165
+ content=excluded.content,
166
+ vector=excluded.vector,
167
+ token_count=excluded.token_count,
168
+ medium_id=excluded.medium_id
169
+ `);
170
+
171
+ const insertFts = db.prepare(`
172
+ INSERT INTO micro_chunks_fts (id, content, breadcrumbs)
173
+ VALUES (?, ?, ?)
174
+ `);
175
+
176
+ const deleteFts = db.prepare("DELETE FROM micro_chunks_fts WHERE id = ?");
177
+
178
+ const insertEdge = db.prepare(`
179
+ INSERT INTO graph_edges (source_id, target_id, relation_type)
180
+ VALUES (?, ?, ?)
181
+ ON CONFLICT(source_id, target_id, relation_type) DO NOTHING
182
+ `);
183
+
184
+ db.exec("BEGIN IMMEDIATE;");
185
+ try {
186
+ if (Array.isArray(snapshot.documents)) {
187
+ for (const d of snapshot.documents) {
188
+ insertDoc.run(
189
+ d.id,
190
+ d.path,
191
+ d.blob_hash,
192
+ d.title,
193
+ d.checksum,
194
+ d.toc_json,
195
+ d.metadata_json,
196
+ d.created_at,
197
+ d.updated_at
198
+ );
199
+ }
200
+ }
201
+
202
+ if (Array.isArray(snapshot.sections)) {
203
+ for (const s of snapshot.sections) {
204
+ insertSection.run(s.id, s.doc_id, s.heading, s.breadcrumbs, s.content, s.token_count);
205
+ }
206
+ }
207
+
208
+ if (Array.isArray(snapshot.medium_chunks)) {
209
+ for (const m of snapshot.medium_chunks) {
210
+ insertMedium.run(m.id, m.section_id, m.doc_id, m.content, m.block_type, m.token_count, m.created_at || Date.now());
211
+ }
212
+ }
213
+
214
+ if (Array.isArray(snapshot.micro_chunks)) {
215
+ for (const mc of snapshot.micro_chunks) {
216
+ let vecBuf = Buffer.alloc(0);
217
+ if (mc.vector) {
218
+ vecBuf = Buffer.from(mc.vector, "base64");
219
+ }
220
+ insertChunk.run(mc.id, mc.section_id, mc.doc_id, mc.content, vecBuf, mc.token_count, mc.medium_id || null);
221
+
222
+ try {
223
+ deleteFts.run(mc.id);
224
+ } catch {}
225
+ insertFts.run(mc.id, mc.content, mc.breadcrumbs || "");
226
+ }
227
+ }
228
+
229
+ if (Array.isArray(snapshot.graph_edges)) {
230
+ for (const e of snapshot.graph_edges) {
231
+ insertEdge.run(e.source_id, e.target_id, e.relation_type);
232
+ }
233
+ }
234
+ db.exec("COMMIT;");
235
+ } catch (err) {
236
+ db.exec("ROLLBACK;");
237
+ throw err;
238
+ }
239
+
240
+ return {
241
+ documents: snapshot.documents ? snapshot.documents.length : 0,
242
+ sections: snapshot.sections ? snapshot.sections.length : 0,
243
+ medium_chunks: snapshot.medium_chunks ? snapshot.medium_chunks.length : 0,
244
+ micro_chunks: snapshot.micro_chunks ? snapshot.micro_chunks.length : 0,
245
+ graph_edges: snapshot.graph_edges ? snapshot.graph_edges.length : 0,
246
+ blobs: blobCount,
247
+ };
248
+ }
249
+
250
+ export function hardResetDatabase({ customDb = null, customBlobDir = BLOBS_DIR } = {}) {
251
+ const db = customDb || getDatabase();
252
+
253
+ let docCount = 0;
254
+ let chunkCount = 0;
255
+ let blobCount = 0;
256
+
257
+ try {
258
+ docCount = db.prepare("SELECT COUNT(*) as cnt FROM documents").get().cnt;
259
+ chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get().cnt;
260
+ } catch {}
261
+
262
+ db.exec("BEGIN IMMEDIATE;");
263
+ try {
264
+ try { db.exec("DELETE FROM micro_chunks_fts;"); } catch {}
265
+ try { db.exec("DELETE FROM micro_chunks;"); } catch {}
266
+ try { db.exec("DELETE FROM medium_chunks;"); } catch {}
267
+ try { db.exec("DELETE FROM sections;"); } catch {}
268
+ try { db.exec("DELETE FROM graph_edges;"); } catch {}
269
+ try { db.exec("DELETE FROM knowledge_links;"); } catch {}
270
+ try { db.exec("DELETE FROM documents;"); } catch {}
271
+ db.exec("COMMIT;");
272
+ } catch (err) {
273
+ db.exec("ROLLBACK;");
274
+ throw err;
275
+ }
276
+
277
+ // Clear Blobs Directory
278
+ if (existsSync(customBlobDir)) {
279
+ try {
280
+ const files = readdirSync(customBlobDir, { recursive: true });
281
+ for (const f of files) {
282
+ const fullPath = join(customBlobDir, f);
283
+ try {
284
+ const st = statSync(fullPath);
285
+ if (st.isFile()) {
286
+ rmSync(fullPath, { force: true });
287
+ blobCount++;
288
+ }
289
+ } catch {}
290
+ }
291
+ } catch {}
292
+ }
293
+
294
+ try {
295
+ db.exec("VACUUM;");
296
+ } catch {}
297
+
298
+ return {
299
+ purgedDocuments: docCount,
300
+ purgedChunks: chunkCount,
301
+ purgedBlobs: blobCount,
302
+ };
303
+ }