inessential by Brent Simmons

November 2013

TextExpander and Data Sharing

MacStories reports that TextExpander touch needs to be revised because it’s sharing data with apps via a special Reminder. (Which is a hack, obviously, but it was needed because the previous method, a shared clipboard, was shut down by Apple.)

I would love to see a supported means of sharing data between apps on iOS. I’d love to see something like AppleScript (only much, much better).

In the absence of that, the only non-hacky way of sharing data is a web service. Our friends at Smile want to publish a new SDK by Monday, and there’s no time to create a web service between now and then. I understand their needing an expedient solution right away. Nevertheless, I would advise switching over to a web service as soon as possible afterward.

The Senate and Political Minorities

The Senate changed its rules today to prevent filibusters on judicial and executive nominees (except for the Supreme Court).

One possible criticism of this change, no matter what your party, is that the filibuster protects the rights of the minority: it helps prevent the tyranny of the majority.

(That tyranny is a risk in any democracy, because the majority may be and often is very, very wrong. There’s an old saying that the First Amendment, were it brought up for a popular vote, would not pass, and I believe it.)

Here’s the thing about the Senate: its construction protects the rights of the minority. No Senate rules can alter that.

The Senate gives two votes to each state. Alaska’s tiny population of 731,449 (2012) has the same number of votes as California’s 50-times-larger population of 38.04 million.

Delaware and Rhode Island can out-vote Texas. Kentucky and Tennessee can out-vote New York.

While the House of Representatives is meant to be proportional (and isn’t, really, but that’s a different topic), the Senate is designed to represent the individual states, to make sure that the interests of small states are as well-considered as the interests of larger states.

Note that the Senate is not designed to protect the interests of the minority political party. (The Constitution makes no mention of political parties.) Instead, it’s designed to protect the states that are outnumbered in the House of Representatives.

For any legislation to finally become a law, it has to pass both the proportional, democratic House and the state-representative Senate — and then, finally, get signed by the President. That’s a pretty high bar, which is as it should be. (To suggest that legislation should reach an even higher bar — a filibuster-proof three-fifths of the Senate, for example — is, I think, anti-democratic.)

Judicial and executive nominations are not like bills. The Constitution says this in Article II, Section 2, paragraph 2:

He [the President] shall have Power, by and with the Advice and Consent of the Senate, to make Treaties, provided two thirds of the Senators present concur; and he shall nominate, and by and with the Advice and Consent of the Senate, shall appoint Ambassadors, other public Ministers and Consuls, Judges of the supreme Court, and all other Officers of the United States, whose Appointments are not herein otherwise provided for, and which shall be established by Law: but the Congress may by Law vest the Appointment of such inferior Officers, as they think proper, in the President alone, in the Courts of Law, or in the Heads of Departments.

Treaties require two-thirds of present Senators to agree, while nominees require just the “Advice and Consent” of the Senate. We take that “consent” to mean a majority vote. (It’s conceivable that we take “consent” to mean something else, but a majority vote is a reasonable and time-tested means.)

It is notable that this is asked of the Senate and not the House. It’s very possible for a nominee with broad popular support to fail to reach 51 votes in the Senate. It’s also very possible for a nominee who’s broadly considered as unqualified to succeed at reaching 51 votes in the Senate.

In both cases the system is working properly — that is, the interests of large and small states are considered equally, rather than allowing Illinois to trample on Wyoming, or Georgia to step on Vermont.

Constitutionally this is the important thing. Not important to the Constitution — not even mentioned — is the rights of the minority political party.

Knowing this, I’m unconcerned about the rules change. I think it’s good. Getting 51 Senators to agree is a high enough bar as it is: it means a majority of states (of any size), not people, have agreed. (The filibuster meant that at least 60 Senators had to agree.)

(Will my party be in the minority some day? I would hope so, since it’s good for the Republic for chambers to switch hands from time to time. And I may not like what the other party does. But I will like that the system is working as designed, which is not true at the moment.)

Should the filibuster remain a possibility for Supreme Court nominees? More important nominations may need to reach a higher bar: maybe a filibuster-proof 60 votes makes sense for the Supreme Court. I’m not sure what I think. But, at least for now, it’s still in place, and I’m good with that.

I would, however, get rid of the filibuster for all other legislation.

The Long and Short Paths for Frontier on Mavericks

Dave Winer wrote about choices for Frontier/Mac users, now that Frontier and the OPML Editor won’t run on Mavericks.

