@comapeo/core 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,13 @@
15
15
  "when": 1729783892753,
16
16
  "tag": "0001_medical_wendell_rand",
17
17
  "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "5",
22
+ "when": 1749657174497,
23
+ "tag": "0002_cooing_princess_powerful",
24
+ "breakpoints": true
18
25
  }
19
26
  ]
20
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comapeo/core",
3
- "version": "3.2.0",
3
+ "version": "4.0.0",
4
4
  "description": "Offline p2p mapping library",
5
5
  "main": "src/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -112,12 +112,13 @@
112
112
  "@comapeo/core2.0.1": "npm:@comapeo/core@2.0.1",
113
113
  "@comapeo/ipc": "^2.1.0",
114
114
  "@mapeo/default-config": "5.0.0",
115
- "@mapeo/mock-data": "^3.0.0",
115
+ "@mapeo/mock-data": "^5.0.0",
116
116
  "@sinonjs/fake-timers": "^10.0.2",
117
117
  "@types/b4a": "^1.6.0",
118
118
  "@types/bogon": "^1.0.2",
119
119
  "@types/compact-encoding": "^2.15.0",
120
120
  "@types/debug": "^4.1.8",
121
+ "@types/geojson": "^7946.0.16",
121
122
  "@types/json-schema": "^7.0.11",
122
123
  "@types/json-stable-stringify": "^1.0.36",
123
124
  "@types/nanobench": "^3.0.0",
@@ -136,6 +137,7 @@
136
137
  "drizzle-kit": "^0.20.14",
137
138
  "eslint": "^8.57.0",
138
139
  "execa": "^9.5.1",
140
+ "filter-obj": "^6.1.0",
139
141
  "husky": "^8.0.0",
140
142
  "iterpal": "^0.4.0",
141
143
  "lint-staged": "^14.0.1",
@@ -160,7 +162,7 @@
160
162
  },
