- enum.js - invariants
- errors.js - local Error types
- link-helper.js - processes Link headers
- - logger.js - a very simple logging class
+ - logger/ - adds service-specific data filters to our logging module
- manager.js - process incoming requests
- service.js - defines incoming endpoints, linking the API server framework to the manager methods
- template/ - HTML content
<polygon fill="white" stroke="transparent" points="-4,4 -4,-1079.63 918,-1079.63 918,4 -4,4"/>
<text text-anchor="middle" x="457" y="-1050.83" font-family="Times,serif" font-size="26.00">Websub Hub Entity-Relations</text>
<text text-anchor="middle" x="457" y="-1021.83" font-family="Times,serif" font-size="26.00">Postgres</text>
-<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.0.3</text>
+<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.0.4</text>
<!-- topic -->
<g id="node1" class="node">
<title>topic</title>
<text text-anchor="start" x="92.5" y="-335.93" font-family="Times,serif" font-size="14.00">content_hash</text>
<polygon fill="none" stroke="black" points="0,-308.13 0,-329.13 280,-329.13 280,-308.13 0,-308.13"/>
<text text-anchor="start" x="93.5" y="-314.93" font-family="Times,serif" font-size="14.00">content_type</text>
+<polygon fill="none" stroke="black" points="0,-287.13 0,-308.13 280,-308.13 280,-287.13 0,-287.13"/>
+<text text-anchor="start" x="105.5" y="-293.93" font-family="Times,serif" font-size="14.00">http_etag</text>
+<polygon fill="none" stroke="black" points="0,-266.13 0,-287.13 280,-287.13 280,-266.13 0,-266.13"/>
+<text text-anchor="start" x="73.5" y="-272.93" font-family="Times,serif" font-size="14.00">http_last_modified</text>
</g>
<!-- topic_fetch_in_progress -->
<g id="node2" class="node">
<polygon fill="white" stroke="transparent" points="-4,4 -4,-1079.63 918,-1079.63 918,4 -4,4"/>
<text text-anchor="middle" x="457" y="-1050.83" font-family="Times,serif" font-size="26.00">Websub Hub Entity-Relations</text>
<text text-anchor="middle" x="457" y="-1021.83" font-family="Times,serif" font-size="26.00">SQLite</text>
-<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.0.3</text>
+<text text-anchor="middle" x="457" y="-992.83" font-family="Times,serif" font-size="26.00">Schema 1.0.4</text>
<!-- topic -->
<g id="node1" class="node">
<title>topic</title>
<text text-anchor="start" x="92.5" y="-335.93" font-family="Times,serif" font-size="14.00">content_hash</text>
<polygon fill="none" stroke="black" points="0,-308.13 0,-329.13 280,-329.13 280,-308.13 0,-308.13"/>
<text text-anchor="start" x="93.5" y="-314.93" font-family="Times,serif" font-size="14.00">content_type</text>
+<polygon fill="none" stroke="black" points="0,-287.13 0,-308.13 280,-308.13 280,-287.13 0,-287.13"/>
+<text text-anchor="start" x="105.5" y="-293.93" font-family="Times,serif" font-size="14.00">http_etag</text>
+<polygon fill="none" stroke="black" points="0,-266.13 0,-287.13 280,-287.13 280,-266.13 0,-266.13"/>
+<text text-anchor="start" x="73.5" y="-272.93" font-family="Times,serif" font-size="14.00">http_last_modified</text>
</g>
<!-- topic_fetch_in_progress -->
<g id="node2" class="node">
const acceptPreferred = [topic.contentType, acceptWildcard].filter((x) => x).join(', ');
return Communication._axiosConfig('GET', topic.url, undefined, {}, {
[Enum.Header.Accept]: acceptPreferred,
+ ...(topic.httpEtag && { [Enum.Header.IfNoneMatch]: topic.httpEtag }),
+ ...(topic.httpLastModified && { [Enum.Header.IfModifiedSince]: topic.httpLastModified }),
});
}
switch (common.httpStatusCodeClass(response.status)) {
case 2:
+ case 3:
// Fall out of switch on success
break;
return;
}
+ if (response.status === 304) {
+ this.logger.info(_scope, 'content has not changed, per server', logInfoData);
+ await this.db.topicFetchComplete(dbCtx, topicId);
+ return;
+ }
+
const contentHash = Communication.contentHash(response.data, topic.contentHashAlgorithm);
logInfoData.contentHash = contentHash;
if (topic.contentHash === contentHash) {
}
const contentType = response.headers[Enum.Header.ContentType.toLowerCase()];
+ const httpETag = response.headers[Enum.Header.ETag.toLowerCase()];
+ const httpLastModified = response.headers[Enum.Header.LastModified.toLowerCase()];
await this.db.transaction(dbCtx, async (txCtx) => {
await this.db.topicSetContent(txCtx, {
content: Buffer.from(response.data),
contentHash,
...(contentType && { contentType }),
+ ...(httpETag && { httpETag }),
+ ...(httpLastModified && { httpLastModified }),
});
await this.db.topicFetchComplete(txCtx, topicId);
this._ensureTypes(data, ['content'], ['string', 'buffer']);
this._ensureTypes(data, ['contentHash'], ['string']);
this._ensureTypes(data, ['contentType'], ['string', 'null', 'undefined']);
+ this._ensureTypes(data, ['eTag'], ['string', 'null', 'undefined']);
+ this._ensureTypes(data, ['lastModified'], ['string', 'null', 'undefined']);
}
* @param {String} data.content
* @param {String} data.contentHash
* @param {String=} data.contentType
+ * @param {String=} data.eTag
+ * @param {String=} data.lastModified
*/
async topicSetContent(dbCtx, data) {
this._notImplemented('topicSetContent', arguments);
max: {
major: 1,
minor: 0,
- patch: 3,
+ patch: 4,
},
};
const _scope = _fileScope('topicSetContent');
const topicSetContentData = {
contentType: null,
+ httpETag: null,
+ httpLastModified: null,
...data,
};
const logData = {
if (result.rowCount != 1) {
throw new DBErrors.UnexpectedResult('did not set topic content');
}
- result = await dbCtx.result(this.statement.topicSetContentHistory, { topicId: data.topicId, contentHash: data.contentHash, contentSize: data.content.length });
+ result = await dbCtx.result(this.statement.topicSetContentHistory, {
+ topicId: data.topicId,
+ contentHash: data.contentHash,
+ contentSize: data.content.length,
+ });
if (result.rowCount != 1) {
throw new DBErrors.UnexpectedResult('did not set topic content history');
}
--- /dev/null
+BEGIN;
+
+ ALTER TABLE topic
+ ADD COLUMN http_etag TEXT,
+ ADD COLUMN http_last_modified TEXT
+ ;
+
+ INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 0, 4);
+
+COMMIT;
\ No newline at end of file
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\nPostgres\nSchema 1.0.4",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ <tr><td port="">http_etag</td></tr>
+ <tr><td port="">http_last_modified</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ topic_content_history [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_CONTENT_HISTORY</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content_size</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_content_history:fk_topic_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">latest_content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id;
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ </table>
+ >];
+
+}
\ No newline at end of file
--- /dev/null
+BEGIN;
+
+ ALTER TABLE topic
+ DROP COLUMN http_etag,
+ DROP COLUMN http_last_modified
+ ;
+
+ DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 0 AND PATCH = 4;
+
+COMMIT;
content_updated = now(),
content = $(content),
content_hash = $(contentHash),
- content_type = $(contentType)
+ content_type = $(contentType),
+ http_etag = $(httpETag),
+ http_last_modified = $(httpLastModified)
WHERE id = $(topicId)
max: {
major: 1,
minor: 0,
- patch: 3,
+ patch: 4,
},
};
const _scope = _fileScope('topicSetContent');
const topicSetContentData = {
contentType: null,
+ httpETag: null,
+ httpLastModified: null,
...data,
};
const logData = {
if (result.changes != 1) {
throw new DBErrors.UnexpectedResult('did not set topic content');
}
- result = this.statement.topicSetContentHistory.run({ topicId: data.topicId, contentHash: data.contentHash, contentSize: data.content.length });
+ result = this.statement.topicSetContentHistory.run({
+ topicId: data.topicId,
+ contentHash: data.contentHash,
+ contentSize: data.content.length,
+ });
if (result.changes != 1) {
throw new DBErrors.UnexpectedResult('did not set topic content history');
}
--- /dev/null
+BEGIN;
+
+ ALTER TABLE topic ADD COLUMN http_etag TEXT;
+ ALTER TABLE topic ADD COLUMN http_last_modified TEXT;
+
+ INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 0, 4);
+
+COMMIT;
--- /dev/null
+digraph WebsubHubERD {
+ graph[
+ rankdir=LR,
+ overlap=false,
+ splines=true,
+ label="Websub Hub Entity-Relations\nSQLite\nSchema 1.0.4",
+ labelloc="t",
+ fontsize=26,
+ ];
+ // layout=neato;
+ node[shape=plain];
+ edge[arrowhead=crow];
+
+ topic [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">url</td></tr>
+ <tr><td port="">lease_seconds_preferred</td></tr>
+ <tr><td port="">lease_seconds_min</td></tr>
+ <tr><td port="">lease_seconds_max</td></tr>
+ <tr><td port="">publisher_validation_url</td></tr>
+ <tr><td port="">content_hash_algorithm</td></tr>
+ <tr><td port="">is_active</td></tr>
+ <tr><td port="">is_deleted</td></tr>
+ <tr><td port="">last_publish</td></tr>
+ <tr><td port="">content_fetch_next_attempt</td></tr>
+ <tr><td port="">content_fetch_attempts_since_success</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ <tr><td port="">content_type</td></tr>
+ <tr><td port="">http_etag</td></tr>
+ <tr><td port="">http_last_modified</td></tr>
+ </table>
+ >];
+
+ topic_fetch_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_FETCH_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_fetch_in_progress:fk_id;
+
+ topic_content_history [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">TOPIC_CONTENT_HISTORY</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">content_updated</td></tr>
+ <tr><td port="">content_size</td></tr>
+ <tr><td port="">content_hash</td></tr>
+ </table>
+ >];
+ topic:pk_id -> topic_content_history:fk_topic_id;
+
+ subscription [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">verified</td></tr>
+ <tr><td port="">expires</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">content_delivered</td></tr>
+ <tr><td port="">latest_content_delivered</td></tr>
+ <tr><td port="">delivery_attempts_since_success</td></tr>
+ <tr><td port="">delivery_next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> subscription:fk_topic_id;
+
+ subscription_delivery_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">SUBSCRIPTION_DELIVERY_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ subscription:pk_id -> subscription_delivery_in_progress:fk_id;
+
+ verification [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION</td></tr>
+ <tr><td port="pk_id">id</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">secret</td></tr>
+ <tr><td port="">signature_algorithm</td></tr>
+ <tr><td port="">http_remote_addr</td></tr>
+ <tr><td port="">http_from</td></tr>
+ <tr><td port="">mode</td></tr>
+ <tr><td port="">reason</td></tr>
+ <tr><td port="">lease_seconds</td></tr>
+ <tr><td port="">is_publisher_validated</td></tr>
+ <tr><td port="">request_id</td></tr>
+ <tr><td port="">attempts</td></tr>
+ <tr><td port="">next_attempt</td></tr>
+ </table>
+ >];
+ topic:pk_id -> verification:fk_topic_id;
+
+ verification_in_progress [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">VERIFICATION_IN_PROGRESS</td></tr>
+ <tr><td port="fk_id">id</td></tr>
+ <tr><td port="fk_topic_id">topic_id</td></tr>
+ <tr><td port="">callback</td></tr>
+ <tr><td port="">claimant</td></tr>
+ <tr><td port="">claimed</td></tr>
+ <tr><td port="">claim_expires</td></tr>
+ </table>
+ >];
+ verification:pk_id -> verification_in_progress:fk_id;
+ topic:pk_id -> verification_in_progress:fk_topic_id;
+
+ authentication [label=<
+ <table cellspacing="0" cellborder="1" border="0">
+ <tr><td border="2" bgcolor="lightblue">AUTHENTICATION</td></tr>
+ <tr><td port="">created</td></tr>
+ <tr><td port="">last_authenticated</td></tr>
+ <tr><td port="">identifier</td></tr>
+ <tr><td port="">credential</td></tr>
+ </table>
+ >];
+
+}
\ No newline at end of file
--- /dev/null
+BEGIN;
+
+ ALTER TABLE topic DROP COLUMN http_etag;
+ ALTER TABLE topic DROP COLUMN http_last_modified;
+
+ DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 0 AND PATCH = 4;
+
+COMMIT;
content_updated = strftime('%s', 'now'),
content = :content,
content_hash = :contentHash,
- content_type = :contentType
+ content_type = :contentType,
+ http_etag = :httpETag,
+ http_last_modified = :httpLastModified
WHERE id = :topicId
headers: {
'content-type': 'text/plain',
link: '<https://example.com/hub/>; rel="hub"',
+ 'last-modified': 'Thu, 18 Nov 2021 20:34:35 GMT',
+ 'etag': '"9c104-1673e-5d1161636d742"',
},
data: 'Jackdaws love my big sphinx of quartz.',
});
assert(!communication.db.topicSetContent.called);
});
+ it('recognizes 304 response', async function () {
+ topic.httpLastModified = 'Thu, 18 Nov 2021 20:34:35 GMT';
+ topic.httpEtag = '"9c104-1673e-5d1161636d742"';
+ communication.db.topicGetById.resolves(topic);
+ communication.axios.resolves({
+ status: 304,
+ });
+
+ await communication.topicFetchProcess(dbCtx, topicId, requestId);
+
+ assert(communication.db.topicFetchComplete.called);
+ assert(!communication.db.topicSetContent.called);
+ });
+
it('updates content', async function () {
await communication.topicFetchProcess(dbCtx, topicId, requestId);