@glassly/engine 0.3.5-dev.772 → 0.3.5-dev.776
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/build/engine.d.ts +3 -0
- package/build/engine.d.ts.map +1 -1
- package/build/facades/memory.d.ts +10 -2
- package/build/facades/memory.d.ts.map +1 -1
- package/build/facades/memory.js +10 -0
- package/build/facades/memory.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +19 -0
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MemoryStore.d.ts +13 -1
- package/build/services/MemoryStore.d.ts.map +1 -1
- package/build/services/MemoryStore.js +98 -1
- package/build/services/MemoryStore.js.map +1 -1
- package/build/services/memoryBackupFile.d.ts +9 -0
- package/build/services/memoryBackupFile.d.ts.map +1 -0
- package/build/services/memoryBackupFile.js +47 -0
- package/build/services/memoryBackupFile.js.map +1 -0
- package/package.json +7 -7
- package/src/facades/memory.ts +11 -2
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/LocalMiniappRuntime.ts +19 -0
- package/src/services/MemoryStore.ts +111 -2
- package/src/services/memoryBackupFile.ts +50 -0
|
@@ -17,7 +17,14 @@
|
|
|
17
17
|
* user-scoped keys so existing history survives the move.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
MemoryBackup,
|
|
22
|
+
MemoryEntry,
|
|
23
|
+
MemoryImportResult,
|
|
24
|
+
MemoryNoteSource,
|
|
25
|
+
MemorySession,
|
|
26
|
+
MemorySummary,
|
|
27
|
+
} from "@glassly/miniapp"
|
|
21
28
|
|
|
22
29
|
import {searchMemoryEntries} from "../utils/memory/search"
|
|
23
30
|
|
|
@@ -91,6 +98,14 @@ export interface MemoryStats {
|
|
|
91
98
|
oldestAt: number | null
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
/** Thrown by importBackup for anything that isn't a backup this build can read. */
|
|
102
|
+
export class MemoryBackupError extends Error {
|
|
103
|
+
constructor(message: string) {
|
|
104
|
+
super(message)
|
|
105
|
+
this.name = "MemoryBackupError"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
94
109
|
export function hourStartOf(ms: number): number {
|
|
95
110
|
return Math.floor(ms / HOUR_MS) * HOUR_MS
|
|
96
111
|
}
|
|
@@ -359,6 +374,65 @@ export class MemoryStore {
|
|
|
359
374
|
return Math.max(...hours)
|
|
360
375
|
}
|
|
361
376
|
|
|
377
|
+
/** The whole timeline as a backup file body (Settings > Memory, session.memory.exportFile). */
|
|
378
|
+
async exportBackup(): Promise<MemoryBackup> {
|
|
379
|
+
await this.ensureLoaded()
|
|
380
|
+
return {
|
|
381
|
+
format: "glassly-memory",
|
|
382
|
+
version: 1,
|
|
383
|
+
exportedAt: Date.now(),
|
|
384
|
+
entries: [...this.entries],
|
|
385
|
+
summaries: [...this.summaries].sort((a, b) => a.hourStart - b.hourStart),
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Merge a backup into the timeline. Skips entries whose id is stored, notes
|
|
391
|
+
* whose text is already in the same hour, and hours that already have a
|
|
392
|
+
* summary. Throws MemoryBackupError when `data` isn't a memory backup.
|
|
393
|
+
*/
|
|
394
|
+
async importBackup(data: unknown): Promise<MemoryImportResult> {
|
|
395
|
+
const backup = parseBackup(data)
|
|
396
|
+
await this.ensureLoaded()
|
|
397
|
+
const result: MemoryImportResult = {entries: 0, summaries: 0, skipped: 0}
|
|
398
|
+
|
|
399
|
+
const ids = new Set(this.entries.map((e) => e.id))
|
|
400
|
+
const noteKeys = new Set(this.entries.filter((e) => e.kind === "note").map(noteKey))
|
|
401
|
+
for (const entry of backup.entries) {
|
|
402
|
+
const key = entry.kind === "note" ? noteKey(entry) : null
|
|
403
|
+
if (ids.has(entry.id) || (key != null && noteKeys.has(key))) {
|
|
404
|
+
result.skipped++
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
ids.add(entry.id)
|
|
408
|
+
if (key != null) noteKeys.add(key)
|
|
409
|
+
this.entries.push(entry)
|
|
410
|
+
result.entries++
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const byHour = new Map(this.summaries.map((s) => [s.hourStart, s]))
|
|
414
|
+
for (const summary of backup.summaries) {
|
|
415
|
+
if (byHour.get(summary.hourStart)?.text.trim()) {
|
|
416
|
+
result.skipped++
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
byHour.set(summary.hourStart, summary)
|
|
420
|
+
result.summaries++
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (result.entries > 0) {
|
|
424
|
+
this.entries.sort((a, b) => a.at - b.at)
|
|
425
|
+
this.prune()
|
|
426
|
+
await this.persistLog()
|
|
427
|
+
}
|
|
428
|
+
if (result.summaries > 0) {
|
|
429
|
+
this.summaries = [...byHour.values()].sort((a, b) => a.hourStart - b.hourStart).slice(-MEMORY_BOUNDS.maxSummaries)
|
|
430
|
+
await this.persistSummaries()
|
|
431
|
+
}
|
|
432
|
+
if (result.entries > 0 || result.summaries > 0) this.emitChanged()
|
|
433
|
+
return result
|
|
434
|
+
}
|
|
435
|
+
|
|
362
436
|
/** Plain-text export of a scope, for the OS share sheet. */
|
|
363
437
|
async exportText(scope: "session" | "day" | "all", at?: number): Promise<string> {
|
|
364
438
|
const sessions = (await this.sessionList()).filter((s) => {
|
|
@@ -563,10 +637,45 @@ function parseArray(raw: unknown): unknown[] {
|
|
|
563
637
|
function normalize(text: string): string {
|
|
564
638
|
return text
|
|
565
639
|
.toLowerCase()
|
|
566
|
-
.replace(/[
|
|
640
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
567
641
|
.trim()
|
|
568
642
|
}
|
|
569
643
|
|
|
644
|
+
function noteKey(entry: MemoryEntry): string {
|
|
645
|
+
return `${hourStartOf(entry.at)}:${normalize(entry.text)}`
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function parseBackup(data: unknown): {entries: MemoryEntry[]; summaries: MemorySummary[]} {
|
|
649
|
+
const raw = data as Record<string, unknown> | null
|
|
650
|
+
if (!raw || typeof raw !== "object" || raw.format !== "glassly-memory" || !Array.isArray(raw.entries)) {
|
|
651
|
+
throw new MemoryBackupError("Not a Glassly memory backup")
|
|
652
|
+
}
|
|
653
|
+
if (typeof raw.version !== "number" || raw.version > 1) {
|
|
654
|
+
throw new MemoryBackupError(`Unsupported memory backup version ${String(raw.version)}`)
|
|
655
|
+
}
|
|
656
|
+
const entries: MemoryEntry[] = []
|
|
657
|
+
for (const v of raw.entries) {
|
|
658
|
+
if (!isEntry(v) || !v.id || !v.text.trim() || !Number.isFinite(v.at)) continue
|
|
659
|
+
entries.push({
|
|
660
|
+
id: v.id,
|
|
661
|
+
kind: v.kind,
|
|
662
|
+
text: v.text.trim(),
|
|
663
|
+
at: v.at,
|
|
664
|
+
speakerId: typeof v.speakerId === "string" ? v.speakerId : null,
|
|
665
|
+
...(typeof v.resolvedSpeakerName === "string" && v.resolvedSpeakerName
|
|
666
|
+
? {resolvedSpeakerName: v.resolvedSpeakerName}
|
|
667
|
+
: {}),
|
|
668
|
+
language: typeof v.language === "string" ? v.language : null,
|
|
669
|
+
...(v.kind === "note" ? {source: v.source === "voice" || v.source === "auto" ? v.source : "ui"} : {}),
|
|
670
|
+
})
|
|
671
|
+
}
|
|
672
|
+
const summaries = (Array.isArray(raw.summaries) ? raw.summaries : [])
|
|
673
|
+
.filter(isSummary)
|
|
674
|
+
.filter((s) => s.text.trim() && Number.isFinite(s.hourStart))
|
|
675
|
+
.map((s) => ({hourStart: s.hourStart, text: s.text, createdAt: s.createdAt}))
|
|
676
|
+
return {entries, summaries}
|
|
677
|
+
}
|
|
678
|
+
|
|
570
679
|
function isEntry(v: unknown): v is MemoryEntry {
|
|
571
680
|
if (!v || typeof v !== "object") return false
|
|
572
681
|
const e = v as Record<string, unknown>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory backup files: the share-sheet export and file-picker import behind
|
|
3
|
+
* Settings > Memory and session.memory.exportFile/importFile.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {MemoryExportResult, MemoryImportResult} from "@glassly/miniapp"
|
|
7
|
+
import {File, Paths} from "expo-file-system"
|
|
8
|
+
import {Platform} from "react-native"
|
|
9
|
+
import Share from "react-native-share"
|
|
10
|
+
|
|
11
|
+
import {MemoryBackupError} from "./MemoryStore"
|
|
12
|
+
import {memoryStore} from "./memoryStoreInstance"
|
|
13
|
+
|
|
14
|
+
// Some Android document providers report .json files as octet-stream.
|
|
15
|
+
const PICK_MIME_TYPES =
|
|
16
|
+
Platform.OS === "android" ? ["application/json", "application/octet-stream"] : ["application/json"]
|
|
17
|
+
|
|
18
|
+
export async function exportMemoryFile(): Promise<MemoryExportResult> {
|
|
19
|
+
const backup = await memoryStore.exportBackup()
|
|
20
|
+
const filename = `glassly-memory-${localDate(backup.exportedAt)}.json`
|
|
21
|
+
const file = new File(Paths.cache, filename)
|
|
22
|
+
file.writeSync(JSON.stringify(backup))
|
|
23
|
+
try {
|
|
24
|
+
await Share.open({url: file.uri, type: "application/json", filename, title: "Glassly memory"})
|
|
25
|
+
return {success: true}
|
|
26
|
+
} catch (error: any) {
|
|
27
|
+
// react-native-share rejects when the user dismisses the sheet.
|
|
28
|
+
if (error?.message?.includes("User did not share")) return {success: false, cancelled: true}
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolves null when the user cancels the picker. */
|
|
34
|
+
export async function importMemoryFile(): Promise<MemoryImportResult | null> {
|
|
35
|
+
const picked = await File.pickFileAsync({mimeTypes: PICK_MIME_TYPES})
|
|
36
|
+
if (picked.canceled) return null
|
|
37
|
+
let data: unknown
|
|
38
|
+
try {
|
|
39
|
+
data = JSON.parse(await picked.result.text())
|
|
40
|
+
} catch {
|
|
41
|
+
throw new MemoryBackupError("Not a Glassly memory backup")
|
|
42
|
+
}
|
|
43
|
+
return memoryStore.importBackup(data)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function localDate(ms: number): string {
|
|
47
|
+
const d = new Date(ms)
|
|
48
|
+
const pad = (n: number) => String(n).padStart(2, "0")
|
|
49
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
50
|
+
}
|