161
163
  "dependencies": {
162
164
  "@comapeo/fallback-smp": "^1.0.0",
163
- "@comapeo/schema": "1.6.0",
165
+ "@comapeo/schema": "2.0.0",
164
166
  "@digidem/types": "^2.3.0",
165
167
  "@fastify/error": "^3.4.1",
166
168
  "@fastify/type-provider-typebox": "^4.1.0",
@@ -189,7 +191,7 @@
189
191
  "json-stable-stringify": "^1.1.1",
190
192
  "magic-bytes.js": "^1.10.0",
191
193
  "map-obj": "^5.0.2",
192
- "mime": "^4.0.3",
194
+ "mime": "^4.0.7",
193
195
  "multi-core-indexer": "^1.0.0",
194
196
  "p-defer": "^4.0.0",
195
197
  "p-event": "^6.0.1",
package/src/blob-api.js CHANGED
@@ -4,35 +4,9 @@ import { Transform, pipelinePromise as pipeline } from 'streamx'
4
4
  import { createHash, randomBytes } from 'node:crypto'
5
5
  /** @import { BlobId, BlobType } from './types.js' */
6
6
 
7
- /**
8
- * Location coordinate data. Based on [Expo's `LocationObjectCoords`][0].
9
- * [0]: https://docs.expo.dev/versions/latest/sdk/location/#locationobjectcoords
10
- *
11
- * @typedef {object} LocationObjectCoords
12
- * @prop {number | null} accuracy
13
- * @prop {number | null} altitude
14
- * @prop {number | null} altitudeAccuracy
15
- * @prop {number | null} heading
16
- * @prop {number} latitude
17
- * @prop {number} longitude
18
- * @prop {number | null} speed
19
- */
20
-
21
- /**
22
- * Location metadata for a blob. Based on [Expo's `LocationObject`][0].
23
- * [0]: https://docs.expo.dev/versions/latest/sdk/location/#locationobject
24
- *
25
- * @typedef {object} LocationObject
26
- * @prop {LocationObjectCoords} coords
27
- * @prop {boolean} [mocked]
28
- * @prop {number} timestamp
29
- */
30
-
31
7
  /**
32
8
  * @typedef {object} Metadata
33
9
  * @prop {string} mimeType
34
- * @prop {number} timestamp
35
- * @prop {LocationObject} [location]
36
10
  */
37
11
 
38
12
  export class BlobApi {
@@ -6,6 +6,7 @@ import { discoveryKey } from 'hypercore-crypto'
6
6
  import { TypedEmitter } from 'tiny-typed-emitter'
7
7
  import ZipArchive from 'zip-stream-promise'
8
8
  import * as b4a from 'b4a'
9
+ import mime from 'mime/lite'
9
10
  // @ts-expect-error
10
11
  import { Readable, pipelinePromise } from 'streamx'
11
12
 
@@ -69,6 +70,10 @@ import { createWriteStream } from 'fs'
69
70
  /** @typedef {Omit<ProjectSettingsValue, 'schemaName'>} EditableProjectSettings */
70
71
  /** @typedef {ProjectSettingsValue['configMetadata']} ConfigMetadata */
71
72
  /** @typedef {Map<string,Attachment>} SeenAttachments*/
73
+ /** @typedef {object} BlobRef
74
+ * @prop {string|undefined} mimeType
75
+ * @prop {BlobId} blobId
76
+ */
72
77
 
73
78
  const CORESTORE_STORAGE_FOLDER_NAME = 'corestore'
74
79
  const INDEXER_STORAGE_FOLDER_NAME = 'indexer'
@@ -83,8 +88,6 @@ export const kClearDataIfLeft = Symbol('clear data if left project')
83
88
  export const kSetIsArchiveDevice = Symbol('set isArchiveDevice')
84
89
  export const kIsArchiveDevice = Symbol('isArchiveDevice (temp - test only)')
85
90
  export const kGeoJSONFileName = Symbol('geoJSONFileName')
86
- export const kExportGeoJSONStream = Symbol('exportGeoJSONStream')
87
- export const kExportZipStream = Symbol('exportZipStream')
88
91
 
89
92
  const EMPTY_PROJECT_SETTINGS = Object.freeze({})
90
93
 
@@ -786,40 +789,43 @@ export class MapeoProject extends TypedEmitter {
786
789
  }
787
790
  }
788
791
 
789
- let latitude = lat
790
- let longitude = lon
791
- let altitude = null
792
- const position = observation?.metadata?.position?.coords
793
- if (position) {
794
- latitude = position.latitude
795
- longitude = position.longitude
796
- if (position.altitude !== undefined) {
797
- altitude = position.altitude
792
+ const metadataCoords = observation.metadata?.position?.coords
793
+ const altitude = metadataCoords?.altitude
794
+
795
+ /** @type {[number, number] | [number, number, number] | null} */
796
+ let coordinates = null
797
+
798
+ // Prioritize using the observation's `lat` and `lon` fields
799
+ if (typeof lat === 'number' && typeof lon === 'number') {
800
+ coordinates =
801
+ typeof altitude === 'number' ? [lon, lat, altitude] : [lon, lat]
802
+ } else {
803
+ // Fall back to using the observation metadata's position if possible
804
+ if (
805
+ typeof metadataCoords?.latitude === 'number' &&
806
+ typeof metadataCoords?.longitude === 'number'
807
+ ) {
808
+ coordinates =
809
+ typeof altitude === 'number'
810
+ ? [metadataCoords.longitude, metadataCoords.latitude, altitude]
811
+ : [metadataCoords.longitude, metadataCoords.latitude]
798
812
  }
799
813
  }
800
814
 
801
- const coordinates = [longitude, latitude]
802
- if (typeof altitude === 'number') {
803
- coordinates.push(altitude)
815
+ /** @type {import('geojson').Feature<import('geojson').Point | null>} */
816
+ const feature = {
817
+ type: 'Feature',
818
+ properties: observation,
819
+ geometry: coordinates
820
+ ? {
821
+ type: 'Point',
822
+ coordinates,
823
+ }
824
+ : null,
804
825
  }
805
- const hasLatLon =
806
- typeof longitude === 'number' && typeof latitude === 'number'
807
- const geometry = hasLatLon
808
- ? {
809
- type: 'Point',
810
- coordinates,
811
- }
812
- : null
813
826
  const comma = first ? '' : ','
814
827
  first = false
815
- yield b4a.from(
816
- `${comma}\n ` +
817
- JSON.stringify({
818
- type: 'Feature',
819
- properties: observation,
820
- geometry,
821
- })
822
- )
828
+ yield b4a.from(`${comma}\n ` + JSON.stringify(feature))
823
829
  }
824
830
  }
825
831
 
@@ -941,7 +947,7 @@ export class MapeoProject extends TypedEmitter {
941
947
  * @param {string} [options.lang]
942
948
  * @returns {Readable<Buffer | Uint8Array>}
943
949
  */
944
- [kExportGeoJSONStream]({
950
+ #exportGeoJSONStream({
945
951
  observations = true,
946
952
  tracks = true,
947
953
  lang,
@@ -1027,7 +1033,7 @@ export class MapeoProject extends TypedEmitter {
1027
1033
  ) {
1028
1034
  const fileName = await this[kGeoJSONFileName](observations, tracks)
1029
1035
  const filePath = path.join(exportFolder, fileName)
1030
- const source = this[kExportGeoJSONStream]({ observations, tracks, lang })
1036
+ const source = this.#exportGeoJSONStream({ observations, tracks, lang })
1031
1037
  const sink = createWriteStream(filePath)
1032
1038
  await pipelinePromise(source, sink)
1033
1039
 
@@ -1036,15 +1042,37 @@ export class MapeoProject extends TypedEmitter {
1036
1042
 
1037
1043
  /**
1038
1044
  * @param {Attachment} attachment
1039
- * @returns {Promise<null | BlobId>}
1045
+ * @returns {Promise<null | BlobRef>}
1040
1046
  */
1041
- async #tryGetBlobId(attachment) {
1047
+ async #tryGetAttachmentBlob(attachment) {
1042
1048
  // Audio must not have variants
1043
1049
  for (const variant of VARIANT_EXPORT_ORDER) {
1044
- const blobId = buildBlobId(attachment, variant)
1045
- const entry = await this.#blobStore.entry(blobId)
1046
- if (!entry) continue
1047
- return blobId
1050
+ try {
1051
+ const blobId = buildBlobId(attachment, variant)
1052
+ const entry = await this.#blobStore.entry(blobId)
1053
+ if (!entry) continue
1054
+ const metadata = entry.value.metadata
1055
+ if (!metadata || typeof metadata !== 'object') continue
1056
+ let mimeType = undefined
1057
+ if ('mimeType' in metadata) {
1058
+ if (typeof metadata.mimeType === 'string') {
1059
+ mimeType = metadata.mimeType
1060
+ } else {
1061
+ this.#l.log('Invalid type for mimeType in blob', blobId, entry)
1062
+ continue
1063
+ }
1064
+ }
1065
+ return { blobId, mimeType }
1066
+ } catch (e) {
1067
+ if (!(e instanceof Error)) throw e
1068
+ this.#l.log(
1069
+ 'Error loading blob id for attachment',
1070
+ attachment,
1071
+ variant,
1072
+ e.message
1073
+ )
1074
+ continue
1075
+ }
1048
1076
  }
1049
1077
 
1050
1078
  return null
@@ -1066,7 +1094,7 @@ export class MapeoProject extends TypedEmitter {
1066
1094
  // GeoJSON
1067
1095
  const geoJSONFileName = await this[kGeoJSONFileName](observations, tracks)
1068
1096
  const seenAttachments = new Map()
1069
- const geoJSONStream = this[kExportGeoJSONStream]({
1097
+ const geoJSONStream = this.#exportGeoJSONStream({
1070
1098
  observations,
1071
1099
  tracks,
1072
1100
  lang,
@@ -1079,16 +1107,31 @@ export class MapeoProject extends TypedEmitter {
1079
1107
  const missingAttachments = []
1080
1108
  // Attachments
1081
1109
  if (attachments) {
1082
- const mediaFolder = this.#exportPrefix('Media') + '/'
1110
+ const mediaFolder = (await this.#exportPrefix('Media')) + '/'
1083
1111
  for (const attachment of seenAttachments.values()) {
1084
- const blobId = await this.#tryGetBlobId(attachment)
1085
- if (blobId === null) {
1112
+ const ref = await this.#tryGetAttachmentBlob(attachment)
1113
+ if (ref === null) {
1086
1114
  missingAttachments.push(attachment)
1087
1115
  continue
1088
1116
  }
1089
1117
 
1118
+ const { blobId, mimeType } = ref
1119
+ let extensionString = ''
1120
+ if (mimeType) {
1121
+ const extension = mime.getExtension(mimeType)
1122
+ if (extension) {
1123
+ extensionString = '.' + extension
1124
+ } else {
1125
+ this.#l.log('Got unknown mime type in attachment blob', attachment)
1126
+ }
1127
+ }
1128
+
1090
1129
  const stream = this.#blobStore.createReadStream(blobId)
1091
- const name = mediaFolder + blobId.variant + '/' + attachment.name
1130
+ const name = path.posix.join(
1131
+ mediaFolder,
1132
+ blobId.variant,
1133
+ `${attachment.name}${extensionString}`
1134
+ )
1092
1135
 
1093
1136
  // @ts-expect-error
1094
1137
  await archive.entry(stream, { name })
@@ -1118,7 +1161,7 @@ export class MapeoProject extends TypedEmitter {
1118
1161
  * @param {string} [options.lang]
1119
1162
  * @returns {Readable<Buffer | Uint8Array>}
1120
1163
  */
1121
- [kExportZipStream]({
1164
+ #exportZipStream({
1122
1165
  observations = true,
1123
1166
  tracks = true,
1124
1167
  attachments = true,
@@ -1153,7 +1196,7 @@ export class MapeoProject extends TypedEmitter {
1153
1196
  ) {
1154
1197
  const fileName = await this.#zipFileName(observations, tracks)
1155
1198
  const filePath = path.join(exportFolder, fileName)
1156
- const source = this[kExportZipStream]({
1199
+ const source = this.#exportZipStream({
1157
1200
  observations,
1158
1201
  tracks,
1159
1202
  attachments,
package/src/types.ts CHANGED
@@ -162,7 +162,7 @@ export type BlobStoreEntriesStream = Readable & {
162
162
  >
163
163
  }
164
164
 
165
- export type Attachment = Observation['attachments'][0]
165
+ export type Attachment = Observation['attachments'][number]
166
166
 
167
167
  export type StringToTaggedUnion<T extends string> = {
168
168
  [K in T]: {