]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Initial proof of concept code for a JavaScript binding which runs in the main window...
authorstephan <stephan@noemail.net>
Wed, 1 Jun 2022 08:09:06 +0000 (08:09 +0000)
committerstephan <stephan@noemail.net>
Wed, 1 Jun 2022 08:09:06 +0000 (08:09 +0000)
FossilOrigin-Name: d9efe3e92d1c95aee6f5ae37a8ba28d8cf4891d746744ce4aa2464f766821a0b

ext/fiddle/sqlite3-api.js
ext/fiddle/testing1.js
ext/fiddle/testing2.js
manifest
manifest.uuid

index e625afc9ba9a14af645542728bba453737df398f..853f16e07bc39a9d9cc8df2aec3bc0a67681c9ec 100644 (file)
@@ -271,33 +271,55 @@ Module.postRun.push(function(namespace/*the module object, the target for
 
        - ()
        - (undefined) (same effect as ())
-       - (Uint8Array holding an sqlite3 db image)
-
-       It always generates a random filename and sets is to
-       the `filename` property of this object.
-
-       Developer's note: the reason it does not (any longer) support
-       ":memory:" as a name is because we can apparently only export
-       images of DBs which are stored in the pseudo-filesystem
-       provided by the JS APIs. Since exporting and importing images
-       is an important usability feature for this class, ":memory:"
-       DBs are not supported (until/unless we can find a way to export
-       those as well). The naming semantics will certainly evolve as
-       this API does.
+       - (filename[,buffer])
+       - (buffer)
+
+       Where a buffer indicates a Uint8Array holding an sqlite3 db
+       image.
+
+       If the filename is provided, only the last component of the
+       path is used - any path prefix is stripped. If no name is
+       provided, a random name is generated. The resulting filename is
+       the one used for accessing the db file within root directory of
+       the emscripten-supplied virtual filesystem, and is set (with no
+       path part) as the DB object's `filename` property.
+
+       Note that the special sqlite3 db names ":memory:" and ""
+       (temporary db) have no special meanings here. We can apparently
+       only export images of DBs which are stored in the
+       pseudo-filesystem provided by the JS APIs. Since exporting and
+       importing images is an important usability feature for this
+       class, ":memory:" DBs are not supported (until/unless we can
+       find a way to export those as well). The naming semantics will
+       certainly evolve as this API does.
     */
     const DB = function(arg){
-        const fn = "db-"+((Math.random() * 10000000) | 0)+
-              "-"+((Math.random() * 10000000) | 0)+".sqlite3";
-        let buffer;
-        if(name instanceof Uint8Array){
+        let buffer, fn;
+        if(arg instanceof Uint8Array){
             buffer = arg;
             arg = undefined;
-        }else if(arguments.length && undefined!==arg){
-            toss("Invalid arguments to DB constructor.",
-                 "Expecting no args, undefined, or a",
-                 "sqlite3 file as a Uint8Array.");
+        }else if(arguments.length){ /*(filename[,buffer])*/
+            if('string'===typeof arg){
+                const p = arg.split('/').pop().replace(':','');
+                if(p) fn = p;
+                if(arguments.length>1){
+                    buffer = arguments[1];
+                }
+            }else if(undefined!==arg){
+                toss("Invalid arguments to DB constructor.",
+                     "Expecting (), (undefined), (name,buffer),",
+                     "or (buffer), where buffer an sqlite3 db ",
+                     "as a Uint8Array.");
+            }
+        }
+        if(!fn){
+            fn = "db-"+((Math.random() * 10000000) | 0)+
+                "-"+((Math.random() * 10000000) | 0)+".sqlite3";
         }
         if(buffer){
+            if(!(buffer instanceof Uint8Array)){
+                toss("Expecting Uint8Array image of db contents.");
+            }
             FS.createDataFile("/", fn, buffer, true, true);
         }
         setValue(pPtrArg, 0, "i32");
@@ -382,7 +404,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
             default: toss("Invalid argument count for exec().");
         };
         if('string'!==typeof out.sql) toss("Missing SQL argument.");
-        if(out.opt.callback){
+        if(out.opt.callback || out.opt.resultRows){
             switch((undefined===out.opt.rowMode)
                     ? 'stmt' : out.opt.rowMode) {
                 case 'object': out.cbArg = (stmt)=>stmt.get({}); break;
@@ -462,16 +484,37 @@ Module.postRun.push(function(namespace/*the module object, the target for
             return stmt;
         },
         /**
-           This function works like execMulti(), and takes the same
-           arguments, but is more efficient (performs much less work)
-           when the input SQL is only a single statement. If passed a
-           multi-statement SQL, it only processes the first one.
+           This function works like execMulti(), and takes most of the
+           same arguments, but is more efficient (performs much less
+           work) when the input SQL is only a single statement. If
+           passed a multi-statement SQL, it only processes the first
+           one.
 
-           This function supports one additional option not used by
-           execMulti():
+           This function supports the following additional options not
+           used by execMulti():
 
            - .multi: if true, this function acts as a proxy for
              execMulti() and behaves identically to that function.
+
+           - .resultRows: if this is an array, each row of the result
+             set (if any) is appended to it in the format specified
+             for the `rowMode` property, with the exception that the
+             `rowMode` property _must_ be one of 'array' or 'object'
+             if this is set (else an exception is throws). It is legal
+             to use both `resultRows` and `callback`, but `resultRows`
+             is likely much simpler to use for small data sets.
+
+           - .columnNames: if this is an array and the query has
+             result columns, the array is passed to
+             Stmt.getColumnNames() to append the column names to it
+             (regardless of whether the query produces any result
+             rows). If the query has no result columns, this value is
+             unchanged.
+
+           The following options to execMulti() are _not_ supported by
+           this method (they are simply ignored):
+
+          - .saveSql
         */
         exec: function(/*(sql [,optionsObj]) or (optionsObj)*/){
             affirmDbOpen(this);
@@ -481,15 +524,29 @@ Module.postRun.push(function(namespace/*the module object, the target for
                 return this.execMulti(arg, undefined, BindTypes);
             }
             const opt = arg.opt;
-            let stmt;
+            let stmt, rowTarget;
             try {
+                if(Array.isArray(opt.resultRows)){
+                    if(opt.rowMode!=='array' && opt.rowMode!=='object'){
+                        throw new Error("Invalid rowMode for resultRows array: must "+
+                                        "be one of 'array' or 'object'.");
+                    }
+                    rowTarget = opt.resultRows;
+                }
                 stmt = this.prepare(arg.sql);
+                if(stmt.columnCount && Array.isArray(opt.columnNames)){
+                    stmt.getColumnNames(opt.columnNames);
+                }
                 if(opt.bind) stmt.bind(opt.bind);
-                if(opt.callback){
+                if(opt.callback || rowTarget){
                     while(stmt.step()){
-                        stmt._isLocked = true;
-                        opt.callback(arg.cbArg(stmt), stmt);
-                        stmt._isLocked = false;
+                        const row = arg.cbArg(stmt);
+                        if(rowTarget) rowTarget.push(row);
+                        if(opt.callback){
+                            stmt._isLocked = true;
+                            opt.callback(row, stmt);
+                            stmt._isLocked = false;
+                        }
                     }
                 }else{
                     stmt.step();
@@ -504,10 +561,11 @@ Module.postRun.push(function(namespace/*the module object, the target for
 
         }/*exec()*/,
         /**
-           Executes one or more SQL statements. Its arguments
-           must be either (sql,optionsObject) or (optionsObject).
-           In the latter case, optionsObject.sql must contain the
-           SQL to execute. Returns this object. Throws on error.
+           Executes one or more SQL statements in the form of a single
+           string. Its arguments must be either (sql,optionsObject) or
+           (optionsObject). In the latter case, optionsObject.sql
+           must contain the SQL to execute. Returns this
+           object. Throws on error.
 
            If no SQL is provided, or a non-string is provided, an
            exception is triggered. Empty SQL, on the other hand, is
@@ -548,6 +606,9 @@ Module.postRun.push(function(namespace/*the module object, the target for
              don't have the string until after that). Empty SQL
              statements are elided.
 
+           See also the exec() method, which is a close cousin of this
+           one.
+
            ACHTUNG #1: The callback MUST NOT modify the Stmt
            object. Calling any of the Stmt.get() variants,
            Stmt.getColumnName(), or similar, is legal, but calling
@@ -806,8 +867,13 @@ Module.postRun.push(function(namespace/*the module object, the target for
         /**
            Exports a copy of this db's file as a Uint8Array and
            returns it. It is technically not legal to call this while
-           any prepared statement are currently active. Throws if this
-           db is not open.
+           any prepared statement are currently active because,
+           depending on the platform, it might not be legal to read
+           the db while a statement is locking it. Throws if this db
+           is not open or has any opened statements.
+
+           The resulting buffer can be passed to this class's
+           constructor to restore the DB.
 
            Maintenance reminder: the corresponding sql.js impl of this
            feature closes the current db, finalizing any active
@@ -825,8 +891,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
                 toss("Cannot export with prepared statements active!",
                      "finalize() all statements and try again.");
             }
-            const img = FS.readFile(this.filename, {encoding:"binary"});
-            return img;
+            return FS.readFile(this.filename, {encoding:"binary"});
         }
     }/*DB.prototype*/;
 
@@ -1397,8 +1462,13 @@ Module.postRun.push(function(namespace/*the module object, the target for
         SQLite3
     };
 
+    const postApiLoaded = function(){
+        setTimeout(()=>postMessage({type:'sqlite3-api',data:'loaded'}), 0);
+    };
+
     if(self === self.window){
         /* This is running in the main window thread, so we're done. */
+        postApiLoaded();
         return;
     }
     /******************************************************************
@@ -1407,24 +1477,108 @@ Module.postRun.push(function(namespace/*the module object, the target for
     ******************************************************************/
 
     /*
-      TODO: we need an API which can proxy the DB API via a Worker
-      message interface. The primary quirky factor in such an API is
-      that clients cannot pass callback functions to it, so have to receive
-      all query results via asynchronous message-passing.
+      UNDER CONSTRUCTION
+
+      We need an API which can proxy the DB API via a Worker message
+      interface. The primary quirky factor in such an API is that we
+      cannot pass callback functions between the window thread and a
+      worker thread, so we have to receive all db results via
+      asynchronous message-passing.
 
       Certain important considerations here include:
 
-      - Support only one db connectior or multiple? The former is far
+      - Support only one db connection or multiple? The former is far
         easier, but there's always going to be a user out there who
-        wants to juggle six database handles at once.
+        wants to juggle six database handles at once. Do we add that
+        complexity or tell such users to write their own code using
+        the provided lower-level APIs?
 
       - Fetching multiple results: do we pass them on as a series of
         messages, with start/end messages on either end, or do we
         collect all results and bundle them back in a single message?
         The former is, generically speaking, more memory-efficient but
-        the latter far easier to implement in this environment.
-     */
-    
+        the latter far easier to implement in this environment. The
+        latter is untennable for large data sets. Despite a web page
+        hypothetically being a relatively limited environment, there
+        will always be those users who feel that they should/need to
+        be able to work with multi-hundred-meg (or larger) blobs, and
+        passing around arrays of those may quickly exhaust the JS
+        engine's memory.
+    */
+
+    const wState = {
+        db: undefined,
+        open: function(arg){
+            if(!arg && this.db) return this.db;
+            else if(this.db) this.db.close();
+            return this.db = (Array.isArray(arg) ? new DB(...arg) : new DB(arg));
+        }
+    };
+    const wMsg = (type,data)=>self.postMessage({type, data});
+
+    /**
+       UNDER CONSTRUCTION:
+
+       A subset of the DB API is accessible via Worker messages in the form:
+
+       { type: apiCommand,
+         data: apiArguments }
+
+       As a rule, these commands respond with a postMessage() of their
+       own in the same form, but will, if needed, transform the `data`
+       member to an object and may add state to it. The responses
+       always have an object-format `data` part. If the inbound `data`
+       is an object which has a `messageId` property, that property is
+       always mirrored in the result object, for use in client-side
+       dispatching of these asynchronous results. Exceptions thrown
+       during processing result in an `error`-type event with a
+       payload in the form:
+
+       {message: error string,
+        errorClass: class name of the error type,
+        [, messageId: if set in the inbound message]}
+
+       The individual APIs will be documented as they are fleshed out.
+    */
+    self.onmessage = function(ev){
+        ev = ev.data;
+        let response = {}, evType = ev.type;
+        try {
+            switch(evType){
+                case 'open': {
+                    const args = [], data = (ev.data || {});
+                    if(data.filename) args.push(data.filename);
+                    if(data.buffer) args.push(data.buffer);
+                    const d = wState.open(args);
+                    response.filename = d.filename;
+                    response.messageId = data.messageId;
+                    break;
+                }
+                case 'exec': {
+                    const opt = (
+                        'string'===typeof ev.data
+                    ) ? {sql: ev.data} : (ev.data || {});
+                    if(!opt.rowMode) opt.rowMode = 'array';
+                    wState.open().exec(opt);
+                    response = opt;
+                    break;
+                }
+                default:
+                    throw new Error("Unknown db worker message type: "+ev.type);
+            }
+        }catch(err){
+            evType = 'error';
+            response = {
+                message: err.message,
+                errorClass: err.name
+            };
+        }
+        if(!response.messageId && ev.data
+           && 'object'===typeof ev.data && ev.data.messageId){
+            response.messageId = ev.data.messageId;
+        }
+        wMsg(evType, response);
+    };
 
-    setTimeout(()=>postMessage({type:'sqlite3-api',data:'loaded'}), 0);
+    postApiLoaded();
 });
index e4afad84eb67a7fa600d3602c7c61efe890c5be1..46af2c926b01f14b4555ffaa4cdd51a395a548b3 100644 (file)
@@ -71,23 +71,27 @@ INSERT INTO t(a,b) VALUES(1,2),(3,4),(?,?);`,
         T.assert(2 === list.length);
         //log("Exec'd SQL:", list);
         let counter = 0, colNames = [];
+        list.length = 0;
         db.exec("SELECT a a, b b FROM t",{
             rowMode: 'object',
+            resultRows: list,
+            columnNames: colNames,
             callback: function(row,stmt){
-                if(!counter) stmt.getColumnNames(colNames);
                 ++counter;
                 T.assert(row.a%2 && row.a<6);
             }
         });
-        assert(2 === colNames.length);
-        assert('a' === colNames[0]);
-        T.assert(3 === counter);
+        T.assert(2 === colNames.length)
+            .assert('a' === colNames[0])
+            .assert(3 === counter)
+            .assert(3 === list.length);
+        list.length = 0;
         db.exec("SELECT a a, b b FROM t",{
             rowMode: 'array',
             callback: function(row,stmt){
                 ++counter;
-                assert(Array.isArray(row));
-                T.assert(0===row[1]%2 && row[1]<7);
+                T.assert(Array.isArray(row))
+                    .assert(0===row[1]%2 && row[1]<7);
             }
         });
         T.assert(6 === counter);
index c25885425c826f85fb051856576a3bf55d6e35ca..de9c92564852d49ada97fcf81b93ef4839e4e13e 100644 (file)
   A basic test script for sqlite3-worker.js.
 */
 (function(){
-    /** Posts a worker message as {type:type, data:data}. */
+    const T = self.SqliteTestUtil;
     const SW = new Worker("sqlite3-worker.js");
-    const wMsg = (type,data)=>SW.postMessage({type, data});
+    /** Posts a worker message as {type:type, data:data}. */
+    const wMsg = function(type,data){
+        SW.postMessage({type, data});
+        return SW;
+    };
     const log = console.log.bind(console);
     const warn = console.warn.bind(console);
+    const error = console.error.bind(console);
+
+    SW.onerror = function(event){
+        warn("onerror",event);
+    };
+
+    const MsgHandlerQueue = {
+        queue: [],
+        id: 0,
+        push: function(type,f){
+            this.queue.push(f);
+            return type + '-' + (++this.id);
+        },
+        shift: function(){
+            return this.queue.shift();
+        }
+    };
+
+    const runOneTest = function(eventType, eventData, callback){
+        T.assert(eventData && 'object'===typeof eventData);
+        eventData.messageId = MsgHandlerQueue.push(eventType,function(ev){
+            log("runOneTest",eventType,"result",ev.data);
+            callback(ev);
+        });
+        wMsg(eventType, eventData);
+    };
+
+    const testCount = ()=>log("Total test count:",T.counter);
+
+    const runTests = function(){
+        /**
+           "The problem" now is that the test results are async. We
+           know, however, that the messages posted to the worker will
+           be processed in the order they are passed to it, so we can
+           create a queue of callbacks to handle them. The problem
+           with that approach is that it's not error-handling
+           friendly, in that an error can cause us to bypass a result
+           handler queue entry. We have to perform some extra
+           acrobatics to account for that.
+        */
+        runOneTest('open', {filename:'testing2.sqlite3'}, function(ev){
+            //log("open result",ev);
+            T.assert('testing2.sqlite3'===ev.data.filename)
+                .assert(ev.data.messageId);
+            testCount();
+        });
+        runOneTest('exec',{
+            sql: ["create table t(a,b)",
+                  "insert into t(a,b) values(1,2),(3,4),(5,6)"
+                 ].join(';'),
+            multi: true,
+            resultRows: [],
+            columnNames: []
+        }, function(ev){
+            ev = ev.data;
+            T.assert(0===ev.resultRows.length)
+                .assert(0===ev.columnNames.length);
+            testCount();
+        });
+        runOneTest('exec',{
+            sql: 'select a a, b b from t order by a',
+            resultRows: [], columnNames: []
+        }, function(ev){
+            ev = ev.data;
+            T.assert(3===ev.resultRows.length)
+                .assert(1===ev.resultRows[0][0])
+                .assert(6===ev.resultRows[2][1])
+                .assert(2===ev.columnNames.length)
+                .assert('b'===ev.columnNames[1]);
+            testCount();
+        });
+        runOneTest('exec',{sql:'select 1 from intentional_error'}, function(){
+            throw new Error("This is not supposed to be reached.");
+        });
+        // Ensure that the message-handler queue survives ^^^ that error...
+        runOneTest('exec',{
+            sql:'select 1',
+            resultRows: [],
+            rowMode: 'array',
+        }, function(ev){
+            ev = ev.data;
+            T.assert(1 === ev.resultRows.length)
+                .assert(1 === ev.resultRows[0][0]);
+            testCount();
+        });
+    };
+
+    const dbMsgHandler = {
+        open: function(ev){
+            log("open result",ev.data);
+        },
+        exec: function(ev){
+            log("exec result",ev.data);
+        },
+        error: function(ev){
+            error("ERROR from the worker:",ev.data);
+        }
+    };
+
     SW.onmessage = function(ev){
         if(!ev.data || 'object'!==typeof ev.data){
             warn("Unknown sqlite3-worker message type:",ev);
             return;
         }
         ev = ev.data/*expecting a nested object*/;
+        //log("main window onmessage:",ev);
+        if(ev.data && ev.data.messageId){
+            /* We're expecting a queued-up callback handler. */
+            const f = MsgHandlerQueue.shift();
+            if('error'===ev.type){
+                dbMsgHandler.error(ev);
+                return;
+            }
+            T.assert(f instanceof Function);
+            f(ev);
+            return;
+        }
         switch(ev.type){
             case 'sqlite3-api':
                 switch(ev.data){
                     case 'ready':
                         log("Message:",ev);
                         self.sqlite3TestModule.setStatus(null);
+                        setTimeout(runTests, 0);
+                        return;
+                    default:
+                        warn("Unknown sqlite3-api message type:",ev);
                         return;
-                    default: break;
                 }
-                break;
+            default:
+                if(dbMsgHandler.hasOwnProperty(ev.type)){
+                    try{dbMsgHandler[ev.type](ev);}
+                    catch(err){
+                        error("Exception while handling db result message",
+                              ev,":",err);
+                    }
+                    return;
+                }
+                warn("Unknown sqlite3-api message type:",ev);
         }
-        warn("Unknown sqlite3-api message type:",ev);
     };
+
     log("Init complete, but async bits may still be running.");
 })();
index 4f9ee1b8f171ac4080f54d76db5464bcce877823..f31c52b903389d0adb589f86918dfeb51b0b48e9 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Initial\sbits\sfor\sa\sJS\sAPI\svariant\sin\swhich\sthe\sclient\soperates\sin\sthe\smain\sthread\sand\ssqlite3\sin\sa\sWorker.\sThis\sis\sfar\sfrom\scomplete.
-D 2022-06-01T00:00:59.491
+C Initial\sproof\sof\sconcept\scode\sfor\sa\sJavaScript\sbinding\swhich\sruns\sin\sthe\smain\swindow\sthread\sbut\sacts\son\sa\sdb\shandle\srunning\sin\sa\sWorker\sthread.\sExpanded\sthe\sDB.exec()\sand\sDB()\sconstructor\sto\ssimplify\scertain\suse\scases.
+D 2022-06-01T08:09:06.176
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -65,13 +65,13 @@ F ext/fiddle/fiddle-worker.js 3a19253dc026d1ad9064ee853f3c4da3385223ce4434dab183
 F ext/fiddle/fiddle.html 724f1cd4126616bc87f5871f78d3f7aaaf41e45c9728724627baab87e6af35f0
 F ext/fiddle/fiddle.js 5b456ed7085830cda2fc75a0801476174a978521949335f24bc4154d076dcd4d
 F ext/fiddle/index.md d9c1c308d8074341bc3b11d1d39073cd77754cb3ca9aeb949f23fdd8323d81cf
-F ext/fiddle/sqlite3-api.js ff9580cf075c08bd124ad057079bd32fd121f1e122c8c40e3a836466c1fe1197
+F ext/fiddle/sqlite3-api.js a2c0fa1a30e564a16650e3224a23a60cc8636325028223183933630669ebec8d
 F ext/fiddle/sqlite3-worker.js c137daed6529b5f527ed61eb358cb0d23f90e04784442479cd15ac684eccdf7a
 F ext/fiddle/testing.css 750572dded671d2cf142bbcb27af5542522ac08db128245d0b9fe410aa1d7f2a
 F ext/fiddle/testing1.html ea1f3be727f78e420007f823912c1a03b337ecbb8e79449abc2244ad4fe15d9a
-F ext/fiddle/testing1.js e09c224da12b34bd90a1128d4a9cfc546ccca00074d00571977eb44746bf487d
+F ext/fiddle/testing1.js b5bf7e33b35f02f4208e4d68eaa41e5ed42eaefd57e0a1131e87cba96d4808dc
 F ext/fiddle/testing2.html 9063b2430ade2fe9da4e711addd1b51a2741cf0c7ebf6926472a5e5dd63c0bc4
-F ext/fiddle/testing2.js 0382f20c6c5e1b2a034240a29d627f0e6ccb9c3be50d7dd1627b04ad5f92b787
+F ext/fiddle/testing2.js 15e53ded82e78a5360daa4af109124c81b52eba79be2de241bef6558697931b7
 F ext/fts1/README.txt 20ac73b006a70bcfd80069bdaf59214b6cf1db5e
 F ext/fts1/ft_hash.c 3927bd880e65329bdc6f506555b228b28924921b
 F ext/fts1/ft_hash.h 06df7bba40dadd19597aa400a875dbc2fed705ea
@@ -1974,8 +1974,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
 F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P 13e89ef6649475815d3f4e4aef73a4be1157dd388e55c7f856faeb4b7387774b
-R 1d9b814402b0908178d5c21bd5502d71
+P f6d6f969791f0d2367ae5418623b4794f6df657d9d7d9002fb5aec4206dcfd4c
+R 3e7544f3d688eef8add8c5291273a264
 U stephan
-Z f7d167c241a574fa46d16e96c1270f84
+Z fea18e61d8da4eacf9dfc674d92c9514
 # Remove this line to create a well-formed Fossil manifest.
index d352df1b8e4539ff9360b8056b5e9a8ddc83665f..7d9aef22faf5825dc31ccfa593bdbd2e24bf5af6 100644 (file)
@@ -1 +1 @@
-f6d6f969791f0d2367ae5418623b4794f6df657d9d7d9002fb5aec4206dcfd4c
\ No newline at end of file
+d9efe3e92d1c95aee6f5ae37a8ba28d8cf4891d746744ce4aa2464f766821a0b
\ No newline at end of file