The earliest comment I can find is from August 20, 1990, though it appears the project was started before then. It’s old code, and it doesn’t look anything like what a modern Mac programmer would write. (It’s written in C, at least, and not C++. Thank goodness. You’ll notice lots of structs and function pointers.)

There are all kinds things that would have to be fixed before it could be made a modern Mac app. Here are some of them:

  • Relies too much on globals.

  • Uses QuickDraw. Switching to Quartz is not a straightforward change.

  • Uses Open Transport instead of sockets.

  • Memory is mostly Handles and Pascal strings. (There’s no Cocoa Foundation in there, and only the most minimal use of CoreFoundation.)

  • Switching to 64-bit may be a major challenge because of the database format.

  • It uses cooperative multitasking rather than preemptive. (YieldToAnyThread()!)

  • Though it was Carbon-ized, it still uses WaitNextEvent rather than Carbon events and timers.

  • It uses a bunch of other deprecated APIs: file system, resource manager, etc.

  • You can’t compile it on Xcode 5. Which means you really need a machine running OS X 10.6 and Xcode 3.x with the 10.5 SDK. That’s the only way you can build and debug, so you can understand how it’s supposed to work.

  • Text editing uses a third-party library called Paige which should be just nuked in favor of the built-in text editor.

  • String objects are not Unicode-aware. (There are some translation verbs, but basically the system expects strings to be MacRoman-encoded strings with single-byte characters.)

  • There are some things that should probably be nuked: running as an OSA component, displaying IOA (MacBird) cards, menu-sharing.

  • This same codebase builds a Win32 app. Most of the code is shared. This just adds to the complexity of making any changes.

  • Plus things I’m not thinking of.

The Slow Path

Dave mentions that it’s organized in layers, and it is.

I recommend starting with the database layer. Make it so that it doesn’t use globals and does us preemptive instead of cooperative threads. Make sure it’s thread-safe. Make it compile for 64-bit, and make it so a database file created on a 64-bit Mac can be opened in 32-bit mode. (Though you might allow the exception that databases beyond a certain size can’t be opened in 32-bit mode.)

Ideally this would be a project itself, one that could also build as a stand-alone library (which was done once in the past). (For those who don’t know: the database is a key-value tree structure. Tables can hold tables, scalars, scripts, and some pre-defined objects. The language uses dot notation to refer to things.)

The next thing to tackle would be the language, the compiler and evaluater. Same issues apply: it shouldn’t use globals, it should be thread-safe, and it should be 64-bit. Ideally it could compile with its only dependency the database layer.

I would say repeat until finished, and that’s partly true, but eventually you get to the UI. I would structure things this way: the UI would be Cocoa and not shared with the Windows version. The database, language, utilities, and other low-level things would remain written in C and shared with Windows.

I described this path in just a few paragraphs, but it’s bigger than it sounds.

The Short Path

There is, however, a shorter path to getting it up-and-running on Mavericks. The main issue is that Open Transport went away, and the networking layer should be replaced with sockets.

I’ve actually done this work already. It needs more testing and surely needs some fixes, but it exists and does at least work to the extent that I tested it.

The problem is that I don’t have a machine capable of running OS X 10.6. If I did, I could get Xcode 3.x and the OS X 10.5 SDK and do a build that would run on Mavericks.

But if you’re a programmer and have such a machine, please feel free. Here’s MacSocketNetEvents.c, which replaces OpenTransportNetEvents.c.

So: that would get it back up on Mavericks. It would still be a 32-bit app and the to-do list would still be huge — but at least it would work again. (Until the next OS X release, anyway.)

Update 5:35 PM Pacific: you’ll also need mutex.h and mutex.c.

Vesper Sync Diary #6 - Merging Notes

The Rules

  1. Conflicts are not permitted.

  2. A note should merge at the property level. (If you change note text on your day phone and a note’s tags on your night phone, both phones should get both changes.)

  3. Real time determines which change wins.

  4. A client doesn’t have to be up-to-date before it can push changes to the server.

  5. The order of calls to the server must not matter.

  6. It’s not important to keep a history of changes.

It’s not Like SCM

With SCM, changes have parents and children. SCM systems work wonderfully and we developers rely on them, but that model doesn’t fit here. I don’t care if change B descends from change A — the only thing that matters is that change B happened later in real time (wall-clock time).

This rules out Lamport clocks. They have their uses, definitely, but not in this case.

(I also rule out vector clocks, because the system would need to know about all the clients for a given user. Clients may come and go, which would make using vector clocks rather challenging. Even if I could use them, I’m not sure they’d enforce the real-time-wins rule.)

