IronFlock SDK
IronFlock SDK を使用すると、エッジアプリケーションから IronFlock プラットフォームと連携できます。SDK は Python、JavaScript、Go の 3 つの言語で提供されており、機能は同じです。以下では並べて説明します。登録済みデバイスで実行する場合、認証を自動的に処理し、データの公開、履歴のクエリ、デバイス間のリモートプロシージャ呼び出し、デバイスメタデータの更新のための関数を提供します。
| SDK | パッケージ | 必要環境 |
|---|---|---|
| Python | ironflock(PyPI) | Python 3.8+ |
| JavaScript | ironflock(npm) | Node.js 18+ またはモダンブラウザ |
| Go | github.com/RecordEvolution/ironflock-go | Go 1.27.2+ |
インストール
Python
pip install ironflockまたは、アプリの requirements.txt に ironflock を追加してください。
クイックスタート
Python
import asyncio
from datetime import datetime, timezone
from ironflock import IronFlock
async def main():
while True:
await ironflock.publish_to_table("sensordata", {
"tsp": datetime.now(timezone.utc).isoformat(),
"temperature": 22.5,
"humidity": 60
})
await asyncio.sleep(5)
ironflock = IronFlock(mainFunc=main)
ironflock.run()IronFlock アプリコンテナ内で使用する場合、SDK は環境から接続資格情報を自動的に読み取ります — 手動設定は不要です。テーブルに書き込むすべての行は、そのタイムスタンプである tsp を持ちます(publishToTable を参照)。
コンストラクタオプション
Python
ironflock = IronFlock(
mainFunc=main, # async function to run after connecting
serial_number="abc123", # override device serial (optional)
reconnect_window=60, # seconds a table operation rides out a platform restart (optional)
)| パラメータ | 説明 |
|---|---|
mainFunc | 接続が確立された後に実行される非同期関数 |
serial_number | 公開やテーブル書き込みとともに送信されるデバイスのシリアル番号を上書きします。デフォルトでは環境変数 DEVICE_SERIAL_NUMBER が使用されます。デバイスエージェントがアプリごとの資格情報を注入している間は、接続が認証に使う ID は変わりません |
reconnect_window | テーブル操作が、再起動後にプラットフォームが復帰するのを待ってから例外を送出するまでの秒数。デフォルトは 60。0 で無効になります(接続の信頼性 を参照) |
データの公開
publishToTable / publish_to_table
フリートテーブルにデータレコードを公開します。テーブル名はアプリの data-template.yml で定義されたテーブルと一致する必要があります。SDK はデータを適切なプロジェクトデータベースに自動的にルーティングします。
Python
from datetime import datetime, timezone
await ironflock.publish_to_table("sensordata", {
"tsp": datetime.now(timezone.utc).isoformat(),
"temperature": 22.5,
"humidity": 60,
"device_id": "sensor-001"
})データバックエンドは最初の引数から行を読み取ります:Python では dict、JavaScript では配列の最初の要素、Go では最初の位置引数(マップまたは構造体)です。追加のキーワード引数(Go では引数の中にある ironflock.Kwargs の値)は、データテンプレートがそれらをマッピングしているカラム(kwargs.<name>)についてのみ読み取られます。
すべての行に tsp が必要です。 すべてのテーブルには必須の tsp カラム、つまり行のタイムスタンプ(例:ISO 8601 文字列、Go では UTC の RFC 3339 として送信される time.Time)があり、データテンプレートが別の場所にマッピングしていない限り、データバックエンドはそれを行から読み取ります。tsp のない公開された行は破棄され、その痕跡はアプリの error-logs テーブルのエントリしか残りません — 確認応答を受けた公開は成功したように見えます。ストレージ不足のために拒否された行は、そのエントリすら残さずに破棄されます。行が保存されたかどうかを知る必要がある場合は、appendToTable / append_to_table を使用してください。
エンティティテーブルへの部分的な行。 エンティティキー(maintainLatestFlagFor)を持つテーブルでは、行にエンティティキー、tsp、変更するカラムだけを含めれば済みます。行が省略したカラムはエンティティの直前の値を保持し、None / null / nilとして送信したカラムはクリアされます。フィールドが省略扱いになるかどうかは、行の組み立て方によって決まります:
- Python: 値が
Noneのキーはnullとして送信され、カラムをクリアします。古い値を保持するには、そのキーをdictから削除してください。 - JavaScript: 値が
undefinedのプロパティはペイロードから除外されるため、古い値が保持されます。カラムをクリアするにはnullを設定してください。 - Go:
omitemptyまたはomitzeroタグ付きの構造体フィールドは、空のとき省略されるため、古い値が保持されます。それ以外の場合、nilポインタのフィールドは、ironflock.Row内のnil値と同様にnullとして送信され、カラムをクリアします。
これは4つの書き込みメソッドすべてに当てはまります。エンティティキーのないテーブルでは、行が省略したカラムはNULLとして保存されます。
appendToTable / append_to_table
Pub/Sub の代わりにリモートプロシージャコールを使用してフリートテーブルにデータを追加します。データが永続化されたことの確認が必要な場合に使用します:拒否された行はエラーになります。
Python
result = await ironflock.append_to_table("sensordata", {
"tsp": datetime.now(timezone.utc).isoformat(),
"temperature": 22.5,
"humidity": 60
})publishToTable と同様に、行には tsp が必要です。データバックエンドは tsp のない行を、理由を示さずに wamp.error.runtime_error で拒否します。その場合、SDK のエラーには “the data backend refused the row without a reason — a missing tsp is the usual cause; see the error-logs table” という一文が追加されます。アプライアンスのディスクがいっぱいの場合など、型付きの拒否は データバックエンドによる拒否 に一覧されています。
publishRowsToTable / publish_rows_to_table
複数の行を 1 つのメッセージで(一括挿入)フリートテーブルに公開します。プラットフォームはバッチ全体を 1 回の操作でアトミックに(オール・オア・ナッシングで)挿入します。1 行ごとにラウンドトリップを行うとコストが高すぎる高頻度データに使用してください。publishToTable と同様に、これはファイア・アンド・フォーゲットです — 確認応答はルーターへの配信を保証するものであり、データベースへの挿入を保証するものではありません。
Python
await ironflock.publish_rows_to_table("sensordata", [
{"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5},
{"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7},
])第 2 引数は、挿入する行オブジェクトの空でないリストで、各行がそれぞれ独自の tsp を持ちます(Go では行のスライス:[]ironflock.Row、[]map[string]any、または構造体のスライス)。データバックエンドが拒否したバッチ(たとえば行に tsp がない場合)は、publishToTable と同様に、公開側には何も知らされずに破棄されます。
appendRowsToTable / append_rows_to_table
複数の行を 1 つのリモートプロシージャ呼び出しで(一括挿入)フリートテーブルに追加します。プラットフォームはバッチ全体をアトミックに(オール・オア・ナッシングで)挿入します。いずれかの行が無効な場合、バッチ全体が拒否され、何も永続化されません。挿入結果が必要な場合は、publishRowsToTable / publish_rows_to_table よりもこちらを使用してください。すべての行にそれぞれの tsp が必要です。拒否の扱いは appendToTable と同じで、理由なしに拒否されたバッチに対するヒントも含まれます。
Python
result = await ironflock.append_rows_to_table("sensordata", [
{"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5},
{"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7},
])
# result -> {"success": True, "count": 2}reportError / report_error
アプリケーションエラーをフリートの error-logs テーブルに記録します。これは publishToTable / appendToTable の便利なラッパーです。行に source: "app"、重大度 level、およびタイムスタンプを付与し、通常のテーブル行と同じように書き込みます。エラーは、fleetdb システムエラーが使用するのと同じ error-logs テーブル(source: "system" でタグ付けされる)に格納されるため、getHistory でクエリでき、subscribeToTable / subscribe_to_table でストリーミングでき、ボードテンプレートで使用でき、transformed.error-logs でリアルタイムに配信されます。プラットフォームのシステムエラートーストを発火させることはありません。
Python
# Fire-and-forget (default): publishes to the error-logs table
await ironflock.report_error("Sensor read timed out", level="warn")
# Pass an exception to capture its traceback (falls back to the message)
try:
risky_operation()
except Exception as err:
await ironflock.report_error(err)
# Use the append RPC when you want to await the insert outcome, and show the
# operator a friendlier line than the raw error
await ironflock.report_error(
"Calibration failed: offset 0.42 out of range",
append=True,
user_message="Please recalibrate the sensor",
)行は { tsp, msg, user_message, source: "app", level } です:msg には技術的なテキスト(トレースバックまたはスタック)が、user_message にはボードのエラートーストに表示される行が入ります — 渡したユーザーメッセージ、指定しなかった場合はメッセージそのものです。
パラメータ:
| パラメータ | 型 | 説明 |
|---|---|---|
error | str / string または例外 / Error | エラーメッセージ、またはトレースバック/スタック(もしくはメッセージ)が記録される例外 |
level | str / string、オプション | 重大度:"error"、"warn"、"info" または "debug"。デフォルトは "error" |
append | bool / boolean、オプション | true の場合、append RPC を使用します(挿入結果を返します)。デフォルトは false(fire-and-forget の公開) |
tsp | str / string、オプション | ISO-8601 タイムスタンプの上書き。デフォルトは現在時刻 |
user_message / userMessage | str / string、オプション | オペレーター向けのテキストで、user_message として保存されます。デフォルトは記録されるメッセージ |
Python ではオプションはキーワード引数です(
report_error(error, level=..., append=..., tsp=..., user_message=...))。JavaScript ではオプションオブジェクトを介して渡します(reportError(error, { level, append, tsp, userMessage }))。Go では構造体を介して渡します(ReportError(ctx, errOrMsg, ironflock.ReportErrorOptions{Level, Append, Tsp, UserMessage}))。ここでerrOrMsgはerrorまたは文字列、Levelはironflock.LevelError、LevelWarn、LevelInfo、LevelDebugのいずれかで、返される*ResultはAppendが設定されていない限りnilです。
publish
独自の WAMP トピックにメッセージを公開します。データベーステーブルにマッピングされないカスタムメッセージングやイベントに使用します。ルーターは一部の名前をプラットフォーム用に予約しています — アプリが使用できる URI を参照してください。
Python
await ironflock.publish("com.myapp.alerts", {
"level": "warning",
"message": "Temperature threshold exceeded"
})履歴データのクエリ
getHistory
フリートテーブルまたは transform から履歴データを取得します:条件に一致する最新の行を、tsp の昇順で返します。フィルタリング、時間範囲、ページネーションをサポートしています。
Python
# Simple query
data = await ironflock.getHistory("sensordata", {"limit": 100})
# Query with time range and filters
data = await ironflock.getHistory("sensordata", {
"limit": 500,
"offset": 0,
"timeRange": ["2026-01-01T00:00:00Z", "2026-03-01T00:00:00Z"],
"filterAnd": [
{"column": "temperature", "operator": ">", "value": 20},
{"column": "humidity", "operator": "<=", "value": 80}
]
})
# Current value(s) only: the "latest" marker returns the newest row per entity
current = await ironflock.getHistory("sensordata", {
"limit": 100,
"filterAnd": [{"latest": True}]
})
# One field of a large json column: the column comes back pruned to its paths
readings = await ironflock.getHistory("sensordata", {
"limit": 100,
"columns": ["json_data"],
"columnPaths": ["json_data.sensor.temp"],
})クエリパラメータ:
| フィールド | 型 | 説明 |
|---|---|---|
limit | int / number | 返す最大行数(1〜10,000、必須。transform の場合は 1〜3,000) |
offset | int / number | ページネーション用のオフセット |
timeRange | list / array | [start, end] — ISO 日時文字列またはエポックミリ秒の数値(混在は不可)。開いた端には None / null。レガシーな {"start": ..., "end": ...} オブジェクトはこのペアに変換されます |
filterAnd | list / array | AND で結合されるフィルタエントリ:条件、グループ、および/または latest マーカー(下記参照) |
columns | list / array | 返すカラム(オプション)。tsp、device_key、authid は常に含まれます。すべてのカラムが必要な場合は省略してください |
columnPaths | list / array | json カラムを絞り込みます(オプション):フィルタと同じ記法の JSON パス("json_data.a.b"、"json_data['key']")。columns に列挙された json カラムは、そのパスの和集合だけを保持した状態で返されます。ここにパスがないカラムは全体がそのまま返されます。transform、および fleetdb v1.0.52 より古いデータバックエンドでは無視されます |
Go では、これらは ironflock.TableQueryParams のフィールド Limit、Offset、TimeRange、FilterAnd、Columns、ColumnPaths です。TimeRange は *ironflock.TimeRange{Start, End} で、各境界値は time.Time、ISO 8601 文字列、エポックミリ秒の数値、または開いた端を表す nil です(ironflock.Between、Since、Until は time.Time の値からこれを作成します)。FilterAnd は []ironflock.Filter です。レガシーなオブジェクト形式はありません。
フィルタ演算子: =, !=, <>, >, <, >=, <=, LIKE, ILIKE, NOT LIKE, NOT ILIKE, IN, NOT IN, IS NULL, IS NOT NULL
各フィルタ条件は column、operator、value のキーを持つオブジェクトです — Go では ironflock.Where(column, operator, value)、ironflock.IsNull(column)、ironflock.IsNotNull(column) で作成します。演算子は大文字・小文字を問わず、前後の空白を無視して照合され、上に挙げた形式 — データバックエンドが受け付ける唯一の形式 — で送信されます。そのため "like" は、グループの中でも "LIKE" として送られます。一覧にない演算子は、警告とともにそのまま送信されます。IS NULL と IS NOT NULL は value を取らず、NULL について問い合わせる唯一の方法です:それ以外の演算子はすべて、SQL とまったく同じく、カラムが NULL の行を除外します。IN と NOT IN はリスト(Go ではスライス)を取ります。LIKE は大文字と小文字を区別します。区別せずに照合するには ILIKE を使用してください。
OR による組み合わせ。 filterAnd のエントリは AND で結合されます。グループエントリは、自身のエントリ(条件、latest マーカー、またはさらに別のグループ)を 1 つの演算子 "AND" または "OR" で組み合わせます:
Python
# Rows that are not soft-deleted: deleted IS NULL OR deleted = false
"filterAnd": [
{
"combinator": "OR",
"filters": [
{"column": "deleted", "operator": "IS NULL"},
{"column": "deleted", "operator": "=", "value": False},
],
}
]グループに対応していないデータバックエンド(v1.2.0 より前の fleetdb)は、この形の一部だけを適用するのではなく、この形そのものを拒否します。そのため、フィルタが除外するはずの行を黙って返すことはなく、明示的に失敗します。
現在の値の読み取り。 filterAnd 内の {"latest": true} エントリ(Go では ironflock.Latest())はフィルタ条件ではなくモードの切り替えです。データバックエンドは、テーブルが maintainLatestFlagFor で宣言したエンティティキーをもとに SQL で導出した、エンティティごとの最新の行のみを返します。エンティティキーを持たないテーブルの場合は、最新の 1 行だけが返されます。
その他の条件は、想定どおりにマーカーと組み合わされます。エンティティキーのカラムに対する条件はどのエンティティを返すかを絞り込み、それ以外のすべての条件と timeRange は、得られた最新の行に対して適用されます。したがって {"latest": true} と deleted フィルタを組み合わせると、削除済みのエンティティは非表示になり、その前の行が再び現れることはありません。
以前のバージョンの IronFlock は、物理的な latest_flag カラムを保存していました。このカラムはもう存在しません。レガシーな latest_flag = true フィルタは引き続き受け付けられ、マーカーとして扱われますが、新しいコードでは {"latest": true} を使用してください。latest マーカーは getSeriesHistory では利用できません。
時間範囲。 データバックエンドは文字列の境界値を JavaScript の Date で読み取ります。そのため、ISO 8601 の日付または日時(2026-07-01、2026-07-01T12:30:15.250Z)で記述し、日付と時刻は T で区切ってください。SDK は、0〜99 年について日付と時刻の間に空白を入れた値を拒否します。Date がそれを誤って解釈するためです:"0001-01-01 00:00:00"(Python の str(datetime.min) が返す値)は 2001 年として読み取られてしまいます。タイムゾーンのない時刻は UTC として読み取られます。数値の境界値はエポックミリ秒であり、有限でなければなりません:NaN と ±Infinity はデータバックエンドに開いた境界として届き、時間フィルタを黙って無効にしてしまうため、SDK は送信前にこれらを拒否します。Go では、time.Time の境界値は UTC の RFC 3339 として送信され、文字列の境界値は Date が正しく読み取れる ISO 8601 形式のいずれかでなければなりません — 日付、または日付と時刻(省略可能な Z または UTC オフセット(+02:00 または +0200)付き)です。それ以外の文字列、たとえば基本形式の 20260701T123000Z、時間だけのオフセット、その月に存在しない日(2026-02-30)などは、何も送信される前に ironflock.ErrInvalidArgument で失敗します。
transform の読み取り。 transform(データテンプレートの SQL ビュー。変換テーブル を参照)は、テーブルとは異なる方法で読み取られます。データバックエンドが適用するのは limit(最大 3000。それより大きい値は URI_INVALID_LIMIT で拒否されます)、offset、filterAnd だけです。timeRange、columns、columnPaths、latest マーカーは無視されます。また、transform が宣言していないカラムに対するフィルタは破棄され(transform には暗黙の tsp、device_key、authid はありません)、その分だけ結果が広がります。行はビュー自身の順序を逆にした順で返されます:最新の行が古い順に得られるのは、ビューが新しい順に並べている場合だけです。transform の時間範囲は、その SQL の中で絞り込んでください。
大きな結果。 データバックエンドの 1 メッセージ(JSON で 8 MiB)に収まらない大きな結果はチャンクに分割されて届き、SDK がメモリ上で再構成します:1 回の読み取りの行が、同じ順序で得られます。チャンクのつじつまが合わない場合(欠落や重複がある、または行数がデータバックエンドのサマリーと異なる場合)は、行の一部を返すのではなく読み取りが失敗します。8 MiB を超える単一の行、またはデータバックエンドの 1 GiB の上限を超える結果は URI_RESULT_TOO_LARGE で拒否されます:limit を下げる、timeRange を狭める、columns を減らす、または columnPaths で絞り込んでください。
getSeriesHistory / get_series_history
フリートテーブルから ダウンサンプリングされた時系列データ を取得します。カラムを時間バケットごとに集約します(例:1 時間ごとの平均)。長期間にわたるグラフに最適です。テーブルで利用可能で(transform では利用できません)、fleetdb v1.0.58 以降のデータバックエンドが必要です。
Python
from datetime import datetime, timedelta, timezone
from ironflock import SeriesMetric
now = datetime.now(timezone.utc)
avg = SeriesMetric(ref="temperature", method="AVG") # AVG, SUM, COUNT, MIN, MAX, FIRST, LAST
series = await ironflock.get_series_history("sensordata", {
"metrics": [avg, {"ref": "humidity", "method": "MAX"}],
"bucketMs": 3_600_000, # one-hour buckets; omit to divide the range into `limit` buckets
"limit": 24, # the bucket budget, 1–10000
"timeRange": [(now - timedelta(hours=24)).isoformat(), now.isoformat()], # required, with a start
"groupBy": ["device_key"],
})
for row in series:
print(row["tsp"], row["device_key"], row[avg.column]) # avg.column is "AVG:temperature"クエリパラメータ:
| フィールド | 型 | 説明 |
|---|---|---|
metrics | list / array | バケットごとに計算される集約で、少なくとも 1 つ必要です:{"ref": ..., "method": ...}。ref はカラム、または json カラム内の JSON パス("json_data.temp")です。method は "AVG"、"SUM"、"COUNT"、"MIN"、"MAX"、"FIRST" または "LAST" です。各メトリクスは "<METHOD>:<ref>" という名前で返され、この名前は 63 バイト以内に収まる必要があります |
bucketMs | int / number | バケットの幅(ミリ秒単位の整数、1000 以上、オプション)。省略すると、時間範囲が limit 個のバケットに分割されます |
limit | int / number | バケットの予算(1〜10,000) — 行数の上限ではありません |
timeRange | list / array | [start, end] — ISO 日時文字列またはエポックミリ秒の数値。必須で、開始値が必要です。終了値に None / null を指定すると現在時刻を意味します。終了は開始より後でなければなりません |
groupBy | list / array | 各バケットをグループごとの行に分割するカラム(オプション) |
filterAnd | list / array | getHistory と同様に AND で結合される条件とグループ(オプション)。ここでは latest マーカーは、グループの中であっても拒否されます。現在の値を読み取るには getHistory を使用してください |
行。 各行は 1 つのバケットです(groupBy を指定した場合は、1 つのグループの 1 つのバケット)。tsp はエポックミリ秒で表したバケットの開始時刻で、各 groupBy カラムはそれ自身の名前で、各メトリクスは "<METHOD>:<ref>"(Python では SeriesMetric.column、JavaScript では seriesColumn(metric)、Go では SeriesMetric.Column())という名前で返されます。例:row["AVG:temperature"]。行は tsp の昇順に並び、行を含まないバケットは結果に現れません(ギャップの補完はありません)。AVG と SUM は数値カラムを受け付け、MIN と MAX は文字列やタイムスタンプも受け付けます。FIRST と LAST はバケット内で最も早い行と最も遅い行の値を返し、COUNT は NULL でない値の数を数えます(tsp の場合は行数)。大きな結果は、getHistory と同様にチャンクから再構成されます。
バケットと開いた終端。 limit は行ではなくバケットを数えます。bucketMs を指定しない場合、時間範囲は limit 個のバケットに分割されます。指定した場合、範囲内のバケット数が limit を超えると、データバックエンドはバケットを bucketMs の倍数に広げます — そして開いた終端は、データバックエンド自身の現在時刻、つまりあなたの現在時刻より少し後まで達します。開いた終端を持つ直近 24 時間には 1 時間のバケットが 25 個含まれるため、limit: 24 では 2 時間のバケットになります。例のように終端を指定するか、バケットを 1 つ多く許容してください:bucketMs と開いた終端を使うと、limit + 1 個のバケットが返ることがあります。
SDK は、データバックエンドが拒否するはずの内容を送信前に確認します:少なくとも 1 つのメトリクス、空でない ref、既知のメソッド、63 バイト以内のカラム名、開始値、開始より後の終了値、そして 1000 以上の整数である bucketMs(Go では、ミリ秒単位の整数で 1 秒以上の Bucket)。データバックエンドは、応答できないクエリを URI_INVALID_TIME_RANGE、URI_INVALID_METRIC、URI_INVALID_GROUP_BY、または URI_SERIES_TOO_MANY_GROUPS(50,000 行超)で拒否します — データバックエンドによる拒否 を参照してください。
非推奨の形式。 1.9.0 までの SDK リリースは、
metricsをカラム名として、トップレベルのmethodとともに送信していました({"metrics": ["temperature"], "method": "AVG", ...})。この形式は、fleetdb v1.0.58 以降のデータバックエンドでは拒否されます。それでもこの形式は引き続き動作します:SDK はカラムごとに 1 つの{ref, method}メトリクスを送信し、警告を出し(Python ではDeprecationWarning、JavaScript ではプロセスごとに 1 回のコンソール警告)、結果のカラム名を元のカラム名に戻します。そのため、古いコードは引き続きrow["temperature"]を読み取れます — ただし、行にすでにその名前のカラムがある場合(tspのCOUNTにおけるtspなど)は除きます。{ref, method}メトリクスに移行し、row["AVG:temperature"]を読み取るようにしてください。Go SDK には現在の形式しかありません:メトリクスごとのメソッド(SeriesMetric)です。
データの購読
subscribeToTable / subscribe_to_table
フリートテーブルのリアルタイム更新を購読します。データバックエンドが新しい行を保存するたびに、保存されたとおりの行(値はデータテンプレートのカラムの型に変換され、シークレットカラムはマスクされます)を最初の引数としてハンドラが呼び出されます。一括挿入パス(publishRowsToTable / appendRowsToTable)で書き込まれた行は、1 行ずつハンドラに配信されるため、データの書き込み方法に関係なくハンドラのコードは変わりません。
Python
def on_sensor_data(*args, **kwargs):
row = args[0]
print("New reading:", row["tsp"], row["temperature"])
await ironflock.subscribe_to_table("sensordata", on_sensor_data)transform には独自の行がありません:スケジュールの各ティックで、データバックエンドはビュー全体(最大 3000 行、ビューの順序を逆にしたもの)を、最初の引数が行のリストである 1 つの イベントとして公開します。Python の event_rows(args[0])、JavaScript の eventRows(args)、Go の ev.Rows() は両方の形を読み取ります:テーブルの行からは [row] が、transform のリストからはそのリスト(空のビューなら [])が得られ、それ以外は None / undefined / nil になります。Go では、transform のイベントに対する ev.Row() は nil です。
Python
from ironflock import event_rows
def on_hourly(*args, **kwargs):
for row in event_rows(args[0]) or []:
print(row)
await ironflock.subscribe_to_table("hourly_averages", on_hourly)subscribe
カスタムリアルタイムメッセージング用に独自の WAMP トピックを購読します。テーブルの行には subscribeToTable / subscribe_to_table を使用してください:ルーターは、テーブルへの生の書き込みトピックの購読を拒否します(アプリが使用できる URI を参照)。
Python
def on_alert(*args, **kwargs):
print("Alert received:", args, kwargs)
await ironflock.subscribe("com.myapp.alerts", on_alert)シークレットカラム
アプリの データテンプレート で secret: true と宣言されたカラムは、保存時に暗号化されます。通常の読み取り経路が平文を返すことは一切ありません。そのデータを書き込んだアプリ自身に対してもです:
| 読み取り経路 | シークレットカラムの見え方 |
|---|---|
getHistory、subscribeToTable / subscribe_to_table、その他メッセージルーター経由のすべて | プレースホルダ __secret__(SECRET_PLACEHOLDER としてエクスポートされています。Go では ironflock.SecretPlaceholder) |
| SQL アクセス(FleetDB Access の Postgres ログイン) | 保存された暗号文 ifsec:1:… |
revealSecrets / reveal_secrets | 復号された平文 |
NULL はすべての経路でそのまま NULL として渡されるため、「設定済みだが非表示」と「一度も書き込まれていない」は引き続き区別できます。
エンティティテーブル では、読み取り・変更・書き戻し(read-modify-write)を安全に行えます。__secret__ プレースホルダを保持したままの行を再送信しても — あるいはシークレットカラムを丸ごと省略しても — 保存済みのシークレットは上書きされずに維持され、明示的な null はそれをクリアします。正確なルールについては、データバックエンドガイドの シークレットカラム を参照してください。
Python
from datetime import datetime, timezone
# Written like any other column — the data backend encrypts it on insert
await ironflock.publish_to_table("credentials", {
"tsp": datetime.now(timezone.utc).isoformat(),
"device_id": "d-1",
"api_token": "hunter2",
})
# A normal read redacts it
rows = await ironflock.getHistory("credentials", {"limit": 1})
# [{"tsp": "...", "device_id": "d-1", "api_token": "__secret__"}]以下の 2 つの関数は、アプリ自身のコンテナからのみ 呼び出せます。メッセージルーターは、あらゆるブラウザロールおよび消費側アプリに対してこれらを拒否します。そのため ConsumedApp ハンドルには対応するものがありません。別のアプリのシークレットカラムは秘匿された状態で届き、そのままです。
revealSecrets / reveal_secrets
自分のテーブルの行を、シークレットカラムを復号した状態で読み取ります。
Python
rows = await ironflock.reveal_secrets("credentials", {
"limit": 1,
"filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}],
})
# [{"tsp": "...", "device_id": "d-1", "api_token": "hunter2"}]クエリパラメータは getHistory と同じですが、1 点だけ異なります:limit は 100 を超えられません。この上限は、1 回の呼び出しでテーブル全体が復号されるのを防ぐためのものであり、データバックエンドはより大きな値を黙って切り詰めるのではなく 拒否 します。
データバックエンドが応答する reveal は、デバイス上のアプリ資格情報ごとに 1 分あたり最大 30 回です。それを超えると、呼び出しは URI_RATE_LIMITED で失敗します。シークレットは使うたびに復号するのではなく、一度復号したら保持してください。シークレットカラムを持たないテーブルは URI_NOT_A_SECRET_COLUMN で、シークレットカラムに対するフィルタは URI_SECRET_COLUMN で拒否されます。
verifySecret / verify_secret
候補の値をシークレットカラムと照合します。シークレットを明かすことは ありません — reveal 関数に対するパスワード照合版です。比較はデータバックエンド内部で一定時間で実行され、返ってくるのは結果だけです。
Python
ok = await ironflock.verify_secret(
"credentials", "api_token", submitted_token,
{"limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}]},
)
if ok:
...パラメータ:
| パラメータ | 型 | 説明 |
|---|---|---|
tablename | str / string | 照合先のテーブル |
column | str / string | 比較するシークレットカラム |
candidate | str / string | 検証する平文。サーバー側で比較され、テーブルに書き込まれることはありません |
query_params / queryParams | SecretQueryParams | どの行を対象にするか。デフォルトは最新の 1 行。limit は 100 を超えられません |
Go では、パラメータはコンテキストの後に続く位置引数の文字列で、クエリは *ironflock.TableQueryParams です(最新の 1 行には nil)。
JavaScript は { match, checked } を、Go は Match と Checked を持つ *ironflock.SecretVerifyResult を返し、Python は単なる bool を返します。checked が得られる場合は活用する価値があります:候補が誤っていた場合(checked > 0)と、セレクタがどの行にも一致しなかった場合(checked === 0)を区別できるためです。この 2 つは、ユーザーに伝えるべき内容がまったく異なる失敗です。
各行はそれぞれ独自のランダムな値で暗号化されるため、同じシークレットを保持する 2 つの行は異なる暗号文を保存します。これが、シークレットカラムに対する = フィルタが機能し得ない理由であり(データバックエンドはシークレットカラムに対するあらゆるフィルタを URI_SECRET_COLUMN で拒否します)、この関数が代わりに存在する理由です。
データバックエンドが応答する照合は、デバイス上のアプリ資格情報ごとに 1 分あたり最大 120 回です。それを超えると、呼び出しは URI_RATE_LIMITED で失敗します。リクエストのたびに照合する場合(API キーなど)は、成功した結果を短時間キャッシュしてください。シークレットカラムではないカラムは URI_NOT_A_SECRET_COLUMN で拒否されます。
アプリ間データアクセス
同じプロジェクト内で、別のアプリのフリートデータを自分のアプリから読み取ります。あなたのアプリは、自身の data-template.yml の consumes: セクションで提供側のアプリを宣言する(または下記のワイルドカード許可を保持する)必要があり、プロジェクトユーザーがアクセスを許可する必要があります。アクセスは 読み取り専用 です。提供側が共有するテーブルおよび transform の履歴をクエリしたり、行をリアルタイムに購読したりできますが、書き込むことはできません。消費先アプリへの接続はアプリごとにキャッシュされ、インスタンスが停止すると自動的に閉じられます。
接続には、デバイスエージェントが注入するアプリごとの資格情報(APP_AUTH_ID、APP_AUTH_SECRET)が必要です。プラットフォームは、別のアプリの realm ではレガシーなデバイス資格情報 — アプリごとの資格情報を受け取らなかったアプリ(古いデバイスエージェントの下で動作している場合など)が代わりに使うシリアル番号 — を拒否します(NOT_AUTHORIZED。メッセージにその旨が示されます)。ただし、提供側のアプリが同じデバイス上で動作している場合は例外で、その場合は読み取り専用ではなく、そのアプリ自身の完全な権限でその realm に参加します。
アプリが ワイルドカード 許可(consumes: [{ app: "*" }])を保持している場合、listConsumableApps / list_consumable_apps と connectToAllApps / connect_to_all_apps(下記)を使用して、提供側を動的に検出して開くことができます。
connectToApp / connect_to_app
別のアプリのデータバックエンドへの読み取り専用接続を開き、ハンドルを返します。ハンドルは getHistory / get_history、subscribeToTable / subscribe_to_table、getSeriesHistory / get_series_history(テーブルのみ)を公開します(自分のテーブルで使うのと同じクエリと購読で、パラメータと結果も同じです)。さらに close と、共有された tables / transforms カタログも提供します。
Python
# Open a read-only handle on another app's data backend
weather = await ironflock.connect_to_app("weather-app")
# Inspect what the provider shares
print([t["tablename"] for t in weather.tables])
# Query history and subscribe, just like your own tables
rows = await weather.get_history("forecasts", {"limit": 100})
def on_forecast(*args, **kwargs):
print("New forecast:", args)
await weather.subscribe_to_table("forecasts", on_forecast)パラメータ:
| パラメータ | 型 | 説明 |
|---|---|---|
app_name / appName | str / string | 提供側アプリの名前。consumes: セクションで宣言したもの |
stage | str / string、オプション | 提供側のステージ:"dev" または "prod"。デフォルトは自分のアプリのステージ |
on_error / onError | callable、オプション | 開いている接続が最終的に拒否された場合(例:許可が後で取り消された場合 — 拒否がいつ最終と見なされるかは下記を参照)に CrossAppAccessError とともに呼び出されます |
アクセスが拒否された、または誤って使用された場合、code フィールド(Go では Code。ironflock.CodeNoGrant などの定数があります)を持つ CrossAppAccessError が送出(Python)/スロー(JavaScript)/返却(Go。errors.As で判定する *ironflock.CrossAppAccessError)されます:NO_GRANT、PROVIDER_NOT_INSTALLED、UNKNOWN_APP、PRIVATE_TABLE、SECRET_COLUMN(提供側がシークレットとしてマークしたカラムに対して — グループの中も含めて — フィルタをかける、またはそのカラムを選択するクエリ)、または NOT_AUTHORIZED(許可が取り消された、またはその時点で検証できなかった場合 — あるいはアプリがレガシーなデバイス資格情報を提示した場合で、その場合はメッセージにその旨が示されます)。
connectToApp / connect_to_app の待機中に拒否された場合、呼び出しは即座に失敗します。ハンドルが開いた後は、拒否が続いた場合にのみ最終と見なされます — 接続が最後に確立されて以降に 3 回以上の拒否があり(ルーターが認証上の理由でセッションを閉じた場合も、拒否された再接続も、それぞれ 1 回と数えます)、その最初の拒否が 60 秒以上前である場合です。これは、プラットフォームがアクセスを一時的に検証できない間も同じ方法で拒否するためです。それまでの間、ハンドルは再接続を続け、再接続に成功すれば回復します。拒否が最終となると、on_error / onError / OnError が呼び出され、ハンドルは破棄されます:次の connectToApp / connect_to_app / ConnectToApp は新しい接続を開きます。それ以降、そのハンドルの読み取りと購読は、on_error / onError が受け取ったエラーで即座に失敗します — Go では、wamp.ErrStopped をラップしたエラーで失敗します。
Python では
stageとon_errorはキーワード引数です。JavaScript ではオプションオブジェクトを介して渡します(connectToApp(appName, { stage, onError }))。Go では構造体を介して渡します(ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError}))。ここでOnErrorはfunc(*ironflock.CrossAppAccessError)です。Go では、ctxが制限するのは呼び出し元の待機だけです:同じアプリに対する並行した呼び出しは 1 回の試行を共有し、Close(ctx)でハンドルを閉じます。
listConsumableApps / list_consumable_apps
プロジェクト内の すべての 非プライベートな提供側を一覧します — ワイルドカード 消費許可(data-template.yml 内の consumes: [{ app: "*" }]、プロジェクトユーザーによって許可される)を保持するアプリのための検出プリミティブです。1 回の呼び出しを実行し、接続は 一切 開きません:返されたカタログをピッカーに表示し、必要なものについて connectToApp / connect_to_app を呼び出します — または connectToAllApps / connect_to_all_apps ですべてを一度に開きます。
注意: アプリの
data-template.ymlで許可を宣言し、*を引用符で囲んでください — 裸の*は YAML のエイリアスであり、解析できません:
consumes:
- app: "*"Python
providers = await ironflock.list_consumable_apps()
for p in providers:
print(p["app"], list(p["stages"].keys())) # e.g. "weather-app" ["dev", "prod"]各エントリは 1 つの提供側を表します:
| フィールド | 型 | 説明 |
|---|---|---|
app | str / string | 提供側アプリの名前 |
provider_app_key | int / number | 提供側のアプリキー |
stages | dict / object | ステージごとのカタログ { dev?, prod? }。提供側がそのステージのデータバックエンドを持つ場合にのみ、ステージが含まれます。各カタログは、共有される非プライベートな tables と transforms を保持します |
Go では、各エントリは App、ProviderAppKey、Stages.Dev / Stages.Prod(Tables と Transforms を持つ *StageCatalog。存在しないステージでは nil)を持つ ironflock.ConsumedAppInfo です。Catalog(stage) は名前でカタログを返します。
アプリがワイルドカード許可を保持していない場合、code: NO_GRANT を持つ CrossAppAccessError が送出(Python)/スロー(JavaScript)/返却(Go)されます。
connectToAllApps / connect_to_all_apps
プロジェクト内の すべての 非プライベートな提供側への読み取り専用ハンドルを 1 回の呼び出しで開きます(ワイルドカード消費側のみ)。listConsumableApps / list_consumable_apps を介して提供側を列挙し、それぞれを開きます。要求されたステージのデータバックエンドを持たないものはスキップします。各ハンドルは connectToApp / connect_to_app と同じキーでキャッシュされるため、後続の connectToApp(name) は既にウォームアップ済みのハンドルを返します。返されたハンドルは、インスタンスが停止すると一括して閉じられます。
Python
apps = await ironflock.connect_to_all_apps(
on_error=lambda err: print("Provider skipped:", err)
)
for app in apps:
rows = await app.get_history(app.tables[0]["tablename"], {"limit": 10})
print(app.app, rows)パラメータ:
| パラメータ | 型 | 説明 |
|---|---|---|
stage | str / string、オプション | 提供側のステージ:"dev" または "prod"。デフォルトは自分のアプリのステージ |
continue_on_error / continueOnError | bool / boolean、オプション | true(デフォルト)の場合、開くのに失敗した提供側は on_error / onError に報告され、結果から除外されます。false の場合、最初の失敗が送出/スローされます |
on_error / onError | callable、オプション | 開けなかった各提供側とともに呼び出されます(continue_on_error / continueOnError が true の間)。また、既に開かれている接続が後で最終的に拒否された場合(例:許可が取り消された場合)に CrossAppAccessError とともに呼び出されます |
正常に開かれた提供側ハンドルを返します(connectToApp / connect_to_app と同じハンドル型)。アプリがワイルドカード許可を保持していない場合、code: NO_GRANT を持つ CrossAppAccessError が送出(Python)/スロー(JavaScript)/返却(Go)されます。
Python では
stage、on_error、continue_on_errorはキーワード引数です。JavaScript ではオプションオブジェクトを介して渡します(connectToAllApps({ stage, onError, continueOnError }))。Go では構造体を介して渡します(ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError}))。ここでOnErrorはfunc(error)で、StopOnError: trueはcontinueOnError: falseに相当します。
マネージドファイルストレージ
すべてのアプリのデータバックエンドは、テーブルと並んでプライベートなオブジェクトストレージを備えており、files プロパティ(Go では ifl.Files()。Start より前に取得できる *filestore.FileStore)からアクセスできます。画像、PDF、カメラフレーム、ファームウェアのバイナリなど、テーブルの行に収めるべきでないものはすべてここに保存します。セットアップは不要です:データテンプレートに files: セクションがないアプリでも、default という名前空間が 1 つ用意されます(独自の名前空間を宣言するテンプレートでは、これが置き換えられます — 名前空間 を参照)。
このセクションはクライアント側のリファレンスです。プラットフォーム側 — 名前空間・クォータ・保持期間の宣言、アプリ間でのファイル共有、アプリごとの S3 資格情報、オンプレミスでの動作 — については、ファイルストレージ を参照してください。
重要なのは、オブジェクトを保存すると 永続的な URL が返され、それをそのままテーブルのカラムに書き込めるという点です。これにより、ダッシュボードのウィジェットは追加の作業なしにそのオブジェクトを表示できます:
Python
from datetime import datetime, timezone
# Store an object and get a permanent URL back in the same call
info = await ironflock.files.put("part-1.jpg", jpeg_bytes, content_type="image/jpeg")
# The URL is safe to store in a table column — a dashboard widget can then
# render <img src="{{photo_url}}"> without any extra round trip
await ironflock.publish_to_table("inspections", {
"tsp": datetime.now(timezone.utc).isoformat(),
"part_id": "1",
"photo_url": info.url,
})
# Read it back
data = await ironflock.files.get("part-1.jpg")
# Every object in the folder 2026/, in its subfolders too (pages are fetched for you)
async for obj in ironflock.files.iter(prefix="2026/"):
print(obj.key, obj.size)この URL が期限切れになることはありませんが、公開リンクでは ありません:このデータバックエンドに対する READ 権限を持つ認証済みのリクエスト元だけが読み取ることができ、認証プロキシがリクエストのたびにそれを再確認します。したがって、データベースに保存しても安全です。
名前空間
名前空間 は、ポリシー(保持期間、共有ルール、許可されるコンテンツタイプ)を伴うキーのプレフィックスです。個別のバケットではありません。アプリのすべての名前空間は、そのアプリの単一のストレージ領域の中に存在します。宣言するのは、あるオブジェクト群に 異なるルール が必要な場合だけにしてください。それ以外の場合は default のままにして、2026/03/part-1.jpg のようなキーのパスでオブジェクトを整理します。
名前空間は data-template.yml で宣言します。いずれかを宣言すると、暗黙の default は 置き換えられます:default という名前の名前空間も併せて列挙してください。そうしないと、名前空間を指定しないすべての呼び出しが NO_SUCH_NAMESPACE で失敗します。
files:
# Storage budget the app suggests for itself. The project user can change it,
# and their setting is the one that gets enforced.
quotaBytes: 5368709120
namespaces:
# Keep default: declaring namespaces replaces the implicit one.
- name: default
- name: frames
description: Raw camera frames, one JPEG per inspected part.
contentTypes: ["image/jpeg"]
maxObjectBytes: 20971520
retention: { deleteAfter: 30 days }この予算は名前空間ごとではなく、アプリ全体に対して一度だけ宣言される点に注意してください。名前空間はアプリの単一のストレージ領域の中にあるキーのプレフィックスにすぎないため、プレフィックスごとの予算を課す対象が存在しません。maxObjectBytes は 名前空間ごと です — これは合計ではなく、単一のオブジェクトの上限を定めます:指定しない場合は 100 MiB、最大 5 GiB です(それを超えると OBJECT_TOO_LARGE)。
以下のすべてのメソッドは名前空間をオプション引数として受け取り、デフォルトは default です。空の名前空間("")も default を意味します。
オブジェクトの保存と読み取り
Python
# Bytes in, bytes out
info = await ironflock.files.put("reports/march.pdf", pdf_bytes, content_type="application/pdf")
data = await ironflock.files.get("reports/march.pdf")
# Or straight from/to a local file — these stream on the large-object path,
# so a multi-gigabyte file never has to fit in memory
await ironflock.files.put_file("firmware/v2.bin", "/data/build/v2.bin")
await ironflock.files.get_to_file("firmware/v2.bin", "/tmp/v2.bin")| メソッド | 説明 |
|---|---|
put(key, data, …) | オブジェクトを保存します(Python では bytes、JavaScript では Uint8Array、Go では []byte)。url を含むオブジェクトのメタデータを返します |
get(key, namespace?) | オブジェクトの内容を返します。インライン制限を超えるオブジェクトはオブジェクトストレージから直接取得され、それができない場合はルーター経由で範囲ごとに取得されます(ラージオブジェクト を参照) |
put_file(key, path, …) / get_to_file(key, path, …) | Python と Go(PutFile / GetToFile。Go は PutReader で io.Reader から、GetTo で io.Writer へのストリーミングも行えます)。ローカルファイルから保存、またはローカルファイルへ書き出します。ラージオブジェクトのパスではストリーミングされます |
delete(key, namespace?) | オブジェクトを削除します |
copy(key, to, …) | オブジェクトをコピーします。別の名前空間へコピーすることもできます |
move(key, to, …) | コピーしてから削除します。アトミックではありません — サービスに move の動詞がないため、削除に失敗すると両方のコピーが残ります |
JavaScript にファイルパス用のヘルパーがないのは、パッケージが Node とブラウザの両方に対して単一のビルドを提供しているためです — ローカルファイルの読み書きは fs で自分で行ってください。
Python では、put_file と get_to_file のファイル処理はイベントループの外で実行されます。get_to_file は存在しない親ディレクトリを作成し、オブジェクトの最初のバイトを受け取った時点でファイルをその場で書き込みます:拒否された読み取り(オブジェクトが存在しない、認可されていない)ではパスに一切手が加えられませんが、途中で失敗した読み取りでは、それまでに書き込まれた内容が残ります。FIFO はオブジェクトのストリーミングに合わせて書き込まれ、FIFO を開く処理は読み手を待つため、誰も読まない FIFO は呼び出しをブロックします(イベントループはブロックしません)。asyncio.wait_for でこれを打ち切ることができます。put_file は FIFO を最後まで読んでから保存します。
Go でも GetToFile は存在しない親ディレクトリを作成しますが、ターゲットの隣にある一時ファイルにダウンロードし、完了した時点でそれをリネームして配置します:失敗またはキャンセルされたダウンロードが不完全なファイルを残すことはなく、以前のファイルはそのまま保持されます。シンボリックリンクはたどられます(リンク先のファイルが書き込まれ、リンクはそのまま残ります)。置き換え後のファイルは、プロセスに可能な範囲で、元のファイルの所有者、グループ、パーミッションビットを引き継ぎます。FIFO やデバイスは、オブジェクトのストリーミングに合わせてその場で書き込まれます。隣に新しいファイルを作成できないターゲット(読み取り専用や疑似ファイルシステム上のものなど)には、自分で開いたファイルに対して GetTo を使用してください。
put が受け取るもの: content_type / contentType(MIME タイプ。名前空間によって許可されるものが制限される場合があります)と namespace。Python ではこれらはキーワード引数です。JavaScript ではオプションオブジェクトに渡します。Go ではオプションです(filestore.ContentType(…)、filestore.Namespace(…))。
一覧取得と検査
| メソッド | 説明 |
|---|---|
list(…) | ファイルサービスの一覧の 1 ページ分で、デフォルトではフォルダ形式です:objects にはキーが / を含まずにプレフィックスに続くオブジェクトが、prefixes にはその下のフォルダが入ります。delimiter に "" を指定するとフラットに一覧します — プレフィックス配下のすべてのオブジェクトを、サブフォルダ内のものも含めて返します。objects、prefixes、is_truncated / isTruncated / IsTruncated、および次のページを取得するために渡す cursor を返します |
iter(…) / iterate(…) | プレフィックス配下の すべての オブジェクトを、サブフォルダ内のものも含めて走査する非同期イテレータで、ページングは自動的に行われます。Python では iter、JavaScript では iterate、Go では Iter(range ループ用の iter.Seq2[*ObjectInfo, error])という名前です |
stat(key, namespace?) | 内容を転送せずに、1 つのオブジェクトのメタデータを取得します |
exists(key, namespace?) | オブジェクトが存在するかどうか |
namespaces() | このアプリが使用できる名前空間 |
usage(…) | アプリがどれだけのストレージを使用しているか — 下記を参照 |
catalog() | 名前空間と、サーバーが発行する制限値およびクォータ。一度取得されてキャッシュされます:catalog(refresh=True) / catalog(true) / RefreshCatalog(ctx) で再取得します |
フォルダ。 / で終わるプレフィックスはフォルダを表します:iter(prefix="2026/") / iterate({ prefix: "2026/" }) / Iter(ctx, filestore.Prefix("2026/")) は 2026/a.txt と 2026/sub/c.txt を返しますが、2026-notes.txt は返しません。list はフォルダを名前で開くことができません。ファイルサービスは / で終わる一覧用プレフィックスを拒否するためです(FleetFiles v0.2 は INTERNAL を返します):プレフィックス 2026 で list を呼び出すと、オブジェクトとして 2026-notes.txt が、プレフィックスとしてフォルダ 2026/ が返されます。サービスはキーを Unicode 正規化形式 C で保存するため、フォルダのプレフィックスも NFC で指定してください。プレフィックスの別の正規化形式の下にあるキーに遭遇したイテレーションは、すべてのキーを読み飛ばすのではなく失敗します。
オブジェクトは、3 つの SDK すべてで同じフィールドで表現され、名前は各言語の命名スタイルに従います:namespace、key、size、etag、content_type / contentType、last_modified / lastModified、checksum_sha256 / checksumSha256、url — Go では filestore.ObjectInfo のフィールド Namespace、Key、Size、ETag、ContentType、LastModified、ChecksumSHA256、URL です。
ストレージ使用量とクォータ
usage は 1 回の呼び出しでオブジェクトストアから直接回答するため、合計値は SDK が足し合わせたものではなく正確な値です:
Python
u = await ironflock.files.usage()
print(u.size_bytes, u.object_count, u.quota_bytes, u.free_bytes)
# Break the total down per namespace (costs one listing per namespace)
detailed = await ironflock.files.usage(detail=True)
print(detailed.per_namespace) # {"default": 1048576, "frames": 73400320}Go では、フィールドは filestore.StorageUsage の SizeBytes、ObjectCount、QuotaBytes、FreeBytes、PerNamespace です。
| フィールド | 意味 |
|---|---|
size_bytes / sizeBytes | 現在保存されているバイト数 |
object_count / objectCount | 保存されているオブジェクトの数 |
quota_bytes / quotaBytes | 実際に課される 予算。0 は無制限を意味します |
free_bytes / freeBytes | 残りのバイト数。-1 は無制限を意味します — ここで 0 を返すと「いっぱい」と読めてしまうためです |
per_namespace / perNamespace | 名前空間ごとのバイト数。詳細な内訳を要求したときに のみ 含まれます |
名前空間ごとの内訳がデフォルトで無効になっているのは、ストアがそれに直接答えられないためです:ストアはストレージ領域単位で集計しており、名前空間は単なるプレフィックスにすぎないため、SDK が各名前空間を一覧してサイズを足し合わせる必要があります。必要なときに要求してください。ホットパスでは使わないでください。
2 つの異なるクォータが登場するので、区別しておく価値があります。catalog() はその両方を返します:
| フィールド | 意味 |
|---|---|
quota_bytes / quotaBytes | 実際に 課される 値で、オブジェクトストアから読み取られます — プロジェクトユーザーの設定です |
suggested_quota_bytes / suggestedQuotaBytes | アプリのデータテンプレートが要求した値。何も要求していない場合は 0 |
Go では、Catalog(ctx) は QuotaBytes と SuggestedQuotaBytes を持つ *filestore.Catalog を返します。
この 2 つは、ユーザーがアプリの予算を引き上げたり引き下げたりした場合に食い違います。実際に課される値をテンプレートではなくストアから読み取るのはそのためです — アプリを再デプロイしたときに、ユーザーの選択が黙って上書きされてはいけません。UI では両方を表示できます(「アプリの推奨は X ですが、あなたは Y に設定しています」)。実際の制限には常に前者が使われます。ユーザーはいつでも予算を変更でき、catalog() は最後に取得した時点の値を報告します:現在の値は usage() から読み取ってください。
オブジェクトの共有
リンクには 2 種類あり、その違いが重要です:
| メソッド | 有効期間 | 読み取れる相手 |
|---|---|---|
url(key, …) | 永続的 | このデータバックエンドに対する READ 権限を持つ認証済みのリクエスト元のみ — リクエストのたびに再確認されます。テーブルのカラムに保存しても安全です |
share_url / shareUrl / ShareURL | 有効期限あり(デフォルト 15 分、サーバー側で上限が適用されます) | リンクを持っている人なら誰でも。 使用時に認可が再確認されることはありません |
share_url / shareUrl はベアラー型のケーパビリティです:一時的なアクセスが必要な人に渡すためのものであり、データベースに保存しないでください。ダッシュボードが表示するものには url を使用してください。
デプロイに HTTP エッジがない場合(たとえばプレーン HTTP のアプライアンス)、url は None / undefined / ""(Go)を返します — これは get にフォールバックすべきという合図です。オブジェクトの etag を version 引数として渡すと(Go では filestore.Version(etag))、ブラウザはレスポンスをイミュータブルなものとしてキャッシュできます。
Python と Go では、cloud_url(key, …) / CloudURL(ctx, key, …) がアプライアンスのクラウドファイルトンネルを経由する永続的な URL(https://i<instanceKey>-files.<cloud-edge>)を返します:アプライアンスがオンラインである間はインターネットから到達でき、認可は url と同じです。クラウドホスト型のデプロイ(url がすでにインターネットから到達可能な場合)、HTTP エッジがない場合、およびデータバックエンドのクラウド共有がオフになっている間は None(Go では "")を返します。ユーザーはクラウド共有をいつでも切り替えられるため、cloud_url / CloudURL はキャッシュされたカタログが 30 秒より古い場合にカタログを読み直します。
upload_url / uploadUrl / UploadURL は、直接アップロードを受け付ける有効期限付きの URL を発行し、url、method、headers、expires_in / expiresIn を返します(Go では URL、Method、Headers、ExpiresIn を持つ *filestore.UploadTarget)。オブジェクトが発行時のコンテンツタイプで保存されるよう、ヘッダーは返されたとおりに送信してください。名前空間のコンテンツタイプとサイズ上限、宣言された size、およびクォータは、URL の使用時ではなく発行時に確認されます:URL はホストに対してのみ署名されているため、URL を持っている人は誰でも、有効期限が切れるまでそのキーに任意の内容を書き込めます。share_url / shareUrl と同様に、これはベアラー型のケーパビリティです — 書き込みのためのものです。
ラージオブジェクト
SDK は サイズ に応じて転送方法を自動的に選択します — 設定するものは何もありません:
| オブジェクトのサイズ | 転送経路 |
|---|---|
| インライン制限まで(現在は 6 MiB) | メッセージルーターを経由する 1 回の呼び出し |
| それより大きい場合 | ルーターを経由せず、HTTPS でオブジェクトストレージへ直接転送 |
| それより大きく、直接転送のパスがない環境で読み取る場合 | メッセージルーターを経由して範囲ごとに転送 |
正確な制限値は、実行時にサーバーから catalog() の inline_max_bytes / inlineMaxBytes / InlineMaxBytes として報告されるため、SDK のリリースなしに引き上げることができます。
直接転送のパスには、署名付き URL を発行できるデプロイと、ルーターだけでなくオブジェクトストレージのホストにも到達できるデバイスが必要です。そのいずれかが欠けている場合 — そのデプロイでは署名付き URL を発行できないとカタログが示している場合(オンプレミスアプライアンスではデフォルトでそうです)、読み取り用 URL の発行が NOT_SUPPORTED を返した場合、またはバイトが 1 つも届く前にオブジェクトストレージに到達できなかった場合 — get(および Python の get_to_file、Go の GetTo と GetToFile)は代わりにルーター経由でオブジェクトを範囲ごとに読み取ります(FleetFiles v0.1.33 以降)。各範囲の大きさは最大でインライン制限です。各範囲はそれぞれ独立した読み取りです:そのような読み取りの最中にオブジェクトが置き換えられると、読み取りは INTERNAL(“object changed while it was read”)で失敗します。書き込みにはこのようなフォールバックはありません。
書き込みには上限が 2 つ残っており、どちらの場合も、どちらの上限に達したのかを示す理由とともに TOO_LARGE が報告されます:
- 5 GiB — オブジェクトストアの単一アップロードの上限です。マルチパートアップロードはまだ実装されていません。
- 直接エンドポイントがない環境でのインライン制限 — 署名付き URL を発行できないデプロイ(アプライアンスではデフォルトでそうです)では、それより大きなオブジェクトをそもそも保存できません。リトライしても、チャンクを小さくしても解決せず、メッセージにもそのように記載されます。
現場でよく起きる、直接転送のパスの 2 つの失敗には、認可の問題のように見えてしまわないよう、専用のコードが用意されています:PRESIGN_UNREACHABLE(ルーターのみを許可しているプロキシ — 読み取りは範囲ごとの転送にフォールバックし、書き込みは失敗します)と CLOCK_SKEW(オブジェクトストアは 15 分以上ずれたリクエストを拒否します — デバイスの NTP を確認してください)。
ファイルストレージのエラー
ファイルサービスまたは直接転送の失敗は、安定した code と人間が読める reason を持つ FileStoreError を送出(Python)/スロー(JavaScript)/返却(Go)します。分岐は必ず code で行い、reason では決して行わないでください。 ローカルファイルのエラー、キャンセルされた呼び出し、および下記でマッピングされているもの以外のルーターレベルの WAMP エラーは、そのまま送出されます。
Python
from ironflock.filestore import FileStoreError
try:
await ironflock.files.put("huge.bin", payload)
except FileStoreError as e:
if e.code == "QUOTA_EXCEEDED":
print("Filestore is full:", e.reason)
else:
raise| コード | 意味 |
|---|---|
NOT_AUTHORIZED | 呼び出し元はこの操作を実行できません |
NO_SUCH_NAMESPACE | その名前空間はデータテンプレートで宣言されていません |
NO_SUCH_OBJECT | そのキーは存在しません |
TOO_LARGE | 1 回の呼び出しでの転送制限を超えています |
OBJECT_TOO_LARGE | 名前空間自身の maxObjectBytes を超えています |
QUOTA_EXCEEDED | ファイルストアが満杯です |
CONTENT_TYPE_NOT_ALLOWED | 名前空間が contentTypes を制限しています |
INVALID_KEY | 形式が不正なキーまたはプレフィックス(下記参照。FleetFiles v0.2 はまだ INTERNAL として報告します) |
INVALID_RANGE | オブジェクトの範囲外のバイト範囲。SDK がそれを要求することはありません |
NOT_SUPPORTED | バックエンドはこれを実行できません |
NOT_AVAILABLE | このデプロイにはファイルサービスがありません — あるいはまだありません:プラットフォームの再起動後や、ファイルサービスがプロシージャを登録している間です。呼び出しはどこでも実行されていないため、繰り返して構いません |
PRESIGN_UNREACHABLE | オブジェクトストレージに直接到達できません(プロキシが原因かもしれません) |
CLOCK_SKEW | デバイスのクロックのずれが大きすぎます |
INTERNAL | それ以外のすべて — 範囲ごとの読み取り中に変更されたオブジェクトも含みます |
新しいサーバーは、この SDK リリースが知らないコードを導入する場合があります。それらはまとめられることなく code としてそのまま渡されるため、認識できない値は一般的な失敗として扱ってください。
ファイル呼び出しは接続を待ちますが(Python と Go では最大 10 秒、JavaScript では 6 秒)、再試行はされません:テーブル操作とは異なり、プラットフォームの再起動を乗り切ることはないため、再起動中の呼び出しは、ファイルサービスが再登録を終えるまで NOT_AVAILABLE で失敗することがあります。そのような呼び出しはどこでも実行されていないため、アプリから繰り返して構いません。
キー。 キーの正否を判断するのはファイルサービスであり、SDK はキーとプレフィックスを指定されたとおりに送信します。形式が不正なキーまたはプレフィックス — 空のもの、先頭または末尾に / があるもの(iter / iterate のフォルダプレフィックスは除く)、空・.・.. のセグメントや、. で終わるセグメント、空白で始まるか終わるセグメントを含むもの、\ : * ? " < > | ^ ~ のいずれか、制御文字、または Unicode の書式文字を含むもの、900 バイト・20 セグメント・セグメントあたり 255 バイトのいずれかを超えるもの、あるいは予約済みの _sys/ プレフィックスの下にあるもの — は、キーのエラーを分類するファイルサービスでは INVALID_KEY で、FleetFiles v0.2 では INTERNAL で拒否されます。タイムスタンプに注意してください:2026-10-09T12:00:00Z は : を含むため、有効なキーのセグメントではありません。
Python では
FileStoreErrorはironflock.filestoreからインポートします。JavaScript ではパッケージルートからエクスポートされています(import { FileStoreError } from "ironflock")。Go では*filestore.Error(ironflock.FileStoreErrorとしても利用可能)で、コードはfilestore.CodeQuotaExceededなどの定数として提供されています。
デバイス間通信
registerDeviceFunction / register_device_function
同じアプリを(同じステージで)実行している他のデバイスが呼び出せるプロシージャを登録します。ダッシュボードウィジェットのアクションからも呼び出せます。SDK は完全な URI <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic> を自動的に組み立てます。ここで STAGE は DEV または PROD です。ルーターはアプリからのそれ以外の登録を受け付けず、また単一登録しか受け付けません(アプリが使用できる URI を参照)。登録は再接続のたびに復元されます。
Python
def add(a, b):
return a + b
await ironflock.register_device_function("com.myapp.add", add)
register()はregister_device_function()のエイリアスです。
callDeviceFunction / call_device_function
同じアプリの別のデバイスが登録したプロシージャを呼び出します。SDK はターゲットデバイスのキーを使用して完全な WAMP トピックを自動的に組み立てます:<SWARM_KEY>.<device_key>.<APP_KEY>.<STAGE>.<topic> で、これはターゲットデバイス上で registerDeviceFunction / register_device_function が登録するのと同じ URI です。
Python
result = await ironflock.call_device_function(
42, # target device key
"com.myapp.add", # procedure name
args=[3, 5] # arguments
)
print(result) # 8call
完全な WAMP URI を使用してリモートプロシージャを呼び出します。アプリは some.full.wamp.topic のような名前を登録できないため、このような名前で到達できるのは、プラットフォームのサービスがアプリのデータ realm に登録したプロシージャ(データバックエンドやファイルストレージのものなど)だけです。アプリの別のデバイスが登録した関数を呼び出すには、callDeviceFunction / call_device_function を使用してください。
Python
result = await ironflock.call("some.full.wamp.topic", args=[42])アプリが使用できる URI
アプリコンテナは、ルーターロール app で自身のデータ realm realm-<SWARM_KEY>-<APP_KEY>-<dev|prod> に参加します。ルーターは、そこでのすべての公開、購読、呼び出し、登録を、そのロールの許可リストと照合します。SDK 自身のメソッドはこの範囲内にとどまります。このルールが問題になるのは、publish、subscribe、call に独自の URI を渡す場合や、connection を直接使用する場合(Go では Connection())です。拒否された操作は wamp.error.not_authorized で失敗します(Go では ironflock.WampURI(err) がこれを返します)。
| 自分の realm 上の URI | 許可される操作 | 使用するもの |
|---|---|---|
独自の名前(例:com.myapp.status) | 公開、購読(プレフィックス購読も可)、呼び出し — 登録は不可 | publish、subscribe、call |
<SWARM_KEY>.<APP_KEY>.<table>、bulk.<SWARM_KEY>.<APP_KEY>.<table> | 公開 — 購読は不可 | publishToTable / publish_to_table、reportError / report_error、publishRowsToTable / publish_rows_to_table |
transformed.<table>、transformed.bulk.<table> | 購読のみ | subscribeToTable / subscribe_to_table |
databackend.errors.…、files.events… | 購読のみ | データバックエンドのエラーイベントとファイルイベント |
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name> | 登録(単一)、呼び出し、公開 — 購読は不可 | registerDeviceFunction / register_device_function、callDeviceFunction / call_device_function |
append.…、appendBulk.…、history.transformed.…、secret.reveal.…、secret.verify.…、files.read.…、files.write.… | 呼び出し(SDK が呼び出します) | テーブル、履歴、シークレット、ファイルの各メソッド |
sys.appaccess.list、sys.appaccess.resolve | 呼び出し | listConsumableApps / list_consumable_apps、connectToApp / connect_to_app、connectToAllApps / connect_to_all_apps |
wamp.session.get | 呼び出し | JavaScript SDK のハートビート |
ID チェックされる名前。 数字で始まる名前 — テーブルへの書き込みトピックとデバイス関数の URI — は、プラットフォームの ID チェックも通過する必要があり、このチェックによって realm のプロジェクトキー(SWARM_KEY)に結び付けられます。登録が受け付けられるのは、自分の realm のプロジェクトキー、アプリキー、ステージを持つ、自分のデバイスの関数 URI に対してだけです。プレフィックス登録やワイルドカード登録は、これら 4 つのセグメントを固定している場合にのみ受け付けられます。公開と呼び出しに必要なのは realm のプロジェクトキーだけです:別のアプリキーを持つテーブルトピックに公開された行は受け付けられますが、どのデータバックエンドにも届かずに失われます。これらの URI の組み立ては SDK に任せてください。
予約済みの名前。 ルーターは、上記の 2 つのアプリ間プロシージャを除く sys. と、wamp.session.get を除く wamp. を拒否します。表にあるその他の名前はプラットフォームのものです:独自の名前を、数字、bulk.、transformed.、databackend.errors.、files.、append.、appendBulk.、history.、secret. で始めないでください。それ以外の先頭セグメントはすべて、アプリ独自のトピック空間です。
生のテーブルトピックは誰も購読しません。 これらのトピックは送信されたとおりの行を運び、シークレットカラムの平文も含みます。データバックエンドがシークレットカラムを暗号化するのは、行を保存するときだけだからです。subscribeToTable / subscribe_to_table は代わりに transformed.<table> から保存済みの行を読み取ります。その行は型付けされ、シークレットカラムはマスクされています。
他のアプリのデータは読み取り専用です。 提供側の realm では、あなたのアプリ(ルーターロール app_reader)は、ハートビートの wamp.session.get のほかには、transformed.… と files.events… の購読、history.transformed.… と files.read.… の呼び出ししかできません — 公開、登録、書き込み、シークレット用のプロシージャはいずれも利用できません。
デバイスメタデータ
setDeviceLocation / set_device_location
デバイスの GPS 位置の更新をプラットフォームに要求します。位置は IronFlock マップにリアルタイムで表示されます。
現在は利用できません: アプリのデータ realm 上でこの呼び出しに応答するプラットフォームサービスはまだないため、呼び出しは
wamp.error.no_such_procedureで失敗します。利用できるようになるまでは、位置を独自のテーブルに保存してください。
Python
await ironflock.set_device_location(long=8.6821, lat=50.1109)| パラメータ | 範囲 |
|---|---|
long | -180 〜 180 |
lat | -90 〜 90 |
位置履歴は保存されません。位置を時系列で追跡するには、専用テーブルを作成して
publish_to_table/publishToTableを使用してください。
getRemoteAccessUrlForPort
アプリの port-template.yml で宣言されたポートが、そのトンネルがアクティブになったときに到達可能となる URL を返します。
Python
url = ironflock.getRemoteAccessUrlForPort(8080)
# "https://<device_key>-<app_name>-8080.app.ironflock.com"
vnc = ironflock.getRemoteAccessUrlForPort(5900, protocol="tcp")
# "tcp://app.ironflock.com:<the tunnel's remote port>"| パラメータ | 型 | 説明 |
|---|---|---|
port | int / number | port-template.yml で宣言したポート |
protocol | str / string、オプション | ポートのプロトコル:"http"(デフォルト)、"https"、"tcp" または "udp" で、大文字・小文字は問いません。前後の空白は無視され、空の場合は "http" になります |
remote_port_environment / remotePortEnvironment | str / string、オプション | tcp/udp のみ:REMOTE_PORT_FOR_<port> の代わりにリモートポートを保持する変数 — テンプレートにあるそのポート自身の remote_port_environment で、0.19.8 より前のデバイスエージェントではこれが必要です |
http と https のポートは、デバイスエージェントが注入するトンネルドメイン(TUNNEL_DOMAIN)上で、トンネルラベル <device_key>-<app name, lower case>-<port>(https ポートの場合はプラットフォームの secure- プレフィックス付き)の下で提供され、常に https:// URL になります。オンプレミスアプライアンスに属するデバイス(エージェントが INSTANCE_KEY を注入します)では、代わりにクラウド経由で転送されるルート https://i<INSTANCE_KEY>-<label>.<CLOUD_TUNNEL_DOMAIN> が返されます。
tcp または udp の URL には、トンネルが割り当てたパブリックポートが含まれます。このポートは、デバイスエージェントが REMOTE_PORT_FOR_<port> として通知します(エージェント 0.19.8 以降):<protocol>://<TUNNEL_DOMAIN>:<remote port>。アプライアンスのデバイスでは、エージェントが通知している間はインターネット側のポート REMOTE_PORT_FOR_<port>_CLOUD が優先されます。それが現れる前、およびエージェントがそれを削除した後(クラウド転送がオフになった場合)は、アプライアンスローカルの URL が返されます。ポートが 0 の場合は、まだ割り当てられていません。
値は /data/env からその都度読み取られるため、結果をキャッシュせずにメソッドを再度呼び出してください。この URL では対応できないケースもあります:
- デバイスエージェントがトンネルするのは、本番(PROD)インストールのポートだけです。開発(DEV)コンテナでは、http(s) の URL はこのデバイス上にある同じアプリの PROD インストールの URL になり、tcp/udp では URL は得られません。DEV アプリには、代わりにデバイスの LAN アドレス(
DEVICE_LAN_IP、DEVICE_PORT_FOR_<port>)でアクセスしてください。 - ドメインなしでインストールされたアプライアンス(プレーンモード)のエッジデバイスでは、エージェントが
TUNNEL_DOMAINをlocalhostに設定するため、そこで組み立てられる URL は localhost を指します:このようなアプライアンスはアプリのポートを自身のホストポートで提供しますが、それはトンネル URL では表現できません。 - 未設定または空のトンネルドメインは、デフォルトで
app.ironflock.comになります。現在、プラットフォームはCLOUD_TUNNEL_DOMAINを設定しません:アプリが環境設定でこの変数を自ら設定しない限り、アプライアンスの URL はapp.ironflock.comを使用します。 - root 以外のユーザーとして実行されるコンテナでは、値はコンテナの起動時のものになります(環境変数 を参照)。
URL を組み立てられない場合、メソッドは None / null(Go では "" と false)を返します:デバイスキーまたはアプリ名がない場合、tcp/udp ポートのリモートポートがまだ通知されていない(または 0 である)場合、プロトコルが不明な場合、またはラベルが有効な DNS ラベルではない場合(63 文字を超える、またはアプリ名にドットが含まれる)です。ブラウザには環境がないため、http(s) の URL は deviceKey と appName オプションから app.ironflock.com 上に組み立てられます。
接続プロパティとライフサイクル
Python
| プロパティ | 型 | 説明 |
|---|---|---|
is_connected | bool | プラットフォームへの接続がアクティブかどうか |
connection | CrossbarConnection | 基盤となる接続インスタンス(上級者向け) |
| メソッド | 説明 |
|---|---|
run() | 接続を開始し、mainFunc を実行します(ブロッキング) |
await start() | 接続を非同期で開始します |
await stop() | 接続と消費先アプリへの接続を停止し、実行中のタスクをキャンセルします。接続はそれぞれの realm から離脱し、その後は再接続しません。start() で新たに接続します。まだ待機中の start() や connect_to_app は失敗します(接続の信頼性 を参照) |
await run_async() | 接続を開始し、非同期で維持します |
接続の信頼性
SDK は自動的に再接続します。ソケットが切断されると、ルーターが復帰するまで再試行し、その後すべてのサブスクリプションと登録済みのデバイス関数を復元します。再接続後に購読し直す必要はありません。いずれかを復元できなかった場合でも残りは復元され、失敗したものはセッションが続く間再試行されます — 1 秒後に始まり、待ち時間は 2 倍ずつ最大 30 秒まで延び、各待ち時間は最大 15 % の範囲で変動します — そして再接続のたびにも再試行されます:ルーターは以前にそれを受け付けていたので、その拒否は一時的なものです。たとえば、デバイス関数に対するプラットフォームの ID チェックが一瞬フェイルクローズした場合などです。最初の失敗は警告としてログに記録され、復元に成功するとその旨もログに記録されます。
テーブル操作 — 書き込み、履歴・系列・シークレットの読み取り、および消費先アプリの履歴の読み取り — も、ルーターやプラットフォームの更新などによるプラットフォームの再起動を乗り切ります。これらの操作は、接続が戻るのを最大 reconnect_window(Python、秒単位)/ reconnectWindowMs(JavaScript)/ WithReconnectWindow(Go、time.Duration)の間(デフォルトは 60 秒)待ち、データバックエンドがまだプロシージャを提供していない間は再試行します:ルーターが wamp.error.no_such_procedure を返す場合や、データバックエンドが自身の登録より先に届いた呼び出しを “client has no handler for registration” で拒否する場合です。このような呼び出しはハンドラに到達していないため、再試行によって行が二重に書き込まれることはありません。それ以外のエラーは発生したとおりに送出されます。ファイル呼び出しは再試行されません(ファイルストレージのエラー を参照)。送信済みのリクエストは、そのセッションが閉じると(ルーターがセッションを閉じた場合、リンクが切断された場合、または stop() が呼び出された場合)、応答を待たずに即座に失敗し、再試行されません:すでにハンドラに到達している可能性があるためです。Go では、このようなリクエスト — および待機時間内に接続が得られなかった操作 — は、wamp.ErrNotConnected(Stop の後は wamp.ErrStopped)をラップしたエラーで失敗します。Go のその他の操作(Publish、Subscribe、Call、デバイス関数、アプリ間の検出)は、接続を最大 10 秒待ち、再試行はされません。
特殊なケースは、ルーターには到達できるものの、アプリ用の realm がまだ存在しない場合です。同時インストール時の競合により、データバックエンドが realm をプロビジョニングする前にアプリコンテナが起動するため、最初の接続は wamp.error.no_such_realm で拒否されます。SDK は数秒ごとに再試行し、realm が作成された瞬間にアプリは立ち上がります。1 分経っても realm が存在しない場合、それはおそらく今後も作成されません(アプリが削除されたか、このステージ用のデータバックエンドがない)。そのため SDK はそれ以降、ルーターを永遠に毎秒叩き続けるのではなく、2 分に 1 回の試行に切り替えます。realm が現れるか、ルーター自体が落ちて復帰すれば、すぐに元の速いペースに戻ります。
stop() は、あらゆる接続待ちを終わらせます。realm への最初の参加をまだ待っている start()(たとえば、realm がまだ存在しない場合)は、Connection to realm <realm> stopped before it opened で失敗します。提供側への接続をまだ開いている途中の connect_to_app / connectToApp は Connection to app '<app>' (<stage>) aborted by stop() で失敗します。それでも接続が realm に参加した場合は、その接続を閉じたうえで Connection to app '<app>' (<stage>) closed by stop() で失敗します。stop() と競合した拒否のエラーは、CrossAppAccessError のままです。stop() から戻った時点で、接続を試みているものは何も残っていません。離脱に応答しないルーターとの接続は、5 秒後に切断されます。
Go では、Stop(ctx) もこれらの待機を終わらせ、wamp.ErrStopped をラップしたエラーを返します:まだ待機中の Start はそれを返し、まだ開いている途中の ConnectToApp は ironflock: connection to app '<app>' (<stage>) aborted by Stop(それでも接続が参加した場合は closed by Stop)で失敗します。Stop は ctx の範囲内で離脱を待ち、その後ソケットを閉じます。Run は 10 秒の猶予を与えます。
より厄介なのは、接続が何も知らせずに死ぬケースです。アイドル状態の接続を NAT やプロキシが両端に通知せずに切断したり、ロードバランサーの背後でルーターが再起動したりする場合です。切断を知らせるものは何も届きません。データを publish するだけのアプリは次の書き込み失敗で気づきますが、購読するだけのアプリはそもそも書き込みを行わないため、何もしなければコンテナが再起動するまで停止したままになります。
3 つの SDK すべてがこれを検出します。検出はデフォルトで有効で、設定は不要です。
Python
Python SDK は WebSocket キープアライブを使用します。リンクがアイドル状態のあいだ 30 秒ごとにルーターへ ping を送り、10 秒以内に pong が返ることを期待します。返らなければ接続を破棄して再接続します。受信トラフィックも生存の証拠とみなされるため、通信中の接続が探索されることはありません。ping が送られるのは、リンクが実際に静かになったときだけです。
connectToApp/connect_to_appで開いたアプリ間接続は通常は購読専用であり、まさにこの仕組みが守る対象です。これらは自動的に対象となり、ルーターに拒否された後も、拒否が続くまでは再接続します(connectToAppを参照)。
エラー処理
SDK のすべてのメソッドは、失敗を明示的に通知します。引数が不正な場合、接続が失われた場合、プラットフォームから拒否された場合には、操作名・トピック・理由を示すメッセージとともに例外を送出(Python)、リジェクト(JavaScript)、またはエラーを返却(Go)します。エラーが暗黙のうちに握りつぶされることはないため、処理を継続させたい呼び出しは try ブロックで囲んでください — Go では、返されたエラーを確認してください。
Python
try:
rows = await ironflock.getHistory("sensordata", {"limit": 100})
except ValueError as e:
# Invalid parameters — e.g. limit out of range, or a malformed filter
print(f"Bad query: {e}")
except RuntimeError as e:
# Not connected, table not in the data-template, or the platform rejected the call
print(f"Query failed: {e}")JavaScript では、プラットフォームに起因する失敗は WampError のインスタンスです。これは通常の Error のサブクラスで、加えて WAMP エラー URI を error に、エラーペイロードを args / kwargs に保持します。それ以外(パラメータの不正、未接続)はプレーンな Error です。
Go では、失敗した操作は *ironflock.OperationError を返します。そのメッセージは操作と原因を示し、原因をラップしています。errors.Is と errors.As はこれを透過して判定します。ルーターまたは呼び出し先による拒否は、URI、Args、Kwargs を持つ *ironflock.WampError(*wamp.Error)です。ironflock.WampURI(err) は URI を返し、なければ "" を返します。その他のセンチネルは ironflock.ErrMissingConfig(操作に必要な ID がない)、ironflock.ErrAlreadyStarted、wamp.ErrStopped(Stop の後)です。
移行時の注意: 以前のバージョンの Python および JavaScript SDK は、呼び出しが失敗するとメッセージをログに出力して
None/nullを返していました。現在は代わりに例外を送出するため、if result is None:のような形のコードでは失敗を検出できません。try/except(またはtry/catch)を使用してください。
データバックエンドによる拒否
データバックエンドが拒否した操作は、以下のいずれかの WAMP エラー URI で失敗し、その最初の引数が理由です。3 つの SDK すべてが、これらを定数としてエクスポートしています。Go では Go のスタイルで命名されています(ironflock.URIRateLimited、ironflock.URIStorageFull、…)。Python では、IronFlock のメソッドは autobahn の ApplicationError を原因(cause)とする RuntimeError を送出し(消費先アプリの読み取りは ApplicationError そのものを送出します)、wamp_error_uri(err) はどちらからでも URI を読み取ります。JavaScript では、エラーは WampError で、その error が URI です。Go では、*ironflock.WampError を包む *ironflock.OperationError で、ironflock.WampURI(err) が URI を読み取ります。SDK はこれらのいずれも再試行しません。データバックエンドが拒否した公開済みの行は、エラーなしに破棄されます — それを報告するのは append メソッドだけです。
Python
from datetime import datetime, timezone
from ironflock import URI_STORAGE_FULL, wamp_error_uri
row = {"tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5}
try:
await ironflock.append_to_table("sensordata", row)
except RuntimeError as err:
if wamp_error_uri(err) == URI_STORAGE_FULL:
pending.append(row) # the appliance's disk is nearly full: append the row later
else:
raise| 定数 | 拒否される内容 |
|---|---|
URI_RATE_LIMITED | デバイス上のアプリ資格情報ごとに 1 分あたり 30 回を超える revealSecrets / reveal_secrets、120 回を超える verifySecret / verify_secret の呼び出し |
URI_RESULT_TOO_LARGE | 8 MiB のメッセージ予算を超える単一の行を含む読み取り、または 1 GiB の上限を超える結果(その他の大きな結果はチャンクに分割されて届き、SDK が再構成します)。キーワード引数で最も大きなカラムが示されます:limit を下げる、timeRange を狭める、columns を減らす、または columnPaths で絞り込んでください |
URI_INVALID_LIMIT | limit が 3000 を超える transform の読み取り |
URI_INVALID_TIME_RANGE、URI_INVALID_METRIC、URI_INVALID_GROUP_BY、URI_SERIES_TOO_MANY_GROUPS | テーブルが応答できない系列クエリ:開始値がない、または終了値が開始値より後でない。テーブルが集約できないメトリクス(存在しないカラム、シークレットカラム、カラムの型が受け付けないメソッド、COUNT 以外のメソッドを指定した tsp)。グループ化できない groupBy カラム。50,000 行を超える結果 |
URI_SECRET_COLUMN、URI_NOT_A_SECRET_COLUMN | シークレットカラムに対するフィルタ。シークレットではないカラムに対する verifySecret / verify_secret、またはシークレットカラムを持たないテーブルの revealSecrets / reveal_secrets |
URI_STORAGE_FULL | アプライアンスのディスクがほぼいっぱいの状態での追加(空き容量が約 3 GiB 未満。4 GiB 以上で再び受け付けられます) — 一時的なものです:行は後で追加してください |
URI_STORAGE_OVERUSAGE | アカウントのストレージ割り当てを超える追加。すぐに再試行しても解決しません |
URI_ENTITY_KEY_CONFLICT | エンティティキー(maintainLatestFlagFor)と tsp が、バッチ内または保存済みの別の行とすでに同じである行:すべての行にそれぞれ独自の tsp を与えてください |
URI_SECRET_SENTINEL_UNRESOLVABLE、URI_SECRET_CIPHERTEXT_REJECTED | シークレットの以前の値を維持する(__secret__)書き込みで、その値を見つけるためのエンティティキーがないもの。すでに暗号化された値の書き込み |
データバックエンドが理由なしに拒否した追加 — tsp のない行に対するデータバックエンドの応答である wamp.error.runtime_error — は、型付きの拒否ではありません:SDK のエラーメッセージは、欠落している tsp と、データバックエンドが原因を記録するアプリの error-logs テーブルを指し示します。
ブラウザでの使用(JavaScript のみ)
JavaScript SDK はモダンブラウザで動作します。ブラウザには環境変数がないため、すべての設定をコンストラクタで渡してください:
import { IronFlock } from "ironflock";
const ironflock = new IronFlock({
serialNumber: "device-serial-from-server",
deviceKey: "12",
appName: "MyWebApp",
swarmKey: 10,
appKey: 20,
env: "PROD",
});
await ironflock.start();
await ironflock.publishToTable("sensordata", [{ tsp: new Date().toISOString(), temperature: 22 }]);資格情報をハードコードする代わりに、IronFlock.fromServer() を使用してバックエンドから設定を取得できます:
const ironflock = await IronFlock.fromServer("/api/ironflock-config");
await ironflock.start();バックエンドのエンドポイントは、接続オプション(serialNumber、deviceKey、appName、swarmKey、appKey、env)を含む JSON オブジェクトを返す必要があります。
AI エージェント関数の登録
SDK は AI エージェントから呼び出し可能な関数を登録できます。プロシージャを登録し、そのトピックを ai-template.yml で参照してください:
Python
def get_sensor_reading(sensor_id):
"""Returns the latest reading from a sensor."""
reading = read_from_hardware(sensor_id)
return {
"sensor_id": sensor_id,
"temperature": reading.temp,
"humidity": reading.hum,
"timestamp": reading.ts
}
await ironflock.register_device_function("sensors.get_latest", get_sensor_reading)ユーザーがリアルタイムのセンサーデータを必要とする質問をした場合、AI エージェントはこの関数を呼び出すことができます。
登録した WAMP トピックを AI エージェントに接続するには、アプリの .ironflock/ai-template.yml で参照してください:
sensor_agent:
tool_description: |
ユーザーがセンサーの読み取り値、ライブデバイスデータ、または
現在の環境条件について質問した場合に、このエージェントに委任してください。
system_prompt: |
あなたはセンサーデータの専門家です。get_current を使って任意のセンサーから
最新の読み取り値を取得してください。回答には必ず単位を含めてください。
main: true
max_context_tokens: 30000
max_iterations: 5
tools:
get_current:
description: センサーの最新の読み取り値を返します。
topic: sensors.get_latest
parameters:
sensor_id:
type: string
description: クエリ対象のセンサー識別子。
required: truetopic の値(sensors.get_latest)は、エッジコードで register_device_function / registerDeviceFunction / RegisterDeviceFunction に渡した名前と一致している必要があります。IronFlock は、関数が登録されているデバイスに自動的に呼び出しをルーティングします。
ai-template.yml の完全なリファレンスについては、エージェントとツールの定義を参照してください。
環境変数
以下の変数は、アプリコンテナ内で IronFlock ランタイムによって自動的に設定されます:
| 変数 | 説明 |
|---|---|
DEVICE_NAME | デバイスの表示名 |
DEVICE_SERIAL_NUMBER | 一意で不変のデバイス識別子 |
DEVICE_KEY | 認証用のデバイスキー |
SWARM_KEY | プロジェクト識別子 |
APP_KEY | アプリ識別子 |
APP_NAME | アプリ名 |
ENV | 環境:DEV または PROD |
APP_AUTH_ID、APP_AUTH_SECRET | SDK がルーターに提示する、アプリ自身の資格情報。アプリ間アクセスにはこれが必要です。これがない場合(古いデバイスエージェント)、SDK はデバイスのシリアル番号にフォールバックします |
Python
import os
device_name = os.environ.get("DEVICE_NAME")
serial = os.environ.get("DEVICE_SERIAL_NUMBER")
project_key = os.environ.get("SWARM_KEY")リモートアクセス用の変数(TUNNEL_DOMAIN、DEVICE_LAN_IP、DEVICE_PORT_FOR_<port>、REMOTE_PORT_FOR_<port>、…)は、ポートの設定 に記載されています。
デバイスエージェントはこれらの値を /data/env にも 1 つずつ <NAME>.txt ファイルとしてミラーし、アプリの実行中はそのミラーを最新の状態に保ちます。SDK はまずこれを読み取るため、リモートアクセス用のポートなど、実行時に変わる値も実行中のアプリに届きます。アプリごとの資格情報は接続を試みるたびに読み直されるため、アプリの実行中にエージェントがローテーションした資格情報は、次の再接続で提示されます。エージェントは /data/env を root だけが読み取れるようにしています:別のユーザーとして実行されるコンテナでは、SDK はコンテナの起動時の値を参照し、警告を 1 回ログに記録します。