WalnutOps.mesa
Copyright Ó 1984, 1988, 1989, 1992 by Xerox Corporation. All rights reserved.
Willie-Sue, August 1, 1989 9:02:01 pm PDT
Jack Kent May 29, 1987 10:51:46 pm PDT
Top-level Walnut Operations Interface
Last edited by: Donahue on February 2, 1985 9:10:48 am PST
(changed New mail operations around)
(Removed Move operation and changed the types of several other procedures; the major change was to redefine the MsgSet type to include a version stamp -- which may be defaulted. MsgSets now have readlocks associated with them!!)
(Changed new mail operations around again)
(Added complete returned value to GetNewMail)
Last Edited by: Willie-sue, January 4, 1985 10:13:59 am PST
Last Edited by: Wert, August 31, 1984 7:24:05 pm PDT
Implemented by: WalnutOpsImpl
DIRECTORY
BasicTime USING [GMT],
IO USING [STREAM],
Rope USING [ROPE],
SendMailParseMsg USING [ParseProc, MsgHeaders, MsgHeaderItem],
ViewerTools USING [TiogaContents],
WalnutDefs USING [dontCareDomainVersion, dontCareMsgSetVersion, GeneralEnumerator, MsgSet, SeFromToCcSuDaMid, ServerInfo, WalnutOpsHandle];
WalnutOps: CEDAR DEFINITIONS = BEGIN
Types
GMT: TYPE = BasicTime.GMT;
STREAM: TYPE = IO.STREAM;
ROPE: TYPE = Rope.ROPE;
dontCareDomainVersion: INT = WalnutDefs.dontCareDomainVersion;
dontCareMsgSetVersion: INT = WalnutDefs.dontCareMsgSetVersion;
DomainVersion: TYPE = INT ¬ dontCareDomainVersion;
MsgSetVersion: TYPE = INT ¬ WalnutDefs.dontCareMsgSetVersion;
MsgSet: TYPE = WalnutDefs.MsgSet;
MsgSet: TYPE = RECORD[name: ROPE, version: MsgSetVersion];
Message set arguments to WalnutOps procedures take an (optional) version number argument that can be considered a read lock on the message set
ActiveName: ROPE;
DeletedName: ROPE;
ServerInfo: TYPE = WalnutDefs.ServerInfo;
ServerInfo: TYPE = RECORD[server: ROPE, num: INT];
WalnutOpsHandle: TYPE = WalnutDefs.WalnutOpsHandle;
EnumeratorForMsgs: TYPE = REF EnumeratorForMsgsObject;
EnumeratorForMsgsObject: TYPE;
GeneralEnumerator: TYPE = WalnutDefs.GeneralEnumerator;
SeFromToCcSuDaMid: TYPE = WalnutDefs.SeFromToCcSuDaMid;
EntryRef: TYPE = REF EntryObject;
EntryObject: TYPE =
RECORD [seFromToCcSuDaMid: SeFromToCcSuDaMid, msgSetName: ROPE, msgText: ROPE];
ParseProc: TYPE = SendMailParseMsg.ParseProc;
ParseProc: TYPE = PROC[fieldName: ROPE] RETURNS[wantThisOne, continue: BOOL];
MsgHeaders: TYPE = SendMailParseMsg.MsgHeaders;
MsgHeaderItem: TYPE = RECORD[fieldName, value: ROPE];
MsgHeaderItem: TYPE = SendMailParseMsg.MsgHeaderItem;
MsgHeaderItem: TYPE = RECORD[fieldName, value: ROPE];