Last-Writer-Wins (to Server) Won’t Always Work

One simple way to make this work almost all the time is to say that the last change the server receives is the winner. (Many APIs work that way.)

The problem with that is when you make changes on your day phone, but it can’t connect to the server, then you go make changes on your night phone — then your day phone finally connects and overwrites the changes you made on your night phone. That violates rules #3 and #5.

Put another way: real time wins, but that has to be the real time that a person made a given change, not the real time that a change made it to the server.

How Merging Works

A note has a few properties that can change. (Text, tags, attachments, sortDate, archived status.)

For each of these properties there’s an additional property: a modification timestamp. (As in textModificationDate, tagsModificationDate, etc.) Those timestamps are set on the client: they’re the real time the user made that change.

Merging happens on both server and client, and it always happens the same way: when comparing a property, it compares the modification date for that property. The later modification date always wins.

This means that changes can arrive on the server and on clients in any order, delayed by any amount of time, and merging will always give the same result.

Why You Suddenly Feel Frightened

To the Eight Fallacies of Distributed Computing you could add a ninth fallacy: client clocks are accurate.

They’re not. You can’t rely on them. But if you can’t rely on client clocks being accurate, how can this system work?

I’ve had some advice on this.

One idea is to have the client check the time against a Network Time Protocol server (or the sync server, which is probably simpler). If the time is off by some substantial amount, then let the user know that syncing may not work correctly, and that they should turn on setting the time automatically on their device.

Another idea is to get the time from the sync server, compare it to the client time, and calculate an offset to use when creating timestamps. That should allow the client to create accurate-enough timestamps.

I like this second idea best. Given a server that reports its time with each call, clients get plenty of opportunities to recalibrate. (The offset calculation would have to take into account the duration of a given call. I’d probably just assume that the server date refers to a date duration-of-call / 2 seconds ago. Close enough.)

There would also be sanity checks. Is a calibrated timestamp way in the past or the future? Is a new calibrated timestamp earlier than the existing timestamp for a given property or earlier than the last date reported by the server? (It’s not allowed to be.) Easy stuff.

The worst-case scenario is a client where the time keeps changing wildly. But, with frequent recalibration and sanity checks, even this rare case should work fine.

Failure

Syncing is, like baseball, a game of failure. This system accounts for all of these failures:

  1. Temporary network failure.

  2. Temporary server failure.

  3. Client clock failure.

  4. Temporary failure to get a client up-to-date with sync changes.

  5. Temporary failure for a client to send sync changes.

Once the failures clear up, everything will sync, and the result will be exactly the same as if there were no failures. (Note that client clock failure can be permanent and it will still work.)

How to Evaluate a Syncing System

I think a good syncing system has four attributes:

  1. It does what the user expects.

  2. It’s efficient.

  3. It resists failures.

  4. It continues to work.

I might add #5: it’s as simple as possible (though not simpler). Bugs are less likely when there’s nowhere they can hide. Achieving 1-4 is easier when the system is not complex.

What I like about this system is that it comes down to date comparison, and it’s hard to get simpler than that. Easy to write, easy to debug, easy to maintain.

Vesper Sync Diary #5 - Sync Tokens and Efficiency

Picture this simple web services API as a function:

NSArray \*uploadDeleted­Objects­(NSArray \*uniqueIDs)

(The reality is more complicated, but only barely.)

The client sends an array of uniqueIDs of objects that have been deleted on the client, and the server returns an array of uniqueIDs for deletions that the client may not know about. (Those deletions may have happened on other devices.)

There are two keys to making this efficient.

The first and obvious part is this: the client should send a given uniqueID only once to the server. This is pretty easy to keep track of — if the server responds with 200 OK, then delete that uniqueID locally and never send it again.

The second part is that the server should not send back everything — it should send only what the client doesn’t know about. Here’s how I’m handling that.

Sync tokens

I’ll explain this backwards, because it makes more sense that way.

When the server responds with an array of uniqueIDs, it also returns a sync token in the response headers. That sync token is an opaque string. The client stores it (but doesn’t understand it), and passes it back to the server on the next call to that API.

On the next call to that API, the server looks for a sync token in the request headers. If it’s there, it cracks it open and finds a date. (The server understands sync tokens, obviously.) It then fetches uniqueIDs only since that date. (The date is the date that uniqueID was stored on the server and not the date the deletion happened on the client.)

