Skip to content

Commit c2ccab9

Browse files
RangerMauveMauve Signweavergmaclennan
authored
Feat/invitor sets device info (#1230)
* feat: Invitor writes DeviceInfo record for invitee * fix: Disable validation, switch device info read/write logic * chore: Remove only from deviceInfo write test * test: Fix config core count in sync tests * fix: Calculate config count for sync test in right place * chore: Remove rpc.infoFor * feat: add peerInfo to invite API * feat: Skip setting own device info if invitor does * chore: Update invite fixtures * test: Adjust sync test config cores back * chore: Prefer device id for device info docs * Update src/member-api.js Co-authored-by: Gregor MacLennan <gmaclennan@awana.digital> * Update src/member-api.js Co-authored-by: Gregor MacLennan <gmaclennan@awana.digital> * chore: Remove deviceInfo validation and its error * fix: Use kCreateWithDocId for deviceInfo, ensure both name and type set on invite * fix: Catch error when loading deviceInfo by deviceId * fix: Only null if not found on datatype get existing for update --------- Co-authored-by: Mauve Signweaver <contact@mauve.moe> Co-authored-by: Gregor MacLennan <gmaclennan@awana.digital>
1 parent 7fe398e commit c2ccab9

File tree

15 files changed

+180
-66
lines changed

15 files changed

+180
-66
lines changed

proto/rpc.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ message Invite {
1212
optional string projectColor = 7;
1313
optional string projectDescription = 8;
1414
bool sendStats = 9;
15+
bool invitorWroteDeviceInfo = 10;
1516
}
1617

1718
message InviteCancel {

src/datatype/index.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {
99
InvalidDocError,
1010
InvalidDocFormatError,
1111
NotFoundError,
12+
nullIfNotFound,
1213
} from '../errors.js'
1314
import { TypedEmitter } from 'tiny-typed-emitter'
1415
import { setProperty, getProperty } from 'dot-prop-extra'
1516
import { parseBcp47 } from '../intl/parse-bcp-47.js'
17+
1618
/** @import { MapeoDoc, MapeoValue } from '@comapeo/schema' */
1719
/** @import { RunResult } from 'better-sqlite3' */
1820
/** @import { SQLiteSelectBase } from 'drizzle-orm/sqlite-core' */
@@ -84,6 +86,7 @@ function generateDate() {
8486
return new Date().toISOString()
8587
}
8688
export const kCreateWithDocId = Symbol('kCreateWithDocId')
89+
export const kCreateOrUpdateWithDocId = Symbol('kCreateWithDocId')
8790
export const kSelect = Symbol('select')
8891
export const kTable = Symbol('table')
8992
export const kDataStore = Symbol('dataStore')
@@ -187,6 +190,20 @@ export class DataType extends TypedEmitter {
187190
return this[kCreateWithDocId](docId, value, { checkExisting: false })
188191
}
189192

193+
/**
194+
* @param {string} docId
195+
* @param {ExcludeSchema<TValue, 'coreOwnership'>} value
196+
* @returns {Promise<TDoc & DerivedDocFields>}
197+
*/
198+
async [kCreateOrUpdateWithDocId](docId, value) {
199+
const existing = await this.getByDocId(docId).catch(nullIfNotFound)
200+
if (existing) {
201+
return this.update(existing.versionId, value)
202+
} else {
203+
return this[kCreateWithDocId](docId, value, { checkExisting: false })
204+
}
205+
}
206+
190207
/**
191208
* @param {string} docId
192209
* @param {ExcludeSchema<TValue, 'coreOwnership'> | CoreOwnershipWithSignaturesValue} value

src/errors.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,6 @@ export const EncryptionKeysNotFoundError = createErrorClass({
120120
status: 400,
121121
})
122122

123-
export const InvalidDeviceInfoError = createErrorClass({
124-
code: 'INVALID_DEVICE_INFO_ERROR',
125-
message:
126-
'Invalid deviceInfo record, cannot write deviceInfo for another device',
127-
status: 400,
128-
})
129-
130123
export const RoleAssignError = createErrorClass({
131124
code: 'ROLE_ASSIGN_ERROR',
132125
message: '{message}',

src/generated/rpc.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface Invite {
1010
projectColor?: string | undefined;
1111
projectDescription?: string | undefined;
1212
sendStats: boolean;
13+
invitorWroteDeviceInfo: boolean;
1314
}
1415
export interface InviteCancel {
1516
inviteId: Buffer;

src/generated/rpc.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ function createBaseInvite() {
122122
projectName: "",
123123
invitorName: "",
124124
sendStats: false,
125+
invitorWroteDeviceInfo: false,
125126
};
126127
}
127128
export var Invite = {
@@ -154,6 +155,9 @@ export var Invite = {
154155
if (message.sendStats === true) {
155156
writer.uint32(72).bool(message.sendStats);
156157
}
158+
if (message.invitorWroteDeviceInfo === true) {
159+
writer.uint32(80).bool(message.invitorWroteDeviceInfo);
160+
}
157161
return writer;
158162
},
159163
decode: function (input, length) {
@@ -217,6 +221,12 @@ export var Invite = {
217221
}
218222
message.sendStats = reader.bool();
219223
continue;
224+
case 10:
225+
if (tag !== 80) {
226+
break;
227+
}
228+
message.invitorWroteDeviceInfo = reader.bool();
229+
continue;
220230
}
221231
if ((tag & 7) === 4 || tag === 0) {
222232
break;
@@ -229,7 +239,7 @@ export var Invite = {
229239
return Invite.fromPartial(base !== null && base !== void 0 ? base : {});
230240
},
231241
fromPartial: function (object) {
232-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
242+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
233243
var message = createBaseInvite();
234244
message.inviteId = (_a = object.inviteId) !== null && _a !== void 0 ? _a : Buffer.alloc(0);
235245
message.projectInviteId = (_b = object.projectInviteId) !== null && _b !== void 0 ? _b : Buffer.alloc(0);
@@ -240,6 +250,7 @@ export var Invite = {
240250
message.projectColor = (_g = object.projectColor) !== null && _g !== void 0 ? _g : undefined;
241251
message.projectDescription = (_h = object.projectDescription) !== null && _h !== void 0 ? _h : undefined;
242252
message.sendStats = (_j = object.sendStats) !== null && _j !== void 0 ? _j : false;
253+
message.invitorWroteDeviceInfo = (_k = object.invitorWroteDeviceInfo) !== null && _k !== void 0 ? _k : false;
243254
return message;
244255
},
245256
};

src/generated/rpc.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface Invite {
1212
projectColor?: string | undefined;
1313
projectDescription?: string | undefined;
1414
sendStats: boolean;
15+
invitorWroteDeviceInfo: boolean;
1516
}
1617

1718
export interface InviteCancel {
@@ -187,6 +188,7 @@ function createBaseInvite(): Invite {
187188
projectName: "",
188189
invitorName: "",
189190
sendStats: false,
191+
invitorWroteDeviceInfo: false,
190192
};
191193
}
192194

@@ -219,6 +221,9 @@ export const Invite = {
219221
if (message.sendStats === true) {
220222
writer.uint32(72).bool(message.sendStats);
221223
}
224+
if (message.invitorWroteDeviceInfo === true) {
225+
writer.uint32(80).bool(message.invitorWroteDeviceInfo);
226+
}
222227
return writer;
223228
},
224229

@@ -292,6 +297,13 @@ export const Invite = {
292297

293298
message.sendStats = reader.bool();
294299
continue;
300+
case 10:
301+
if (tag !== 80) {
302+
break;
303+
}
304+
305+
message.invitorWroteDeviceInfo = reader.bool();
306+
continue;
295307
}
296308
if ((tag & 7) === 4 || tag === 0) {
297309
break;
@@ -315,6 +327,7 @@ export const Invite = {
315327
message.projectColor = object.projectColor ?? undefined;
316328
message.projectDescription = object.projectDescription ?? undefined;
317329
message.sendStats = object.sendStats ?? false;
330+
message.invitorWroteDeviceInfo = object.invitorWroteDeviceInfo ?? false;
318331
return message;
319332
},
320333
};

src/invite/invite-api.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export class InviteApi extends TypedEmitter {
129129
projectColor,
130130
projectDescription,
131131
sendStats,
132+
invitorWroteDeviceInfo,
132133
} = inviteRpcMessage
133134
const invite = { ...inviteRpcMessage, receivedAt: Date.now() }
134135

@@ -169,6 +170,7 @@ export class InviteApi extends TypedEmitter {
169170
projectColor,
170171
projectDescription,
171172
sendStats,
173+
invitorWroteDeviceInfo,
172174
})
173175
}),
174176
},

src/mapeo-manager.js

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import { migrate } from './lib/drizzle-helpers.js'
7070
/** @import { ProjectSettings, ProjectSettingsValue } from '@comapeo/schema' */
7171

7272
/** @typedef {SetNonNullable<ProjectKeys, 'encryptionKeys'>} ValidatedProjectKeys */
73-
/** @typedef {Pick<ProjectJoinDetails, 'projectKey' | 'encryptionKeys'> & { projectName: string, projectColor?: string, projectDescription?: string, sendStats?: boolean }} ProjectToAddDetails */
73+
/** @typedef {Pick<ProjectJoinDetails, 'projectKey' | 'encryptionKeys'> & { projectName: string, projectColor?: string, projectDescription?: string, sendStats?: boolean, invitorWroteDeviceInfo? : boolean }} ProjectToAddDetails */
7474
/** @typedef {Pick<ProjectSettings, 'createdAt' | 'updatedAt' | 'name' | 'projectColor' | 'projectDescription' | 'sendStats'>} ListedProjectSettings */
7575
/** @typedef {ListedProjectSettings & { status: 'joined', projectId: string } | ProjectInfo & { status: 'joining' | 'left', projectId: string }} ListedProject */
7676

@@ -703,6 +703,7 @@ export class MapeoManager extends TypedEmitter {
703703
projectColor,
704704
projectDescription,
705705
sendStats = false,
706+
invitorWroteDeviceInfo = false,
706707
},
707708
{ waitForSync = true } = {}
708709
) => {
@@ -785,18 +786,21 @@ export class MapeoManager extends TypedEmitter {
785786
this.#activeProjects.delete(projectPublicId)
786787
})
787788

788-
try {
789-
const deviceInfo = this.getDeviceInfo()
790-
if (hasSavedDeviceInfo(deviceInfo)) {
791-
await project[kSetOwnDeviceInfo](deviceInfo)
789+
// Only write info on invite if configured
790+
if (!invitorWroteDeviceInfo) {
791+
try {
792+
const deviceInfo = this.getDeviceInfo()
793+
if (hasSavedDeviceInfo(deviceInfo)) {
794+
await project[kSetOwnDeviceInfo](deviceInfo)
795+
}
796+
} catch (e) {
797+
// Can ignore an error trying to write device info
798+
this.#l.log(
799+
'ERROR: failed to write project %h deviceInfo %o',
800+
projectKey,
801+
e
802+
)
792803
}
793-
} catch (e) {
794-
// Can ignore an error trying to write device info
795-
this.#l.log(
796-
'ERROR: failed to write project %h deviceInfo %o',
797-
projectKey,
798-
e
799-
)
800804
}
801805

802806
// 5. Wait for initial project sync

src/mapeo-project.js

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Database from 'better-sqlite3'
33
import { decodeBlockPrefix, decode, parseVersionId } from '@comapeo/schema'
44
import { drizzle } from 'drizzle-orm/better-sqlite3'
55
import { sql, count, eq } from 'drizzle-orm'
6-
import { discoveryKey } from 'hypercore-crypto'
76
import { TypedEmitter } from 'tiny-typed-emitter'
87
import ZipArchive from 'zip-stream-promise'
98
import * as b4a from 'b4a'
@@ -14,7 +13,11 @@ import { Readable, pipelinePromise } from 'streamx'
1413
import { NAMESPACES, NAMESPACE_SCHEMAS } from './constants.js'
1514
import { CoreManager } from './core-manager/index.js'
1615
import { DataStore } from './datastore/index.js'
17-
import { DataType, kCreateWithDocId } from './datatype/index.js'
16+
import {
17+
DataType,
18+
kCreateOrUpdateWithDocId,
19+
kCreateWithDocId,
20+
} from './datatype/index.js'
1821
import { BlobStore } from './blob-store/index.js'
1922
import { BlobApi } from './blob-api.js'
2023
import { IndexWriter } from './index-writer/index.js'
@@ -66,7 +69,6 @@ import {
6669
CategoryFileNotFoundError,
6770
ensureKnownError,
6871
getErrorCode,
69-
InvalidDeviceInfoError,
7072
NotFoundError,
7173
ExhaustivenessError,
7274
nullIfNotFound,
@@ -303,8 +305,6 @@ export class MapeoProject extends TypedEmitter {
303305
switch (doc.schemaName) {
304306
case 'coreOwnership':
305307
return mapAndValidateCoreOwnership(doc, version)
306-
case 'deviceInfo':
307-
return mapAndValidateDeviceInfo(doc, version)
308308
default:
309309
return doc
310310
}
@@ -892,29 +892,26 @@ export class MapeoProject extends TypedEmitter {
892892

893893
/**
894894
* @param {Pick<import('@comapeo/schema').DeviceInfoValue, 'name' | 'deviceType' | 'selfHostedServerDetails'>} value
895-
* @returns {Promise<import('@comapeo/schema').DeviceInfo>}
896895
*/
897896
async [kSetOwnDeviceInfo](value) {
898897
const { deviceInfo } = this.#dataTypes
899898

900-
const configCoreId = this.#coreManager
901-
.getWriterCore('config')
902-
.key.toString('hex')
903-
904899
const doc = {
905900
name: value.name,
906901
deviceType: value.deviceType,
907902
selfHostedServerDetails: value.selfHostedServerDetails,
908903
schemaName: /** @type {const} */ ('deviceInfo'),
909904
}
910905

911-
const existingDoc = await deviceInfo
912-
.getByDocId(configCoreId)
913-
.catch(nullIfNotFound)
914-
if (existingDoc) {
915-
return await deviceInfo.update(existingDoc.versionId, doc)
916-
} else {
917-
return await deviceInfo[kCreateWithDocId](configCoreId, doc)
906+
// TODO: Remove configCore once we know everyone has deviceId
907+
const configCoreId = this.#coreManager
908+
.getWriterCore('config')
909+
.key.toString('hex')
910+
911+
const docIds = [this.deviceId, configCoreId]
912+
913+
for (const docId of docIds) {
914+
await deviceInfo[kCreateOrUpdateWithDocId](docId, doc)
918915
}
919916
}
920917

@@ -1553,22 +1550,6 @@ function getCoreKeypairs({ projectKey, projectSecretKey, keyManager }) {
15531550
}
15541551

15551552
/**
1556-
* Validate that a deviceInfo record is written by the device that is it about,
1557-
* e.g. version.coreKey should equal docId
1558-
*
1559-
* @param {import('@comapeo/schema').DeviceInfo} doc
1560-
* @param {import('@comapeo/schema').VersionIdObject} version
1561-
* @returns {import('@comapeo/schema').DeviceInfo}
1562-
*/
1563-
function mapAndValidateDeviceInfo(doc, { coreDiscoveryKey }) {
1564-
if (!coreDiscoveryKey.equals(discoveryKey(Buffer.from(doc.docId, 'hex')))) {
1565-
throw new InvalidDeviceInfoError()
1566-
}
1567-
return doc
1568-
}
1569-
1570-
/**
1571-
*
15721553
* @param {string} baseUrl
15731554
* @param {string} projectPublicId
15741555
* @returns {string}

0 commit comments

Comments
 (0)