Procedures
These procedures are all of the primitive atomic actions out of which Walnut is built. WalnutOps actions include:
Starting and stopping Walnut
ValidOpsHandle: PROC[opsHandle: WalnutOpsHandle] RETURNS[isValid: BOOL];
find out is opsHandle has been invalidated
CreateWalnutOpsHandle: PROC[rootFile: ROPE, wantReadOnly: BOOL ¬ FALSE]
RETURNS[opsHandle: WalnutOpsHandle];
this must be called first - will destroy any existing walnutHandle on the same rootFile
GetHandleForRootfile: PROC[rootFile: ROPE] RETURNS[opsHandle: WalnutOpsHandle];
returns NIL if no such handle
Startup: PROC [opsHandle: WalnutOpsHandle] RETURNS[newMailExists: BOOL];
Start up Walnut with a reference to a walnutHandle. This may involve replaying any actions specified in the "tail" of the log (that portion beyond the lastCommit) so that the database and log agree (in fact, if the database does not exist, it may involve reconstruction of the database in its entirety). If wantReadOnly is TRUE, the log and the database are opened for reading only. If the log or the database cannot be written, walnutHandle.readOnly is TRUE upon return.
The startup code also returns information about new mail (see below).
Errors raised:
already started ($AlreadyStarted).
database does not match log ($DatabaseIncomplete).
database has become inaccessible ($DatabaseInaccessible)
Shutdown: PROC[opsHandle: WalnutOpsHandle];
Save the Walnut state and shutdown Walnut.
Scavenge: PROC [opsHandle: WalnutOpsHandle] RETURNS[newMailExists: BOOL];
Erases the database referenced in walnutHandle and initializes the schemaVersion date; then the log is read into the database; need NOT call Shutdown before doing an Erase; generates an error if the database is ReadOnly
Information about the Walnut database
SizeOfDatabase: PROC[opsHandle: WalnutOpsHandle] RETURNS[messages, msgSets: INT];
LogLength: PROC[opsHandle: WalnutOpsHandle] RETURNS[length: INT];
MsgSetsInfo: PROC[opsHandle: WalnutOpsHandle]
RETURNS[version: DomainVersion, num: INT];
version changes whenever a msgSet is created or destroyed. Num is the total number
RegisterReporter: PROC[opsHandle: WalnutOpsHandle, reportStream: STREAM];
used to report progress of long running operations like scavenge or expunge;
UnregisterReporter: PROC[opsHandle: WalnutOpsHandle, reportStream: STREAM];
unregisters reportStream
Adding new messages to the database
StartNewMail: PROC[opsHandle: WalnutOpsHandle] RETURNS[newMailStream: STREAM];
Get the right to read new mail from Grapevine to the New Mail log. This operation gives access to the newMailLog. If newMailStream is NIL, either it could not be opened or it was already in use.
GetNewMail: PROC[opsHandle: WalnutOpsHandle, activeVersion: MsgSetVersion, proc: PROC [msg, tocEntry: ROPE, startOfSubject: INT] ¬ NIL ] RETURNS[responses: LIST OF ServerInfo, complete: BOOL];
GetNewMail takes a version of the active message set and enumerates the messages that are currently "unaccepted" This is solely a database query and does not change the state of the Walnut database (subsequent GetNewMail operations will return the same result until an AcceptNewMail is done). It may fail either because the of a version stamp mismatch or because of database unavailability. If complete is true, then all of the new messages available were retrieved; if false, there is still new mail in the system.
AcceptNewMail: PROC[opsHandle: WalnutOpsHandle, activeVersion: MsgSetVersion];
Accept accepts all currently unaccepted messages and resets the server response entries in the database -- it also increments the version number of the active message set.
RecordNewMailInfo: PROC[opsHandle: WalnutOpsHandle, logLen: INT, server: ROPE, num: INT];
The length given must be the length of a legal tail of a newMailLog
EndNewMail: PROC[opsHandle: WalnutOpsHandle];
Release the new mail log (close the stream)
CreateMsg: PROC[opsHandle: WalnutOpsHandle, msgName: ROPE, body: ViewerTools.TiogaContents];
Log a message from some agent other than Grapevine and add it to the database. Body is expected to conform to the syntax for messages. The first message on the log with a given name wins. Any subsequent messages with the same name are dropped on the floor.
Space conservation (removing unreferenced messages)
ExpungeMsgs: PROC[opsHandle: WalnutOpsHandle, deletedVersion: MsgSetVersion]
RETURNS[bytesInDestroyedMsgs: INT];
Removes all messages from the Deleted message set.
GetExpungeInfo: PROC[opsHandle: WalnutOpsHandle]
RETURNS[firstDestroyedMsgPos, bytesInDestroyedMsgs: INT];
returns the the log position of the first destroyed msg and the number of bytes in destroyed msgs. The actual amount of space reclaimed during an expunge will probably be more (entry overhead, other entries referring to destroyed msgs)
CopyToExpungeLog: PROC[opsHandle: WalnutOpsHandle] RETURNS[copyDone: BOOL];
The log is re-written, starting from the beginning, unless there was not enough quota - then copyDone is false
GetTimeOfLastExpunge: PROC[opsHandle: WalnutOpsHandle] RETURNS[when: GMT];
Primitive message set operations
CreateMsgSet: PROC[opsHandle: WalnutOpsHandle, name: ROPE, msDomainVersion: DomainVersion];
Create a message set with name msgSet. If the named message set exists, this is a noop.
MsgSetExists: PROC[opsHandle: WalnutOpsHandle, name: ROPE, msDomainVersion: DomainVersion]
RETURNS[exists: BOOL, version: MsgSetVersion];
SizeOfMsgSet: PROC[opsHandle: WalnutOpsHandle, name: ROPE]
RETURNS[messages: INT, version: MsgSetVersion];
EmptyMsgSet: PROC[opsHandle: WalnutOpsHandle, msgSet: MsgSet]
RETURNS[someInDeleted: BOOL];
Remove all messages from msgSet. Messages that are no longer in any message set will be added to "Deleted".
msgSet is "Deleted" (do nothing).
messages added to "Deleted" (return with someInDeleted TRUE).
DestroyMsgSet: PROC[opsHandle: WalnutOpsHandle, msgSet: MsgSet, msDomainVersion: DomainVersion]
RETURNS[someInDeleted: BOOL];
Remove the specified message set from the database.
msgSet must exist (ignore otherwise).
msgSet is "Active" (same as EmptyMsgSet).
msgSet is "Deleted" (do nothing).
EnumerateMsgsInMsgSet: PROC[opsHandle: WalnutOpsHandle, name: ROPE]
RETURNS[enum: EnumeratorForMsgs];
a lazy enumerator for the messages in the msgSet name, at the time of the call. The enumerator is valid until the next time the msgSet's version changes (by adding or deleting msgs); its use after that will cause a WalnutDefs.Error with code $InvalidEnumerator. See NextMsg for details.
EnumerateMsgs: PROC[opsHandle: WalnutOpsHandle] RETURNS[enum: EnumeratorForMsgs];
a lazy enumerator for the messages in the database, at the time of the call. The enumerator is valid until the next expunge; its use after that will cause a WalnutDefs.Error with code $InvalidEnumerator. See NextMsg for details.
NextMsg: PROC [enum: EnumeratorForMsgs]
RETURNS[msgID: ROPE, msList: LIST OF ROPE, headers: REF TEXT];
a lazy enumerator for the messages in the database or in a msgSet, at the time of the call. When the end of the messages is reached, msgID will be NIL. msList is the list of message sets the msg belongs to, at the time of the call. headers contains just the headers information from the message. Be warned - subsequent calls to NextMsg may reuse the REF TEXT.
MsgsEnumeration: PROC[opsHandle: WalnutOpsHandle, alphaOrder: BOOL ¬ TRUE]
RETURNS[mL: LIST OF ROPE];
MsgSetNames: PROC [opsHandle: WalnutOpsHandle, alphaOrder: BOOL ¬ TRUE]
RETURNS[mL: LIST OF ROPE, msDomainVersion: DomainVersion];
MsgsInSetEnumeration: PROC [opsHandle: WalnutOpsHandle, name: ROPE, fromStart: BOOL¬ TRUE]
RETURNS [mL: LIST OF ROPE, msVersion: MsgSetVersion];
EnumerateMsgSets: PROC [opsHandle: WalnutOpsHandle, alphaOrder: BOOL ¬ TRUE, proc: PROC [msgSet: MsgSet] RETURNS[continue: BOOL] ]
RETURNS [msDomainVersion: DomainVersion];
NOTE: proc may not do ANY database calls
EnumerateMsgsInSet: PROC [
opsHandle: WalnutOpsHandle,
name: ROPE,
fromStart: BOOL¬ TRUE,
proc: PROC [msg, tocEntry: ROPE, hasBeenRead: BOOL, startOfSubject: INT] RETURNS[continue: BOOL] ]
RETURNS [msVersion: MsgSetVersion];
for each msg in msgSet, will call proc with the indicated information about the msg. NOTE: proc may not do ANY database calls;
Parsing a message
ParseHeaders: PROC [opsHandle: WalnutOpsHandle, headers: ROPE, proc: ParseProc]
RETURNS[msgHeaders: MsgHeaders];
proc is called for each fieldName encountered in the headers; if proc is NIL then all headers are returned. It is ok to take the REF TEXT returned by NextMsg or GetMsgText and use it, wrapped in a RefText.TrustTextAsRope.
Primitive message operations
MsgExists: PROC [opsHandle: WalnutOpsHandle, msg: ROPE] RETURNS[exists: BOOL];
GetCategories: PROC [opsHandle: WalnutOpsHandle, msg: ROPE] RETURNS[LIST OF ROPE];
GetDisplayProps: PROC [opsHandle: WalnutOpsHandle, msg: ROPE]
RETURNS[hasBeenRead: BOOL, tocEntry: ROPE, startOfSubject: INT];
GetMsgDate: PROC [opsHandle: WalnutOpsHandle, msg: ROPE] RETURNS[date: GMT];
returns the date used to index the message
GetMsg: PROC [opsHandle: WalnutOpsHandle, msg: ROPE]
RETURNS[contents: ViewerTools.TiogaContents, herald, shortName: ROPE];
GetMsgHeaders: PROC [opsHandle: WalnutOpsHandle, msg: ROPE, text: REF TEXT]
RETURNS[headers: REF TEXT];
returns just the headers part of the msg
contents will be a new REF TEXT if text was not big enough
GetMsgText: PROC [opsHandle: WalnutOpsHandle, msg: ROPE, text: REF TEXT]
RETURNS[contents: REF TEXT];
returns just the contents part of the msg (not the formatting information); note that the contents of comment nodes will NOT be included.
contents will be a new REF TEXT if text was not big enough
GetMsgShortName: PROC [opsHandle: WalnutOpsHandle, msg: ROPE]
RETURNS[shortName: ROPE];
GetMsgSize: PROC [opsHandle: WalnutOpsHandle, msg: ROPE]
RETURNS[textLen, formatLen: INT];
returns the size of the text and formatting information of a message
GetIsInReplyTo: PROC [opsHandle: WalnutOpsHandle, msg: ROPE] RETURNS[isInReplyTo: BOOL];
returns TRUE if the subject field of this message started with "Re: "
GetHasBeenRead: PROC [opsHandle: WalnutOpsHandle, msg: ROPE]
RETURNS[hadBeenRead: BOOL];
SetHasBeenRead: PROC [opsHandle: WalnutOpsHandle, msg: ROPE];
GenerateUniqueMsgName: PROC[opsHandle: WalnutOpsHandle] RETURNS[msg: ROPE];
creates a unique (for this database at this time) msg entity name
GenerateEntriesPlusDate: PROC [
opsHandle: WalnutOpsHandle,
attr: ATOM,
start: ROPE ¬ NIL,
end: ROPE ¬ NIL,
dateStart: ROPE ¬ NIL,
dateEnd: ROPE ¬ NIL ]
RETURNS [genEnum: GeneralEnumerator];
a lot like EnumerateMsgsInMsgSet, but more general...
NextEntry: PROC [genEnum: GeneralEnumerator] RETURNS[entry: EntryRef];
if msgID is NIL and valid is true, then the enumeration is finished; if valid is false, the msgSet version numbers don't match
More complex message set / message operations
AddMsg: PROC [opsHandle: WalnutOpsHandle, msg: ROPE, from, to: MsgSet]
RETURNS [exists: BOOL];
Add the message (msg) from the message set (from) to the message set (to).
msg must exist and be in from (ignore).
to must exist (ignore) and the versions must agree.
from must exist (ignore) and the versions agree
to must not be "Deleted" (ignore).
msg is already in to (return with exists TRUE)
MoveMsg: PROC [opsHandle: WalnutOpsHandle, msg: ROPE, from, to: MsgSet]
RETURNS [exists: BOOL];
Move the message (msg) from the message set (from) to the message set (to).
msg must exist and be in from (ignore).
to must exist (ignore) and the versions must agree.
from must exist (ignore) and the versions agree
to must not be "Deleted" (ignore).
msg is already in to (return with exists TRUE)
RemoveMsg: PROC [opsHandle: WalnutOpsHandle, msg: ROPE, from: MsgSet, deletedVersion: MsgSetVersion] RETURNS [deleted: BOOL];
Remove the message (msg) from the message set (from). If this action results in the message not belonging to any message set, it will be placed in the Deleted message set.
msg must exist (ignore).
from must exist (ignore).
from must not be "Deleted" (ignore).
msg must be in from (ignore).
msg was added to "Deleted" (the deleted version stamp agrees with the database and nowInDeleted is TRUE).
Producing and reading archive files
Reading an archive file is a long operation; it is resilient, so that it will complete when restarted. The file is first written to a readArchiveLog (if the archive file exists), then this log is copied to the current log and then the tail of the log is "replayed", which may involve creating new messages and shuffling them around in the database.
ReadArchiveFile: PROC [opsHandle: WalnutOpsHandle, file: ROPE, msgSet: MsgSet]
RETURNS[numNew: INT];
Write a "readArchiveFile" log entry; if the archiveFile exists, parses it and writes a separate readArchiveLog with appropriate entries (new msgs and moves if msgSet is not "Active" (NIL defaults to categories specified in the file). The archiveLog is then copied to the tail of the currentLog and the log is parsed. IF the file couldn't be read, numNew = -1; if there were problems parsing file, messages are printed
WriteArchiveFile: PROC [opsHandle: WalnutOpsHandle, file: ROPE, msgSetList: LIST OF MsgSet, append: BOOL ¬ FALSE] RETURNS[ok: BOOL];
Write an archive file that contains the messages from the given message sets. No log entry is written and no updates are made to the database (we just hold the monitor to guarantee that no changes to the message sets occur).
END.