From 045ab0cdf61fa34329b65e0601853c0d992f3bc4 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 15:42:59 +0300 Subject: [PATCH] ludic.save save_write_tree: a tree straight to disk through Json.write_file, the backup and read-back kept; ludic.telemetry writes its id file the same way Co-Authored-By: Claude Opus 5.5 --- packages/ludic.save/README.md | 5 +++-- packages/ludic.save/tests/save_test.ludic | 19 +++++++++++++++++++ packages/ludic.save/write.ludic | 22 ++++++++++++++++++++++ packages/ludic.telemetry/ident.ludic | 3 ++- 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/ludic.save/README.md b/packages/ludic.save/README.md index 8b731d3e..4fbe0585 100644 --- a/packages/ludic.save/README.md +++ b/packages/ludic.save/README.md @@ -17,7 +17,7 @@ f.is_one = fn looks_like_a_trip # an object with a day in it let r = save_open(path, backup, f) # read, check, carry forward, or the backup if r.status == SAVE_OK { apply(r.value) } ... -save_write(path, backup, Json.encode(v)) # false unless it is on disk +save_write_tree(path, backup, v) # false unless it is on disk; keeps nothing ``` ## The contract @@ -31,7 +31,8 @@ save_write(path, backup, Json.encode(v)) # false unless it is on disk string closed). A best-effort JSON parser hands back half a file as a shorter, plausible one. - **A backup, and only of a whole file**: `save_write` copies the old file aside when it is intact, writes, and reads the text straight back. A caller about to delete something on the strength of - the answer needs to know it is on disk. + the answer needs to know it is on disk. `save_write_tree` does the same for a tree through + `Json.write_file`, so a save holds no text of its own afterwards. - **A newer build's file is refused and read-only.** It is never replaced by its backup (that is from the newer build too), and `save_write_to` refuses a read that says `readonly`. Both copies damaged is read-only as well. Never write over a file you could not read. diff --git a/packages/ludic.save/tests/save_test.ludic b/packages/ludic.save/tests/save_test.ludic index fe1590bc..69450bf9 100644 --- a/packages/ludic.save/tests/save_test.ludic +++ b/packages/ludic.save/tests/save_test.ludic @@ -91,6 +91,25 @@ program SaveTest { expect(Fs.read_text(bak()) == "{\"body\": \"one\", \"version\": 3}") } + test "a tree is written straight to the file, and the old file backed up only when whole" { + fresh() + let v = value_object() + value_put(v, "body", value_str("one")) + value_put(v, "version", value_int(3)) + expect(save_write_tree(path(), bak(), v)) + expect(not Fs.exists(bak())) + expect(Fs.read_text(path()) == Json.encode(v)) + value_put(v, "body", value_str("two")) + expect(save_write_tree(path(), bak(), v)) + expect(save_file_intact(bak())) + expect(save_open(path(), bak(), fmt()).status == SAVE_OK) + Fs.write_text(path(), "{\"body\": \"thr") + expect(save_write_tree(path(), bak(), v)) + expect(save_int(Json.parse(Fs.read_text(bak())), "version", 0) == 3) + let r = save_open(path(), bak(), fmt()) + r.readonly = true + expect(not save_write_tree_to(r, path(), bak(), v)) + } test "the backup stands in for a damaged file, and both damaged is read-only" { fresh() Fs.write_text(path(), "{\"body\": \"cut") diff --git a/packages/ludic.save/write.ludic b/packages/ludic.save/write.ludic index 4b5e6feb..048acebd 100644 --- a/packages/ludic.save/write.ludic +++ b/packages/ludic.save/write.ludic @@ -14,3 +14,25 @@ export function save_write_to(r: SaveRead, path: string, backup: string, text: s if r != null and r.readonly { return false } return save_write(path, backup, text) } + +# save_write for a tree: the same backup, then Json.write_file (the runtime's kept buffer, .tmp and +# rename), so a game that saves often holds nothing more for it. Read back whole, as a save is read +export function save_write_tree(path: string, backup: string, v: Val) -> bool { + if len(backup) > 0 and save_file_intact(path) { Fs.copy(path, backup) } + if not Json.write_file(v, path) { return false } + return save_file_intact(path) +} + +export function save_write_tree_to(r: SaveRead, path: string, backup: string, v: Val) -> bool { + if r != null and r.readonly { return false } + return save_write_tree(path, backup, v) +} + +# whether the file on disk is a whole one; the text read to tell is given back +export function save_file_intact(path: string) -> bool { + let s = Fs.read_text(path) + if s == null { return false } + let whole = save_text_intact(s) + unsafe { free(s) } + return whole +} diff --git a/packages/ludic.telemetry/ident.ludic b/packages/ludic.telemetry/ident.ludic index 83a9a600..9d53d170 100644 --- a/packages/ludic.telemetry/ident.ludic +++ b/packages/ludic.telemetry/ident.ludic @@ -17,7 +17,8 @@ export function telemetry_set_id(telemetry_st: mut TelemetryState, id: string) - var v = telemetry__read_obj(path) if v == null { v = Value.object() } Value.put(v, "id", Value.str(id)) - Fs.write_text(path, Json.encode(v)) + Json.write_file(v, path) + Json.free(v) } telemetry__fact(telemetry_st, TELEMETRY_ID, 0, 0, 0) }