Remove mtime

This commit is contained in:
binwiederhier
2026-01-05 21:14:29 -05:00
parent 1c2550d749
commit aca9a77498
10 changed files with 67 additions and 97 deletions

View File

@@ -31,7 +31,6 @@ const (
mid TEXT NOT NULL,
sid TEXT NOT NULL,
time INT NOT NULL,
mtime INT NOT NULL,
expires INT NOT NULL,
topic TEXT NOT NULL,
message TEXT NOT NULL,
@@ -57,7 +56,6 @@ const (
CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid);
CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid);
CREATE INDEX IF NOT EXISTS idx_time ON messages (time);
CREATE INDEX IF NOT EXISTS idx_mtime ON messages (mtime);
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires);
CREATE INDEX IF NOT EXISTS idx_sender ON messages (sender);
@@ -71,53 +69,53 @@ const (
COMMIT;
`
insertMessageQuery = `
INSERT INTO messages (mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO messages (mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published, deleted)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
deleteMessageQuery = `DELETE FROM messages WHERE mid = ?`
updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
selectMessagesByIDQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages
WHERE mid = ?
`
selectMessagesSinceTimeQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages
WHERE topic = ? AND time >= ? AND published = 1
ORDER BY mtime, id
ORDER BY time, id
`
selectMessagesSinceTimeIncludeScheduledQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages
WHERE topic = ? AND time >= ?
ORDER BY mtime, id
ORDER BY time, id
`
selectMessagesSinceIDQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages
WHERE topic = ? AND id > ? AND published = 1
ORDER BY mtime, id
ORDER BY time, id
`
selectMessagesSinceIDIncludeScheduledQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages
WHERE topic = ? AND (id > ? OR published = 0)
ORDER BY mtime, id
ORDER BY time, id
`
selectMessagesLatestQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages
WHERE topic = ? AND published = 1
ORDER BY time DESC, id DESC
LIMIT 1
`
selectMessagesDueQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
SELECT mid, sid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages
WHERE time <= ? AND published = 0
ORDER BY mtime, id
ORDER BY time, id
`
selectMessagesExpiredQuery = `SELECT mid FROM messages WHERE expires <= ? AND published = 1`
updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?`
@@ -270,10 +268,8 @@ const (
//13 -> 14
migrate13To14AlterMessagesTableQuery = `
ALTER TABLE messages ADD COLUMN sid TEXT NOT NULL DEFAULT('');
ALTER TABLE messages ADD COLUMN mtime INT NOT NULL DEFAULT('0');
ALTER TABLE messages ADD COLUMN deleted INT NOT NULL DEFAULT('0');
CREATE INDEX IF NOT EXISTS idx_sid ON messages (sid);
CREATE INDEX IF NOT EXISTS idx_mtime ON messages (mtime);
`
)
@@ -415,7 +411,6 @@ func (c *messageCache) addMessages(ms []*message) error {
m.ID,
m.SID,
m.Time,
m.MTime,
m.Expires,
m.Topic,
m.Message,
@@ -723,14 +718,13 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
}
func readMessage(rows *sql.Rows) (*message, error) {
var timestamp, mtimestamp, expires, attachmentSize, attachmentExpires int64
var timestamp, expires, attachmentSize, attachmentExpires int64
var priority, deleted int
var id, sid, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string
err := rows.Scan(
&id,
&sid,
&timestamp,
&mtimestamp,
&expires,
&topic,
&msg,
@@ -782,7 +776,6 @@ func readMessage(rows *sql.Rows) (*message, error) {
ID: id,
SID: sid,
Time: timestamp,
MTime: mtimestamp,
Expires: expires,
Event: messageEvent,
Topic: topic,

View File

@@ -24,11 +24,9 @@ func TestMemCache_Messages(t *testing.T) {
func testCacheMessages(t *testing.T, c *messageCache) {
m1 := newDefaultMessage("mytopic", "my message")
m1.Time = 1
m1.MTime = 1000
m2 := newDefaultMessage("mytopic", "my other message")
m2.Time = 2
m2.MTime = 2000
require.Nil(t, c.AddMessage(m1))
require.Nil(t, c.AddMessage(newDefaultMessage("example", "my example message")))
@@ -126,13 +124,10 @@ func testCacheMessagesScheduled(t *testing.T, c *messageCache) {
m1 := newDefaultMessage("mytopic", "message 1")
m2 := newDefaultMessage("mytopic", "message 2")
m2.Time = time.Now().Add(time.Hour).Unix()
m2.MTime = time.Now().Add(time.Hour).UnixMilli()
m3 := newDefaultMessage("mytopic", "message 3")
m3.Time = time.Now().Add(time.Minute).Unix() // earlier than m2!
m3.MTime = time.Now().Add(time.Minute).UnixMilli() // earlier than m2!
m3.Time = time.Now().Add(time.Minute).Unix() // earlier than m2!
m4 := newDefaultMessage("mytopic2", "message 4")
m4.Time = time.Now().Add(time.Minute).Unix()
m4.MTime = time.Now().Add(time.Minute).UnixMilli()
require.Nil(t, c.AddMessage(m1))
require.Nil(t, c.AddMessage(m2))
require.Nil(t, c.AddMessage(m3))
@@ -206,25 +201,18 @@ func TestMemCache_MessagesSinceID(t *testing.T) {
func testCacheMessagesSinceID(t *testing.T, c *messageCache) {
m1 := newDefaultMessage("mytopic", "message 1")
m1.Time = 100
m1.MTime = 100000
m2 := newDefaultMessage("mytopic", "message 2")
m2.Time = 200
m2.MTime = 200000
m3 := newDefaultMessage("mytopic", "message 3")
m3.Time = time.Now().Add(time.Hour).Unix() // Scheduled, in the future, later than m7 and m5
m3.MTime = time.Now().Add(time.Hour).UnixMilli() // Scheduled, in the future, later than m7 and m5
m3.Time = time.Now().Add(time.Hour).Unix() // Scheduled, in the future, later than m7 and m5
m4 := newDefaultMessage("mytopic", "message 4")
m4.Time = 400
m4.MTime = 400000
m5 := newDefaultMessage("mytopic", "message 5")
m5.Time = time.Now().Add(time.Minute).Unix() // Scheduled, in the future, later than m7
m5.MTime = time.Now().Add(time.Minute).UnixMilli() // Scheduled, in the future, later than m7
m5.Time = time.Now().Add(time.Minute).Unix() // Scheduled, in the future, later than m7
m6 := newDefaultMessage("mytopic", "message 6")
m6.Time = 600
m6.MTime = 600000
m7 := newDefaultMessage("mytopic", "message 7")
m7.Time = 700
m7.MTime = 700000
require.Nil(t, c.AddMessage(m1))
require.Nil(t, c.AddMessage(m2))
@@ -285,17 +273,14 @@ func testCachePrune(t *testing.T, c *messageCache) {
m1 := newDefaultMessage("mytopic", "my message")
m1.Time = now - 10
m1.MTime = (now - 10) * 1000
m1.Expires = now - 5
m2 := newDefaultMessage("mytopic", "my other message")
m2.Time = now - 5
m2.MTime = (now - 5) * 1000
m2.Expires = now + 5 // In the future
m3 := newDefaultMessage("another_topic", "and another one")
m3.Time = now - 12
m3.MTime = (now - 12) * 1000
m3.Expires = now - 2
require.Nil(t, c.AddMessage(m1))
@@ -546,7 +531,6 @@ func TestSqliteCache_Migration_From1(t *testing.T) {
// Add delayed message
delayedMessage := newDefaultMessage("mytopic", "some delayed message")
delayedMessage.Time = time.Now().Add(time.Minute).Unix()
delayedMessage.MTime = time.Now().Add(time.Minute).UnixMilli()
require.Nil(t, c.AddMessage(delayedMessage))
// 10, not 11!

View File

@@ -874,7 +874,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
return err
}
minc(metricMessagesPublishedSuccess)
return s.writeJSON(w, m)
return s.writeJSON(w, m.forJSON())
}
func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v *visitor) error {
@@ -1291,7 +1291,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *visitor) error {
encoder := func(msg *message) (string, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(&msg); err != nil {
if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil {
return "", err
}
return buf.String(), nil
@@ -1302,7 +1302,7 @@ func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *
func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request, v *visitor) error {
encoder := func(msg *message) (string, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(&msg); err != nil {
if err := json.NewEncoder(&buf).Encode(msg.forJSON()); err != nil {
return "", err
}
if msg.Event != messageEvent {

View File

@@ -24,10 +24,9 @@ const (
// message represents a message published to a topic
type message struct {
ID string `json:"id"` // Random message ID
SID string `json:"sid"` // Message sequence ID for updating message contents
Time int64 `json:"time"` // Unix time in seconds
MTime int64 `json:"mtime"` // Unix time in milliseconds
ID string `json:"id"` // Random message ID
SID string `json:"sid,omitempty"` // Message sequence ID for updating message contents (omitted if same as ID)
Time int64 `json:"time"` // Unix time in seconds
Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
Event string `json:"event"` // One of the above
Topic string `json:"topic"`
@@ -53,7 +52,6 @@ func (m *message) Context() log.Context {
"message_id": m.ID,
"message_sid": m.SID,
"message_time": m.Time,
"message_mtime": m.MTime,
"message_event": m.Event,
"message_body_size": len(m.Message),
}
@@ -66,6 +64,16 @@ func (m *message) Context() log.Context {
return fields
}
// forJSON returns a copy of the message prepared for JSON output.
// It clears SID if it equals ID (to avoid redundant output).
func (m *message) forJSON() *message {
msg := *m
if msg.SID == msg.ID {
msg.SID = "" // Will be omitted due to omitempty
}
return &msg
}
type attachment struct {
Name string `json:"name"`
Type string `json:"type,omitempty"`
@@ -123,7 +131,6 @@ func newMessage(event, topic, msg string) *message {
return &message{
ID: util.RandomString(messageIDLength),
Time: time.Now().Unix(),
MTime: time.Now().UnixMilli(),
Event: event,
Topic: topic,
Message: msg,
@@ -162,11 +169,7 @@ type sinceMarker struct {
}
func newSinceTime(timestamp int64) sinceMarker {
return newSinceMTime(timestamp * 1000)
}
func newSinceMTime(mtimestamp int64) sinceMarker {
return sinceMarker{time.UnixMilli(mtimestamp), ""}
return sinceMarker{time.Unix(timestamp, 0), ""}
}
func newSinceID(id string) sinceMarker {
@@ -557,7 +560,7 @@ func newWebPushPayload(subscriptionID string, message *message) *webPushPayload
return &webPushPayload{
Event: webPushMessageEvent,
SubscriptionID: subscriptionID,
Message: message,
Message: message.forJSON(),
}
}

View File

@@ -70,8 +70,7 @@
"notifications_delete": "Delete",
"notifications_copied_to_clipboard": "Copied to clipboard",
"notifications_tags": "Tags",
"notifications_sid": "Sequence ID",
"notifications_revisions": "Revisions",
"notifications_modified": "modified {{date}}",
"notifications_priority_x": "Priority {{priority}}",
"notifications_new_indicator": "New notification",
"notifications_attachment_image": "Attachment image",

View File

@@ -25,9 +25,6 @@ const addNotification = async ({ subscriptionId, message }) => {
const db = await dbAsync();
const populatedMessage = message;
if (!("mtime" in populatedMessage)) {
populatedMessage.mtime = message.time * 1000;
}
if (!("sid" in populatedMessage)) {
populatedMessage.sid = message.id;
}

View File

@@ -157,7 +157,7 @@ class SubscriptionManager {
// killing performance. See https://dexie.org/docs/Collection/Collection.offset()#a-better-paging-approach
const notifications = await this.db.notifications
.orderBy("mtime") // Sort by time first
.orderBy("time") // Sort by time
.filter((n) => n.subscriptionId === subscriptionId)
.reverse()
.toArray();
@@ -167,30 +167,39 @@ class SubscriptionManager {
async getAllNotifications() {
const notifications = await this.db.notifications
.orderBy("mtime") // Efficient, see docs
.orderBy("time") // Efficient, see docs
.reverse()
.toArray();
return this.groupNotificationsBySID(notifications);
}
// Collapse notification updates based on sids
// Collapse notification updates based on sids, keeping only the latest version
// Also tracks the original time (earliest) for each sequence
groupNotificationsBySID(notifications) {
const results = {};
const latestBySid = {};
const originalTimeBySid = {};
notifications.forEach((notification) => {
const key = `${notification.subscriptionId}:${notification.sid}`;
if (key in results) {
if ("history" in results[key]) {
results[key].history.push(notification);
} else {
results[key].history = [notification];
}
} else {
results[key] = notification;
// Track the latest notification for each sid (first one since sorted DESC)
if (!(key in latestBySid)) {
latestBySid[key] = notification;
}
// Track the original (earliest) time for each sid
const currentOriginal = originalTimeBySid[key];
if (currentOriginal === undefined || notification.time < currentOriginal) {
originalTimeBySid[key] = notification.time;
}
});
return Object.values(results);
// Return latest notifications with originalTime set
return Object.entries(latestBySid).map(([key, notification]) => ({
...notification,
originalTime: originalTimeBySid[key],
}));
}
/** Adds notification, or returns false if it already exists */
@@ -201,9 +210,6 @@ class SubscriptionManager {
}
try {
const populatedNotification = notification;
if (!("mtime" in populatedNotification)) {
populatedNotification.mtime = notification.time * 1000;
}
if (!("sid" in populatedNotification)) {
populatedNotification.sid = notification.id;
}
@@ -227,9 +233,6 @@ class SubscriptionManager {
async addNotifications(subscriptionId, notifications) {
const notificationsWithSubscriptionId = notifications.map((notification) => {
const populatedNotification = notification;
if (!("mtime" in populatedNotification)) {
populatedNotification.mtime = notification.time * 1000;
}
if (!("sid" in populatedNotification)) {
populatedNotification.sid = notification.id;
}

View File

@@ -11,9 +11,9 @@ const createDatabase = (username) => {
const dbName = username ? `ntfy-${username}` : "ntfy"; // IndexedDB database is based on the logged-in user
const db = new Dexie(dbName);
db.version(3).stores({
db.version(4).stores({
subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]",
notifications: "&id,sid,subscriptionId,time,mtime,new,[subscriptionId+new]", // compound key for query performance
notifications: "&id,sid,subscriptionId,time,new,[subscriptionId+new]", // compound key for query performance
users: "&baseUrl,username",
prefs: "&key",
});

View File

@@ -69,7 +69,7 @@ export const toNotificationParams = ({ subscriptionId, message, defaultTitle, to
badge,
icon,
image,
timestamp: message.mtime,
timestamp: message.time * 1000,
tag,
renotify: true,
silent: false,

View File

@@ -236,7 +236,9 @@ const NotificationItem = (props) => {
const { t, i18n } = useTranslation();
const { notification } = props;
const { attachment } = notification;
const date = formatShortDateTime(notification.time, i18n.language);
const isModified = notification.originalTime && notification.originalTime !== notification.time;
const originalDate = formatShortDateTime(notification.originalTime || notification.time, i18n.language);
const modifiedDate = isModified ? formatShortDateTime(notification.time, i18n.language) : null;
const otherTags = unmatchedTags(notification.tags);
const tags = otherTags.length > 0 ? otherTags.join(", ") : null;
const handleDelete = async () => {
@@ -267,8 +269,6 @@ const NotificationItem = (props) => {
const hasUserActions = notification.actions && notification.actions.length > 0;
const showActions = hasAttachmentActions || hasClickAction || hasUserActions;
const showSid = notification.id !== notification.sid || notification.history;
return (
<Card sx={{ padding: 1 }} role="listitem" aria-label={t("notifications_list_item")}>
<CardContent>
@@ -289,7 +289,8 @@ const NotificationItem = (props) => {
</Tooltip>
)}
<Typography sx={{ fontSize: 14 }} color="text.secondary">
{date}
{originalDate}
{modifiedDate && ` (${t("notifications_modified", { date: modifiedDate })})`}
{[1, 2, 4, 5].includes(notification.priority) && (
<img
src={priorityFiles[notification.priority]}
@@ -325,16 +326,6 @@ const NotificationItem = (props) => {
{t("notifications_tags")}: {tags}
</Typography>
)}
{showSid && (
<Typography sx={{ fontSize: 14 }} color="text.secondary">
{t("notifications_sid")}: {notification.sid}
</Typography>
)}
{notification.history && (
<Typography sx={{ fontSize: 14 }} color="text.secondary">
{t("notifications_revisions")}: {notification.history.length + 1}
</Typography>
)}
</CardContent>
{showActions && (
<CardActions sx={{ paddingTop: 0 }}>