The server then creates a new sync token and passes it, along with the uniqueIDs it fetched, back to the client. And the client stores that new sync token. The server never has to store sync tokens — it just has to be able to decode them.

(I didn’t invent this, by the way. It was a part of the NewsGator RSS sync API and a part of the Glassboard API. I wouldn’t be surprised to find that this idea is widely used.)

Why not just ask for changes since a certain date?

A sync token in Vesper is formed like this:

1:timestamp base64-encoded. Like this: MToxMzg0MjgxNTc2MzU1, which decodes to 1:1384281576355.

The 1 signifies that it’s a version one sync token. There could be other versions later — it might be useful to store other data in a sync token.

But I could have avoided all this just by making the API look like this:

NSArray \*uploadDeleted­Objects­(NSArray \*uniqueIDs, NSDate \*sinceDate);

That would have put the responsibility on the client to make syncing efficient — but I think that that’s the server’s job.

It’s the server’s job because I can update the server at any time and make it more efficient. (I could, for instance, add data to the sync token without having to update the clients.)

It’s the server’s job because it’s good engineering to consider the possibility that there could be other clients on other platforms, and we’d be duplicating code if we had to make those clients smart too. Better that the client code is simple and the server code is smart.

Vesper Sync Diary #4 - In Another Country

I mentioned previously that Vesper will sync via web services.

Web services don’t just appear out of the foam like Aphrodite from the sea — they have to be written. Though I was tempted to run a server on a Mac and write the backend in Objective-C, I decided to do what normal people do: use a scripting language. JavaScript.

Here are the main features of JavaScript country — what I’ve seen so far, anyway. (You may know all this already. If so, skip it. This is written for people like me who are mainly Cocoa developers.)

Node.js

It’s a fast JavaScript runtime, library, and server. It’s interesting in that it doesn’t create a bunch of threads: instead, it uses callbacks. Once you get over the shock of a different language, it should seem very familiar to Cocoa developers.

Now, of course, there are two ways of doing callbacks, and there’s only one way that’s bearable: closures. (Blocks, in Objective-C-world.) JavaScript has closures. I had no idea.

(Before I got started, I thought of JavaScript as the web’s answer to AppleScript — hard to write and prickly, seemingly nondeterministic. I’m getting over that.)

Learning JavaScript

Node.js comes with a REPL, which is great when trying to get the hang of the syntax and libraries. Here’s an example pasted from Terminal:

> d = {foo : "bar"}
{ foo: 'bar' }
> d.foo
'bar'
> d['foo']
'bar'
> d[foo]
ReferenceError: foo is not defined
  at repl:1:4
  at REPLServer.self.eval (repl.js:109:21)
  at Interface.<anonymous> (repl.js:248:12)
  at Interface.EventEmitter.emit (events.js:96:17)
  at Interface._onLine (readline.js:200:10)
  at Interface._line (readline.js:518:8)
  at Interface._ttyWrite (readline.js:736:14)
  at ReadStream.onkeypress (readline.js:97:10)
  at ReadStream.EventEmitter.emit (events.js:126:20)
  at emitKey (readline.js:1058:12)

This examples illustrates one of the parts of JavaScript I find weird. You can create a dictionary like this: {foo : 'bar'} where foo isn’t a defined variable.

And then you can refer to d.foo and d['foo'] but not to d[foo].

What happens if foo is a defined variable?

> foo = "xyz"
'xyz'
> d = {foo : "bar"}
{ foo: 'bar' }

This makes me nervous.

Another weird thing is ===, which bypasses type coercion. x === y means that x and y are equal and the same type. (Most of the time you probably want ===, for safety’s sake.)

A third weird thing: const is not yet part of the official language. (Though some implementations have added it.) This freaks me out.

There’s plenty to like about JavaScript, and I do like it. It certainly saves typing (compared to Objective-C; but then, what doesn’t?). Everything is a var. Function parameters are untyped. It has the usual advantages of scripting languages (which are also the usual disadvantages).

JavaScript is not just for server-side work (or in-the-browser work). You can write shell scripts in JavaScript via Node.js.

You can even script Mac apps and do Cocoa-ish things — see Gus’s JSTalk.

Mocha

I recently spent a few days adding unit tests to Vesper. (It would take another week to get caught-up.) Even though some things are hard or impossible to test — does that animation look right? — it’s still worth it for the low-level code. (It’s so much easier to concentrate on that awesome animation code if you know the app’s foundation is solid and bug-free.)

