Stored timestamps have one writer: local::iso and local::now
CI & Build / Build now, or wait for Android? (push) Successful in 6s
Android / Build, or is the channel already serving this? (push) Successful in 6s
CI & Build / Python tests (push) Successful in 13s
CI & Build / Python lint (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 13s
CI & Build / integration (push) Successful in 2m6s
CI & Build / Build & push image (push) Skipped
Android / Core and FFI clippy and tests (push) Successful in 1m18s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Failing after 3m19s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 9m29s
Android / Build the server image (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 6s
Android / Build, or is the channel already serving this? (push) Successful in 6s
CI & Build / Python tests (push) Successful in 13s
CI & Build / Python lint (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 13s
CI & Build / integration (push) Successful in 2m6s
CI & Build / Build & push image (push) Skipped
Android / Core and FFI clippy and tests (push) Successful in 1m18s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Failing after 3m19s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 9m29s
Android / Build the server image (push) Successful in 2s
The store's time format (RFC 3339, UTC, milliseconds, Z) is what makes lexical order chronological. It was spelled out ten times as to_rfc3339_opts(SecondsFormat::Millis, true), with two private now() copies (store, pull). local::iso(t) and local::now() now hold it; store, pull, engine and portable call them. DRY pass #2, batch 2, F5 (#5372). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ pub mod store;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use chrono::{DateTime, SecondsFormat, Utc};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
|
||||||
/// The store's file inside a client's data directory. The layout is the same on
|
/// The store's file inside a client's data directory. The layout is the same on
|
||||||
@@ -23,6 +24,17 @@ pub const DB_FILE: &str = "inkwell.db";
|
|||||||
/// Attachment bytes, filed by content hash, beside [`DB_FILE`].
|
/// Attachment bytes, filed by content hash, beside [`DB_FILE`].
|
||||||
pub const BLOBS_DIR: &str = "blobs";
|
pub const BLOBS_DIR: &str = "blobs";
|
||||||
|
|
||||||
|
/// An instant in the store's own form: RFC 3339, UTC, milliseconds, `Z`. Every
|
||||||
|
/// stored time is written this way, which is what makes lexical order chronological.
|
||||||
|
pub fn iso(t: DateTime<Utc>) -> String {
|
||||||
|
t.to_rfc3339_opts(SecondsFormat::Millis, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Now, in the store's own form ([`iso`]).
|
||||||
|
pub fn now() -> String {
|
||||||
|
iso(Utc::now())
|
||||||
|
}
|
||||||
|
|
||||||
/// The shared database handle. rusqlite connections aren't `Sync`, so a `Mutex`
|
/// The shared database handle. rusqlite connections aren't `Sync`, so a `Mutex`
|
||||||
/// serializes access — fine, since operations are quick and a client is single-user.
|
/// serializes access — fine, since operations are quick and a client is single-user.
|
||||||
/// How it is held is the caller's business: Tauri manages it as state, Android holds
|
/// How it is held is the caller's business: Tauri manages it as state, Android holds
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
use std::io::{Cursor, Read, Write};
|
use std::io::{Cursor, Read, Write};
|
||||||
|
|
||||||
use chrono::{DateTime, SecondsFormat, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use rusqlite::{params, Connection};
|
use rusqlite::{params, Connection};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
@@ -21,7 +21,7 @@ use zip::write::SimpleFileOptions;
|
|||||||
use zip::{CompressionMethod, ZipArchive, ZipWriter};
|
use zip::{CompressionMethod, ZipArchive, ZipWriter};
|
||||||
|
|
||||||
use super::models::{Note, NoteCreateInput};
|
use super::models::{Note, NoteCreateInput};
|
||||||
use super::{derive, recur, store};
|
use super::{derive, iso, now, recur, store};
|
||||||
use crate::sync::blobs::BlobStore;
|
use crate::sync::blobs::BlobStore;
|
||||||
|
|
||||||
/// The `app` an export's notes.json carries. "thoughtsync" is what every export
|
/// The `app` an export's notes.json carries. "thoughtsync" is what every export
|
||||||
@@ -128,7 +128,7 @@ pub fn export_zip(conn: &Connection, blobs: &BlobStore) -> Result<Vec<u8>, Strin
|
|||||||
let doc = json!({
|
let doc = json!({
|
||||||
"app": "inkwell",
|
"app": "inkwell",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"exported_at": Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true),
|
"exported_at": now(),
|
||||||
"labels": labels,
|
"labels": labels,
|
||||||
"notes": notes,
|
"notes": notes,
|
||||||
});
|
});
|
||||||
@@ -508,9 +508,7 @@ fn create_imported(
|
|||||||
|
|
||||||
// Last, because adding an attachment touches the note: the source's own times are
|
// Last, because adding an attachment touches the note: the source's own times are
|
||||||
// what this note should carry, not the moment it was imported.
|
// what this note should carry, not the moment it was imported.
|
||||||
let trashed_at = spec
|
let trashed_at = spec.trashed.then(now);
|
||||||
.trashed
|
|
||||||
.then(|| Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true));
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE notes SET pinned = ?1, archived = ?2, remind_at = ?3, recurrence = ?4,
|
"UPDATE notes SET pinned = ?1, archived = ?2, remind_at = ?3, recurrence = ?4,
|
||||||
trashed = ?5, trashed_at = ?6,
|
trashed = ?5, trashed_at = ?6,
|
||||||
@@ -561,16 +559,13 @@ fn strings(v: Option<&Value>) -> Vec<String> {
|
|||||||
/// Any RFC 3339 instant, in the store's own form (UTC, milliseconds, `Z`). Anything
|
/// Any RFC 3339 instant, in the store's own form (UTC, milliseconds, `Z`). Anything
|
||||||
/// else is treated as absent, as the server's `parse_dt` does.
|
/// else is treated as absent, as the server's `parse_dt` does.
|
||||||
fn instant(raw: &str) -> Option<String> {
|
fn instant(raw: &str) -> Option<String> {
|
||||||
DateTime::parse_from_rfc3339(raw).ok().map(|t| {
|
DateTime::parse_from_rfc3339(raw).ok().map(|t| iso(t.with_timezone(&Utc)))
|
||||||
t.with_timezone(&Utc)
|
|
||||||
.to_rfc3339_opts(SecondsFormat::Millis, true)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keep's timestamps: integer microseconds since the epoch.
|
/// Keep's timestamps: integer microseconds since the epoch.
|
||||||
fn usec_instant(v: &Value) -> Option<String> {
|
fn usec_instant(v: &Value) -> Option<String> {
|
||||||
let usec = v.as_i64().or_else(|| v.as_str()?.parse().ok())?;
|
let usec = v.as_i64().or_else(|| v.as_str()?.parse().ok())?;
|
||||||
DateTime::from_timestamp_micros(usec).map(|t| t.to_rfc3339_opts(SecondsFormat::Millis, true))
|
DateTime::from_timestamp_micros(usec).map(iso)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The image types the server infers from a name when the source gave no mime.
|
/// The image types the server infers from a name when the source gave no mime.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
//! ("YYYY-MM-DDTHH:MM:SS.sssZ") so string ordering and date-range comparisons line
|
//! ("YYYY-MM-DDTHH:MM:SS.sssZ") so string ordering and date-range comparisons line
|
||||||
//! up with the values the frontend sends.
|
//! up with the values the frontend sends.
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, SecondsFormat, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use rusqlite::{params, params_from_iter, Connection, OptionalExtension, Params};
|
use rusqlite::{params, params_from_iter, Connection, OptionalExtension, Params};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -15,10 +15,7 @@ use uuid::Uuid;
|
|||||||
use crate::local::derive;
|
use crate::local::derive;
|
||||||
use crate::local::models::*;
|
use crate::local::models::*;
|
||||||
use crate::local::recur;
|
use crate::local::recur;
|
||||||
|
use crate::local::{iso, now};
|
||||||
fn now() -> String {
|
|
||||||
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_id() -> String {
|
fn new_id() -> String {
|
||||||
Uuid::new_v4().to_string()
|
Uuid::new_v4().to_string()
|
||||||
@@ -571,8 +568,7 @@ fn should_snapshot(conn: &Connection, id: &str, new_body: &str) -> rusqlite::Res
|
|||||||
}
|
}
|
||||||
// String comparison, not date maths: timestamps are RFC3339 UTC with a fixed
|
// String comparison, not date maths: timestamps are RFC3339 UTC with a fixed
|
||||||
// millisecond field (see the module header), so lexical order IS chronological.
|
// millisecond field (see the module header), so lexical order IS chronological.
|
||||||
let cutoff = (Utc::now() - Duration::minutes(REVISION_WINDOW_MINUTES))
|
let cutoff = iso(Utc::now() - Duration::minutes(REVISION_WINDOW_MINUTES));
|
||||||
.to_rfc3339_opts(SecondsFormat::Millis, true);
|
|
||||||
let recent: i64 = conn.query_row(
|
let recent: i64 = conn.query_row(
|
||||||
"SELECT COUNT(*) FROM note_revisions WHERE note_id = ?1 AND created_at >= ?2",
|
"SELECT COUNT(*) FROM note_revisions WHERE note_id = ?1 AND created_at >= ?2",
|
||||||
params![id, cutoff],
|
params![id, cutoff],
|
||||||
@@ -700,7 +696,7 @@ pub fn complete_reminder(conn: &Connection, id: &str) -> rusqlite::Result<Note>
|
|||||||
match next {
|
match next {
|
||||||
Some(at) => conn.execute(
|
Some(at) => conn.execute(
|
||||||
"UPDATE notes SET remind_at = ?1 WHERE id = ?2",
|
"UPDATE notes SET remind_at = ?1 WHERE id = ?2",
|
||||||
params![at.to_rfc3339_opts(SecondsFormat::Millis, true), id],
|
params![iso(at), id],
|
||||||
)?,
|
)?,
|
||||||
None => conn.execute(
|
None => conn.execute(
|
||||||
"UPDATE notes SET remind_at = NULL, recurrence = NULL WHERE id = ?1",
|
"UPDATE notes SET remind_at = NULL, recurrence = NULL WHERE id = ?1",
|
||||||
@@ -719,7 +715,7 @@ pub fn snooze_reminder(conn: &Connection, id: &str, minutes: i64) -> rusqlite::R
|
|||||||
require_owner(conn, id)?;
|
require_owner(conn, id)?;
|
||||||
// 0 or less would set a reminder in the past; a huge value overflows `Duration`.
|
// 0 or less would set a reminder in the past; a huge value overflows `Duration`.
|
||||||
let minutes = minutes.clamp(1, SNOOZE_MAX_MINUTES);
|
let minutes = minutes.clamp(1, SNOOZE_MAX_MINUTES);
|
||||||
let t = (Utc::now() + Duration::minutes(minutes)).to_rfc3339_opts(SecondsFormat::Millis, true);
|
let t = iso(Utc::now() + Duration::minutes(minutes));
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE notes SET remind_at = ?1 WHERE id = ?2",
|
"UPDATE notes SET remind_at = ?1 WHERE id = ?2",
|
||||||
params![t, id],
|
params![t, id],
|
||||||
|
|||||||
@@ -4,14 +4,13 @@
|
|||||||
//! own inside this crate, but exposing them separately would let a caller pull
|
//! own inside this crate, but exposing them separately would let a caller pull
|
||||||
//! without pushing, which quietly overwrites unsent local edits.
|
//! without pushing, which quietly overwrites unsent local edits.
|
||||||
|
|
||||||
use chrono::{SecondsFormat, Utc};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use super::blobs::BlobStore;
|
use super::blobs::BlobStore;
|
||||||
use super::pull;
|
use super::pull;
|
||||||
use super::push;
|
use super::push;
|
||||||
use super::state;
|
use super::state;
|
||||||
use crate::local::Db;
|
use crate::local::{self, Db};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct SyncOutcome {
|
pub struct SyncOutcome {
|
||||||
@@ -102,8 +101,7 @@ pub async fn run_cycle(
|
|||||||
}
|
}
|
||||||
// Stamped only here, after BOTH halves succeeded. A timestamp written after a
|
// Stamped only here, after BOTH halves succeeded. A timestamp written after a
|
||||||
// partial cycle would tell the user they're up to date when they aren't.
|
// partial cycle would tell the user they're up to date when they aren't.
|
||||||
let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
|
state::mark_synced(&conn, &local::now()).map_err(|e| e.to_string())?;
|
||||||
state::mark_synced(&conn, &now).map_err(|e| e.to_string())?;
|
|
||||||
state::status(&conn).map_err(|e| e.to_string())?
|
state::status(&conn).map_err(|e| e.to_string())?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
//! transaction** as the page it describes — a cursor committed ahead of its data
|
//! transaction** as the page it describes — a cursor committed ahead of its data
|
||||||
//! would silently skip those rows forever, which reads as a clean sync.
|
//! would silently skip those rows forever, which reads as a clean sync.
|
||||||
|
|
||||||
use chrono::{SecondsFormat, Utc};
|
|
||||||
use rusqlite::{params, Connection, OptionalExtension};
|
use rusqlite::{params, Connection, OptionalExtension};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ use super::blobs::BlobStore;
|
|||||||
use super::client;
|
use super::client;
|
||||||
use super::state;
|
use super::state;
|
||||||
use super::wire;
|
use super::wire;
|
||||||
use crate::local::Db;
|
use crate::local::{now, Db};
|
||||||
|
|
||||||
/// Backstop against a server that never stops saying `has_more`. At the server's
|
/// Backstop against a server that never stops saying `has_more`. At the server's
|
||||||
/// 1000-row page cap this is 10M rows — far past any real store, so hitting it means
|
/// 1000-row page cap this is 10M rows — far past any real store, so hitting it means
|
||||||
@@ -108,10 +107,6 @@ async fn download_missing_blobs(
|
|||||||
Ok((downloaded, failed))
|
Ok((downloaded, failed))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn now() -> String {
|
|
||||||
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply one page and advance the cursor, atomically.
|
/// Apply one page and advance the cursor, atomically.
|
||||||
///
|
///
|
||||||
/// Labels are applied before notes so a membership never references a label row that
|
/// Labels are applied before notes so a membership never references a label row that
|
||||||
|
|||||||
Reference in New Issue
Block a user