The good thing about server-side JavaScript is that, in theory, it’s all testable. It’s all low-level code: no UI, no animations, nothing the user actually sees.

I’m using Mocha for testing. It’s easy.

One of the frustrations with XCTest is that you have to do special things (run the runloop) to handle asynchronous tests. (I haven’t even tried yet.) Mocha handles asynchronous tests easily.

It even provides color-coded output, which I like.

Text Editors

I live in Xcode, like most Cocoa developers. My editor for everything else is BBEdit.

But last night I wondered if there might be another editor that’s particularly stellar for JavaScript development. I tried three.

Sublime Text lasted about a minute. It appears to be wonderfully powerful — but it’s also not Mac-like. The first thing I did was open a file and select some text. Dead give-away right there. Then I checked out the preferences. Ah. No. Not for me, though I appreciate that many people love it.

TextMate 2 alpha is a Mac app. But there were deal-stoppers there too. (Deal-stoppers for me. This is all personal taste.) I didn’t like having to double-click in the file list to open a file. I especially didn’t like that I couldn’t find an easy way to edit the color schemes. (I like syntax coloring, but can’t stand syntax-bolding and syntax-italicizing.)

I like Chocolat. It’s very much a Mac app. The file list works via single-click. I could turn off syntax-bolding and syntax-italicizing. It has some nice features, too — my favorite so far is the always-visible symbols list.

Chocolat also looks better than the other text editors. It’s cleaner. (It’s weird to care about that, since I’m mostly just looking at text. But it means the app has fewer things to distract from the text.)

Chocolat isn’t without issues, though, and I have 14 days left to decide whether or not to buy it. (It’s $49, a totally fair price for a development tool.)

Update a few minutes later. Matthew Johnson has TextMate tips: how to change the bold/italics and how to open a file with a single click.

Update quite a few minutes later. I forgot to mention that I’m also using Express. Don’t use Node without it.

Update a couple days later. I tried Chocolat for a while, then tried TextMate 2 for a while — then went back to BBEdit.

VoodooPad News

Gus: A New Home for VoodooPad:

I first approached my friend Mike Ash a while back about taking over VoodooPad. I’ve known Mike for about a decade now and he’s been using VoodooPad for about that long as well. He’s a good guy, a solid coder, and one of the few people I would trust with VoodooPad’s future.

This is great news for fans of VoodooPad.

I’m such a fan that about a year ago I considered making Gus an offer and buying it. Gus wasn’t even considering selling it at that point (as far as I know) — but I’m a text guy who loves VoodooPad, and it was obvious that Acorn was taking up most of Gus’s time.

Well, I ended up with enough to do that I couldn’t have concentrated on VoodooPad, so I’m very glad that Plausible Labs is taking it over. Great app goes to great home.

Vesper Sync Diary #3 - Immutability, Deleting, and Calculated Properties

Vesper syncing will work via web services.

I admire the folks who make syncing work via flat files. I assume Omni uses some kind of Operational Transformation with OmniPresence (though I don’t know this for sure). The folks at Clear certainly do.

But we’re using web services instead. Because:

  • I’ve been doing web services programming since the ’90s, since before JSON, REST, SOAP, and even XML-RPC. I’ve been a web services guy for my entire career.

  • We don’t want to lock out the possibility of doing a web app, and web apps certainly prefer web services.

  • Syncing one user’s data is not necessarily fundamentally different than how my previous app Glassboard, a group sharing app, worked. While you might not think to use the word “syncing” with Glassboard, it’s the same thing: it makes your local copy the same as what’s on the other clients, the web app, and the server. It syncs an object graph.

  • I’ve done RSS syncing via web services three times. In the process I’ve made a lot of mistakes and learned a ton about syncing.

The above is just to provide background for the things I’m talking about: immutability, deleting objects, and calculated properties.

The Black Beast

The worst part of syncing is merging, where you have two sets of data and need to turn them into one. The right one.

When you see duplicate somethings you know that merging went badly somewhere. Or when you know you entered some data and it disappears later.

So one major goal of a sync design is to limit the amount of merging that needs to be done. Here’s how I’m doing that in Vesper.

The Data Model

There are just four conceptual entities:

Tag
Note
Attachment
Attachment Data

A tag can have many notes; a note can have many tags. A note can have many attachments; an attachment can have just one note. An attachment has one attachment data, and vice versa.

AttachmentData is Immutable

AttachmentData has two properties: uniqueID (which matches the uniqueID of an attachment) and binaryData.

Once created, these cannot be changed. This means the app never has to re-download a picture that may have changed. A different picture (or other attachment data) gets a different uniqueID.

(I’ve worked with APIs where the client had to poll a given endpoint to see if a picture had changed. Ugh. Even with conditional GET this is awful.)

Attachments are Immutable

While note.attachments — the array of attachments for a note — may change, the individual attachments will not change.

A given attachment has several properties: uniqueID, mimeType, height, and width.

This means that the client can ask the server for attachments created since the last sync and just grab those. It never has to re-download or merge attachments the client already has, and it never has to re-upload an attachment that’s already been uploaded.

Attachment and attachment data may sound like a side issue compared to tags and notes, but in terms of bandwidth they’re just about the entire ballgame. (One picture’s data could be larger than the entire database of notes.)

The simpler attachment syncing is, the more I can be sure that attachment syncing is as efficient as possible.

DeletedObjects are Immutable

Okay — there’s one more entity, the DeletedObject.

A DeletedObject has two properties: uniqueID and objectType. The objectType is one of note, tag, or attachment.

One way to support deleting would be to give each entity a deleted property. (Or, in Core Data, probably userDeleted or something that doesn’t conflict with a reserved word.) I chose not to do this, because it means additional mutability for each object type.

Also:

  • It implies that an object could be un-deleted.

  • It implies that you’d keep the data around for that object rather than really delete it.

I’m doing it like this instead: deleting an object creates a DeletedObject. The original object is removed from the database. The DeletedObject is sent to the server, which then removes its copy of the original object from the database. The next client then downloads that DeletedObject from the server and then removes its copy of the original object.

This makes syncing of deleted objects pretty simple: upload the list of local DeletedObjects since the last sync and download the corresponding list from the server. (Probably in just one call with a return value. Not two calls.) Do the deletions specified by the server.

There is no un-deleting.

Now, I know what you’re thinking. What about undo? What about undoing a delete?

The first thing to remember about undo is that it’s not distributed: it’s a local command, which means it’s implemented entirely in the client. Undo also does not persist after an app has been terminated.

Undoing the deletion of a tag or note is simple: just create a new one, with a new uniqueID, that has the exact same data and relationships as the deleted tag or note. (The client will keep the data it needs to be able to make this work.)

Undoing the deletion of an attachment is also simple: just create a new one just like the deleted version. But undoing the deletion of attachment data requires some special handling, since you don’t want to just give that image data a new uniqueID, which would mean uploading (and downloading on other clients) that data again.

But the answer to that is just code: it just means the client should delay committing the deletion of an attachment until undo is no longer possible. From a UI standpoint you’d never notice, since what governs what you see in the app is the note.attachments relationship, not whether or not the attachment is truly deleted or not.

Attachment deleting can be done extremely lazily, in other words.

Not Everything is Immutable

A tag’s name can change in case. (FOO or foo or Foo.) But notes are the main mutable object.

A note can have multiple tags and attachments, and it has the following properties:

uniqueID
text
archived
creationDate
sortDate
detectedData
attachmentThumbnailUniqueID
truncatedText

That’s a lot of mutable stuff. But, even though a note is mutable, it’s worth trying to limit the number of mutable properties that sync. There are two ways to do this:

  1. Find the individual properties that are immutable.

  2. Find the properties that can be calculated on the client, that don’t actually need to sync.

Here are the immutable properties of a note:

uniqueID
creationDate

And here are the properties that can be calculated on the client:

detectedData
attachmentThumbnailUniqueID
truncatedText

This leaves a more manageable set of mutable properties:

text
archived
sortDate
tags relationship
attachments relationship

That’s not nothing. But it’s also not nearly as complex as the problem seems from a first look at the object graph. It’s not bad — just five things.

Which leaves the question: how am I going to sync and merge those properties?

I’ll answer that in a future post.

Identical Cousins 23

In Still Number One Michael and I talk about the successful launch of Fantastical 2 (which is good; you should get it). And we talk about such diverse topics as being Sherlocked, arcade games, the Red Sox, and talking on airplanes (which you shouldn’t ever do under any circumstances).

Along the way, Michael mentioned an interesting development trick.

Fantastical 2 is a new app, but they didn’t want that thing where a new app means you lose all your preferences and have to set up everything all over again.

So what they did is have both versions of Fantastical share a keychain, and they had Fantastical 1 store a copy of some of the preferences in the keychain. Then Fantastical 2 read those preferences back from the keychain — and there you go. No need to set it up again. Nice.